/* Copyright (c) 2006-2010 by OpenLayers Contributors (see authors.txt for * full list of contributors). Published under the Clear BSD license. * See http://svn.openlayers.org/trunk/openlayers/license.txt for the * full text of the license. */ /** * @requires OpenLayers/Renderer/Elements.js */ /** * Class: OpenLayers.Renderer.SVG * * Inherits: * - */ OpenLayers.Renderer.SVG = OpenLayers.Class(OpenLayers.Renderer.Elements, { /** * Property: xmlns * {String} */ xmlns: "http://www.w3.org/2000/svg", /** * Property: xlinkns * {String} */ xlinkns: "http://www.w3.org/1999/xlink", /** * Constant: MAX_PIXEL * {Integer} Firefox has a limitation where values larger or smaller than * about 15000 in an SVG document lock the browser up. This * works around it. */ MAX_PIXEL: 15000, /** * Property: translationParameters * {Object} Hash with "x" and "y" properties */ translationParameters: null, /** * Property: symbolMetrics * {Object} Cache for symbol metrics according to their svg coordinate * space. This is an object keyed by the symbol's id, and values are * an array of [width, centerX, centerY]. */ symbolMetrics: null, /** * Property: isGecko * {Boolean} */ isGecko: null, /** * Property: supportUse * {Boolean} true if defs/use is supported - known to not work as expected * at least in some applewebkit/5* builds. * See https://bugs.webkit.org/show_bug.cgi?id=33322 */ supportUse: null, /** * Constructor: OpenLayers.Renderer.SVG * * Parameters: * containerID - {String} */ initialize: function(containerID) { if (!this.supported()) { return; } OpenLayers.Renderer.Elements.prototype.initialize.apply(this, arguments); this.translationParameters = {x: 0, y: 0}; this.supportUse = (navigator.userAgent.toLowerCase().indexOf("applewebkit/5") == -1); this.isGecko = (navigator.userAgent.toLowerCase().indexOf("gecko/") != -1); this.symbolMetrics = {}; }, /** * APIMethod: destroy */ destroy: function() { OpenLayers.Renderer.Elements.prototype.destroy.apply(this, arguments); }, /** * APIMethod: supported * * Returns: * {Boolean} Whether or not the browser supports the SVG renderer */ supported: function() { var svgFeature = "http://www.w3.org/TR/SVG11/feature#"; return (document.implementation && (document.implementation.hasFeature("org.w3c.svg", "1.0") || document.implementation.hasFeature(svgFeature + "SVG", "1.1") || document.implementation.hasFeature(svgFeature + "BasicStructure", "1.1") )); }, /** * Method: inValidRange * See #669 for more information * * Parameters: * x - {Integer} * y - {Integer} * xyOnly - {Boolean} whether or not to just check for x and y, which means * to not take the current translation parameters into account if true. * * Returns: * {Boolean} Whether or not the 'x' and 'y' coordinates are in the * valid range. */ inValidRange: function(x, y, xyOnly) { var left = x + (xyOnly ? 0 : this.translationParameters.x); var top = y + (xyOnly ? 0 : this.translationParameters.y); return (left >= -this.MAX_PIXEL && left <= this.MAX_PIXEL && top >= -this.MAX_PIXEL && top <= this.MAX_PIXEL); }, /** * Method: setExtent * * Parameters: * extent - {} * resolutionChanged - {Boolean} * * Returns: * {Boolean} true to notify the layer that the new extent does not exceed * the coordinate range, and the features will not need to be redrawn. * False otherwise. */ setExtent: function(extent, resolutionChanged) { OpenLayers.Renderer.Elements.prototype.setExtent.apply(this, arguments); var resolution = this.getResolution(); var left = -extent.left / resolution; var top = extent.top / resolution; // If the resolution has changed, start over changing the corner, because // the features will redraw. if (resolutionChanged) { this.left = left; this.top = top; // Set the viewbox var extentString = "0 0 " + this.size.w + " " + this.size.h; this.rendererRoot.setAttributeNS(null, "viewBox", extentString); this.translate(0, 0); return true; } else { var inRange = this.translate(left - this.left, top - this.top); if (!inRange) { // recenter the coordinate system this.setExtent(extent, true); } return inRange; } }, /** * Method: translate * Transforms the SVG coordinate system * * Parameters: * x - {Float} * y - {Float} * * Returns: * {Boolean} true if the translation parameters are in the valid coordinates * range, false otherwise. */ translate: function(x, y) { if (!this.inValidRange(x, y, true)) { return false; } else { var transformString = ""; if (x || y) { transformString = "translate(" + x + "," + y + ")"; } this.root.setAttributeNS(null, "transform", transformString); this.translationParameters = {x: x, y: y}; return true; } }, /** * Method: setSize * Sets the size of the drawing surface. * * Parameters: * size - {} The size of the drawing surface */ setSize: function(size) { OpenLayers.Renderer.prototype.setSize.apply(this, arguments); this.rendererRoot.setAttributeNS(null, "width", this.size.w); this.rendererRoot.setAttributeNS(null, "height", this.size.h); }, /** * Method: getNodeType * * Parameters: * geometry - {} * style - {Object} * * Returns: * {String} The corresponding node type for the specified geometry */ getNodeType: function(geometry, style) { var nodeType = null; switch (geometry.CLASS_NAME) { case "OpenLayers.Geometry.Point": if (style.externalGraphic) { nodeType = "image"; } else if (this.isComplexSymbol(style.graphicName)) { nodeType = this.supportUse === false ? "svg" : "use"; } else { nodeType = "circle"; } break; case "OpenLayers.Geometry.Rectangle": nodeType = "rect"; break; case "OpenLayers.Geometry.LineString": nodeType = "polyline"; break; case "OpenLayers.Geometry.LinearRing": nodeType = "polygon"; break; case "OpenLayers.Geometry.Polygon": case "OpenLayers.Geometry.Curve": case "OpenLayers.Geometry.Surface": nodeType = "path"; break; default: break; } return nodeType; }, /** * Method: setStyle * Use to set all the style attributes to a SVG node. * * Takes care to adjust stroke width and point radius to be * resolution-relative * * Parameters: * node - {SVGDomElement} An SVG element to decorate * style - {Object} * options - {Object} Currently supported options include * 'isFilled' {Boolean} and * 'isStroked' {Boolean} */ setStyle: function(node, style, options) { style = style || node._style; options = options || node._options; var r = parseFloat(node.getAttributeNS(null, "r")); var widthFactor = 1; var pos; if (node._geometryClass == "OpenLayers.Geometry.Point" && r) { node.style.visibility = ""; if (style.graphic === false) { node.style.visibility = "hidden"; } else if (style.externalGraphic) { pos = this.getPosition(node); if (style.graphicTitle) { node.setAttributeNS(null, "title", style.graphicTitle); } if (style.graphicWidth && style.graphicHeight) { node.setAttributeNS(null, "preserveAspectRatio", "none"); } var width = style.graphicWidth || style.graphicHeight; var height = style.graphicHeight || style.graphicWidth; width = width ? width : style.pointRadius*2; height = height ? height : style.pointRadius*2; var xOffset = (style.graphicXOffset != undefined) ? style.graphicXOffset : -(0.5 * width); var yOffset = (style.graphicYOffset != undefined) ? style.graphicYOffset : -(0.5 * height); var opacity = style.graphicOpacity || style.fillOpacity; node.setAttributeNS(null, "x", (pos.x + xOffset).toFixed()); node.setAttributeNS(null, "y", (pos.y + yOffset).toFixed()); node.setAttributeNS(null, "width", width); node.setAttributeNS(null, "height", height); node.setAttributeNS(this.xlinkns, "href", style.externalGraphic); node.setAttributeNS(null, "style", "opacity: "+opacity); } else if (this.isComplexSymbol(style.graphicName)) { // the symbol viewBox is three times as large as the symbol var offset = style.pointRadius * 3; var size = offset * 2; var id = this.importSymbol(style.graphicName); pos = this.getPosition(node); widthFactor = this.symbolMetrics[id][0] * 3 / size; // remove the node from the dom before we modify it. This // prevents various rendering issues in Safari and FF var parent = node.parentNode; var nextSibling = node.nextSibling; if(parent) { parent.removeChild(node); } if(this.supportUse === false) { // workaround for webkit versions that cannot do defs/use // (see https://bugs.webkit.org/show_bug.cgi?id=33322): // copy the symbol instead of referencing it var src = document.getElementById(id); node.firstChild && node.removeChild(node.firstChild); node.appendChild(src.firstChild.cloneNode(true)); node.setAttributeNS(null, "viewBox", src.getAttributeNS(null, "viewBox")); } else { node.setAttributeNS(this.xlinkns, "href", "#" + id); } node.setAttributeNS(null, "width", size); node.setAttributeNS(null, "height", size); node.setAttributeNS(null, "x", pos.x - offset); node.setAttributeNS(null, "y", pos.y - offset); // now that the node has all its new properties, insert it // back into the dom where it was if(nextSibling) { parent.insertBefore(node, nextSibling); } else if(parent) { parent.appendChild(node); } } else { node.setAttributeNS(null, "r", style.pointRadius); } var rotation = style.rotation; if ((rotation !== undefined || node._rotation !== undefined) && pos) { node._rotation = rotation; rotation |= 0; if(node.nodeName !== "svg") { node.setAttributeNS(null, "transform", "rotate(" + rotation + " " + pos.x + " " + pos.y + ")"); } else { var metrics = this.symbolMetrics[id]; node.firstChild.setAttributeNS(null, "transform", "rotate(" + style.rotation + " " + metrics[1] + " " + metrics[2] + ")"); } } } if (options.isFilled) { node.setAttributeNS(null, "fill", style.fillColor); node.setAttributeNS(null, "fill-opacity", style.fillOpacity); } else { node.setAttributeNS(null, "fill", "none"); } if (options.isStroked) { node.setAttributeNS(null, "stroke", style.strokeColor); node.setAttributeNS(null, "stroke-opacity", style.strokeOpacity); node.setAttributeNS(null, "stroke-width", style.strokeWidth * widthFactor); node.setAttributeNS(null, "stroke-linecap", style.strokeLinecap || "round"); // Hard-coded linejoin for now, to make it look the same as in VML. // There is no strokeLinejoin property yet for symbolizers. node.setAttributeNS(null, "stroke-linejoin", "round"); style.strokeDashstyle && node.setAttributeNS(null, "stroke-dasharray", this.dashStyle(style, widthFactor)); } else { node.setAttributeNS(null, "stroke", "none"); } if (style.pointerEvents) { node.setAttributeNS(null, "pointer-events", style.pointerEvents); } if (style.cursor != null) { node.setAttributeNS(null, "cursor", style.cursor); } return node; }, /** * Method: dashStyle * * Parameters: * style - {Object} * widthFactor - {Number} * * Returns: * {String} A SVG compliant 'stroke-dasharray' value */ dashStyle: function(style, widthFactor) { var w = style.strokeWidth * widthFactor; var str = style.strokeDashstyle; switch (str) { case 'solid': return 'none'; case 'dot': return [1, 4 * w].join(); case 'dash': return [4 * w, 4 * w].join(); case 'dashdot': return [4 * w, 4 * w, 1, 4 * w].join(); case 'longdash': return [8 * w, 4 * w].join(); case 'longdashdot': return [8 * w, 4 * w, 1, 4 * w].join(); default: return OpenLayers.String.trim(str).replace(/\s+/g, ","); } }, /** * Method: createNode * * Parameters: * type - {String} Kind of node to draw * id - {String} Id for node * * Returns: * {DOMElement} A new node of the given type and id */ createNode: function(type, id) { var node = document.createElementNS(this.xmlns, type); if (id) { node.setAttributeNS(null, "id", id); } return node; }, /** * Method: nodeTypeCompare * * Parameters: * node - {SVGDomElement} An SVG element * type - {String} Kind of node * * Returns: * {Boolean} Whether or not the specified node is of the specified type */ nodeTypeCompare: function(node, type) { return (type == node.nodeName); }, /** * Method: createRenderRoot * * Returns: * {DOMElement} The specific render engine's root element */ createRenderRoot: function() { return this.nodeFactory(this.container.id + "_svgRoot", "svg"); }, /** * Method: createRoot * * Parameter: * suffix - {String} suffix to append to the id * * Returns: * {DOMElement} */ createRoot: function(suffix) { return this.nodeFactory(this.container.id + suffix, "g"); }, /** * Method: createDefs * * Returns: * {DOMElement} The element to which we'll add the symbol definitions */ createDefs: function() { var defs = this.nodeFactory(this.container.id + "_defs", "defs"); this.rendererRoot.appendChild(defs); return defs; }, /************************************** * * * GEOMETRY DRAWING FUNCTIONS * * * **************************************/ /** * Method: drawPoint * This method is only called by the renderer itself. * * Parameters: * node - {DOMElement} * geometry - {} * * Returns: * {DOMElement} or false if the renderer could not draw the point */ drawPoint: function(node, geometry) { return this.drawCircle(node, geometry, 1); }, /** * Method: drawCircle * This method is only called by the renderer itself. * * Parameters: * node - {DOMElement} * geometry - {} * radius - {Float} * * Returns: * {DOMElement} or false if the renderer could not draw the circle */ drawCircle: function(node, geometry, radius) { var resolution = this.getResolution(); var x = (geometry.x / resolution + this.left); var y = (this.top - geometry.y / resolution); if (this.inValidRange(x, y)) { node.setAttributeNS(null, "cx", x); node.setAttributeNS(null, "cy", y); node.setAttributeNS(null, "r", radius); return node; } else { return false; } }, /** * Method: drawLineString * This method is only called by the renderer itself. * * Parameters: * node - {DOMElement} * geometry - {} * * Returns: * {DOMElement} or null if the renderer could not draw all components of * the linestring, or false if nothing could be drawn */ drawLineString: function(node, geometry) { var componentsResult = this.getComponentsString(geometry.components); if (componentsResult.path) { node.setAttributeNS(null, "points", componentsResult.path); return (componentsResult.complete ? node : null); } else { return false; } }, /** * Method: drawLinearRing * This method is only called by the renderer itself. * * Parameters: * node - {DOMElement} * geometry - {} * * Returns: * {DOMElement} or null if the renderer could not draw all components * of the linear ring, or false if nothing could be drawn */ drawLinearRing: function(node, geometry) { var componentsResult = this.getComponentsString(geometry.components); if (componentsResult.path) { node.setAttributeNS(null, "points", componentsResult.path); return (componentsResult.complete ? node : null); } else { return false; } }, /** * Method: drawPolygon * This method is only called by the renderer itself. * * Parameters: * node - {DOMElement} * geometry - {} * * Returns: * {DOMElement} or null if the renderer could not draw all components * of the polygon, or false if nothing could be drawn */ drawPolygon: function(node, geometry) { var d = ""; var draw = true; var complete = true; var linearRingResult, path; for (var j=0, len=geometry.components.length; j} * * Returns: * {DOMElement} or false if the renderer could not draw the rectangle */ drawRectangle: function(node, geometry) { var resolution = this.getResolution(); var x = (geometry.x / resolution + this.left); var y = (this.top - geometry.y / resolution); if (this.inValidRange(x, y)) { node.setAttributeNS(null, "x", x); node.setAttributeNS(null, "y", y); node.setAttributeNS(null, "width", geometry.width / resolution); node.setAttributeNS(null, "height", geometry.height / resolution); return node; } else { return false; } }, /** * Method: drawSurface * This method is only called by the renderer itself. * * Parameters: * node - {DOMElement} * geometry - {} * * Returns: * {DOMElement} or false if the renderer could not draw the surface */ drawSurface: function(node, geometry) { // create the svg path string representation var d = null; var draw = true; for (var i=0, len=geometry.components.length; i} */ drawText: function(featureId, style, location) { var resolution = this.getResolution(); var x = (location.x / resolution + this.left); var y = (location.y / resolution - this.top); var label = this.nodeFactory(featureId + this.LABEL_ID_SUFFIX, "text"); var tspan = this.nodeFactory(featureId + this.LABEL_ID_SUFFIX + "_tspan", "tspan"); label.setAttributeNS(null, "x", x); label.setAttributeNS(null, "y", -y); if (style.fontColor) { label.setAttributeNS(null, "fill", style.fontColor); } if (style.fontOpacity) { label.setAttributeNS(null, "opacity", style.fontOpacity); } if (style.fontFamily) { label.setAttributeNS(null, "font-family", style.fontFamily); } if (style.fontSize) { label.setAttributeNS(null, "font-size", style.fontSize); } if (style.fontWeight) { label.setAttributeNS(null, "font-weight", style.fontWeight); } if(style.labelSelect === true) { label.setAttributeNS(null, "pointer-events", "visible"); label._featureId = featureId; tspan._featureId = featureId; tspan._geometry = location; tspan._geometryClass = location.CLASS_NAME; } else { label.setAttributeNS(null, "pointer-events", "none"); } var align = style.labelAlign || "cm"; label.setAttributeNS(null, "text-anchor", OpenLayers.Renderer.SVG.LABEL_ALIGN[align[0]] || "middle"); if (this.isGecko) { label.setAttributeNS(null, "dominant-baseline", OpenLayers.Renderer.SVG.LABEL_ALIGN[align[1]] || "central"); } else { tspan.setAttributeNS(null, "baseline-shift", OpenLayers.Renderer.SVG.LABEL_VSHIFT[align[1]] || "-35%"); } tspan.textContent = style.label; if(!label.parentNode) { label.appendChild(tspan); this.textRoot.appendChild(label); } }, /** * Method: getComponentString * * Parameters: * components - {Array()} Array of points * separator - {String} character between coordinate pairs. Defaults to "," * * Returns: * {Object} hash with properties "path" (the string created from the * components and "complete" (false if the renderer was unable to * draw all components) */ getComponentsString: function(components, separator) { var renderCmp = []; var complete = true; var len = components.length; var strings = []; var str, component; for(var i=0; i 0) { if (this.getShortString(components[i - 1])) { strings.push(this.clipLine(components[i], components[i-1])); } } if (i < len - 1) { if (this.getShortString(components[i + 1])) { strings.push(this.clipLine(components[i], components[i+1])); } } complete = false; } } return { path: strings.join(separator || ","), complete: complete }; }, /** * Method: clipLine * Given two points (one inside the valid range, and one outside), * clips the line betweeen the two points so that the new points are both * inside the valid range. * * Parameters: * badComponent - {)} original geometry of the * invalid point * goodComponent - {)} original geometry of the * valid point * Returns * {String} the SVG coordinate pair of the clipped point (like * getShortString), or an empty string if both passed componets are at * the same point. */ clipLine: function(badComponent, goodComponent) { if (goodComponent.equals(badComponent)) { return ""; } var resolution = this.getResolution(); var maxX = this.MAX_PIXEL - this.translationParameters.x; var maxY = this.MAX_PIXEL - this.translationParameters.y; var x1 = goodComponent.x / resolution + this.left; var y1 = this.top - goodComponent.y / resolution; var x2 = badComponent.x / resolution + this.left; var y2 = this.top - badComponent.y / resolution; var k; if (x2 < -maxX || x2 > maxX) { k = (y2 - y1) / (x2 - x1); x2 = x2 < 0 ? -maxX : maxX; y2 = y1 + (x2 - x1) * k; } if (y2 < -maxY || y2 > maxY) { k = (x2 - x1) / (y2 - y1); y2 = y2 < 0 ? -maxY : maxY; x2 = x1 + (y2 - y1) * k; } return x2 + "," + y2; }, /** * Method: getShortString * * Parameters: * point - {} * * Returns: * {String} or false if point is outside the valid range */ getShortString: function(point) { var resolution = this.getResolution(); var x = (point.x / resolution + this.left); var y = (this.top - point.y / resolution); if (this.inValidRange(x, y)) { return x + "," + y; } else { return false; } }, /** * Method: getPosition * Finds the position of an svg node. * * Parameters: * node - {DOMElement} * * Returns: * {Object} hash with x and y properties, representing the coordinates * within the svg coordinate system */ getPosition: function(node) { return({ x: parseFloat(node.getAttributeNS(null, "cx")), y: parseFloat(node.getAttributeNS(null, "cy")) }); }, /** * Method: importSymbol * add a new symbol definition from the rendererer's symbol hash * * Parameters: * graphicName - {String} name of the symbol to import * * Returns: * {String} - id of the imported symbol */ importSymbol: function (graphicName) { if (!this.defs) { // create svg defs tag this.defs = this.createDefs(); } var id = this.container.id + "-" + graphicName; // check if symbol already exists in the defs if (document.getElementById(id) != null) { return id; } var symbol = OpenLayers.Renderer.symbol[graphicName]; if (!symbol) { throw new Error(graphicName + ' is not a valid symbol name'); } var symbolNode = this.nodeFactory(id, "symbol"); var node = this.nodeFactory(null, "polygon"); symbolNode.appendChild(node); var symbolExtent = new OpenLayers.Bounds( Number.MAX_VALUE, Number.MAX_VALUE, 0, 0); var points = []; var x,y; for (var i=0; i object * * Returns: * {} A geometry from an event that * happened on a layer. */ getFeatureIdFromEvent: function(evt) { var featureId = OpenLayers.Renderer.Elements.prototype.getFeatureIdFromEvent.apply(this, arguments); if(this.supportUse === false && !featureId) { var target = evt.target; featureId = target.parentNode && target != this.rendererRoot && target.parentNode._featureId; } return featureId; }, CLASS_NAME: "OpenLayers.Renderer.SVG" }); /** * Constant: OpenLayers.Renderer.SVG.LABEL_ALIGN * {Object} */ OpenLayers.Renderer.SVG.LABEL_ALIGN = { "l": "start", "r": "end", "b": "bottom", "t": "hanging" }; /** * Constant: OpenLayers.Renderer.SVG.LABEL_VSHIFT * {Object} */ OpenLayers.Renderer.SVG.LABEL_VSHIFT = { // according to // http://www.w3.org/Graphics/SVG/Test/20061213/htmlObjectHarness/full-text-align-02-b.html // a baseline-shift of -70% shifts the text exactly from the // bottom to the top of the baseline, so -35% moves the text to // the center of the baseline. "t": "-70%", "b": "0" };