1007 lines
34 KiB
JavaScript
1007 lines
34 KiB
JavaScript
/* Copyright (c) 2006-2012 by OpenLayers Contributors (see authors.txt for
|
|
* full list of contributors). Published under the 2-clause BSD license.
|
|
* See license.txt in the OpenLayers distribution or repository for the
|
|
* full text of the license. */
|
|
|
|
/**
|
|
* @requires OpenLayers/Renderer/Elements.js
|
|
*/
|
|
|
|
/**
|
|
* Class: OpenLayers.Renderer.SVG
|
|
*
|
|
* Inherits:
|
|
* - <OpenLayers.Renderer.Elements>
|
|
*/
|
|
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,
|
|
|
|
/**
|
|
* 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.symbolMetrics = {};
|
|
},
|
|
|
|
/**
|
|
* 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 - {<OpenLayers.Bounds>}
|
|
* 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) {
|
|
var coordSysUnchanged = OpenLayers.Renderer.Elements.prototype.setExtent.apply(this, arguments);
|
|
|
|
var resolution = this.getResolution(),
|
|
left = -extent.left / resolution,
|
|
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(this.xOffset, 0);
|
|
return true;
|
|
} else {
|
|
var inRange = this.translate(left - this.left + this.xOffset, top - this.top);
|
|
if (!inRange) {
|
|
// recenter the coordinate system
|
|
this.setExtent(extent, true);
|
|
}
|
|
return coordSysUnchanged && 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 - {<OpenLayers.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 - {<OpenLayers.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 = "svg";
|
|
} 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":
|
|
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);
|
|
//Standards-conformant SVG
|
|
// Prevent duplicate nodes. See issue https://github.com/openlayers/openlayers/issues/92
|
|
var titleNode = node.getElementsByTagName("title");
|
|
if (titleNode.length > 0) {
|
|
titleNode[0].firstChild.textContent = style.graphicTitle;
|
|
} else {
|
|
var label = this.nodeFactory(null, "title");
|
|
label.textContent = style.graphicTitle;
|
|
node.appendChild(label);
|
|
}
|
|
}
|
|
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);
|
|
node.onclick = OpenLayers.Renderer.SVG.preventDefault;
|
|
} 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 src = this.importSymbol(style.graphicName);
|
|
pos = this.getPosition(node);
|
|
widthFactor = this.symbolMetrics[src.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);
|
|
}
|
|
|
|
// The more appropriate way to implement this would be use/defs,
|
|
// but due to various issues in several browsers, it is safer to
|
|
// copy the symbols instead of referencing them.
|
|
// See e.g. ticket http://trac.osgeo.org/openlayers/ticket/2985
|
|
// and this email thread
|
|
// http://osgeo-org.1803224.n2.nabble.com/Select-Control-Ctrl-click-on-Feature-with-a-graphicName-opens-new-browser-window-tc5846039.html
|
|
node.firstChild && node.removeChild(node.firstChild);
|
|
node.appendChild(src.firstChild.cloneNode(true));
|
|
node.setAttributeNS(null, "viewBox", src.getAttributeNS(null, "viewBox"));
|
|
|
|
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[src.id];
|
|
node.firstChild.setAttributeNS(null, "transform", "rotate("
|
|
+ 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() {
|
|
var svg = this.nodeFactory(this.container.id + "_svgRoot", "svg");
|
|
svg.style.position = "absolute";
|
|
return svg;
|
|
},
|
|
|
|
/**
|
|
* Method: createRoot
|
|
*
|
|
* Parameters:
|
|
* 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 - {<OpenLayers.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 - {<OpenLayers.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 - this.featureDx) / 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 - {<OpenLayers.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 - {<OpenLayers.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 - {<OpenLayers.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<len; j++) {
|
|
d += " M";
|
|
linearRingResult = this.getComponentsString(
|
|
geometry.components[j].components, " ");
|
|
path = linearRingResult.path;
|
|
if (path) {
|
|
d += " " + path;
|
|
complete = linearRingResult.complete && complete;
|
|
} else {
|
|
draw = false;
|
|
}
|
|
}
|
|
d += " z";
|
|
if (draw) {
|
|
node.setAttributeNS(null, "d", d);
|
|
node.setAttributeNS(null, "fill-rule", "evenodd");
|
|
return complete ? node : null;
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Method: drawRectangle
|
|
* This method is only called by the renderer itself.
|
|
*
|
|
* Parameters:
|
|
* node - {DOMElement}
|
|
* geometry - {<OpenLayers.Geometry>}
|
|
*
|
|
* Returns:
|
|
* {DOMElement} or false if the renderer could not draw the rectangle
|
|
*/
|
|
drawRectangle: function(node, geometry) {
|
|
var resolution = this.getResolution();
|
|
var x = ((geometry.x - this.featureDx) / 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: drawText
|
|
* This method is only called by the renderer itself.
|
|
*
|
|
* Parameters:
|
|
* featureId - {String}
|
|
* style -
|
|
* location - {<OpenLayers.Geometry.Point>}
|
|
*/
|
|
drawText: function(featureId, style, location) {
|
|
var drawOutline = (!!style.labelOutlineWidth);
|
|
// First draw text in halo color and size and overlay the
|
|
// normal text afterwards
|
|
if (drawOutline) {
|
|
var outlineStyle = OpenLayers.Util.extend({}, style);
|
|
outlineStyle.fontColor = outlineStyle.labelOutlineColor;
|
|
outlineStyle.fontStrokeColor = outlineStyle.labelOutlineColor;
|
|
outlineStyle.fontStrokeWidth = style.labelOutlineWidth;
|
|
delete outlineStyle.labelOutlineWidth;
|
|
this.drawText(featureId, outlineStyle, location);
|
|
}
|
|
|
|
var resolution = this.getResolution();
|
|
|
|
var x = ((location.x - this.featureDx) / resolution + this.left);
|
|
var y = (location.y / resolution - this.top);
|
|
|
|
var suffix = (drawOutline)?this.LABEL_OUTLINE_SUFFIX:this.LABEL_ID_SUFFIX;
|
|
var label = this.nodeFactory(featureId + suffix, "text");
|
|
|
|
label.setAttributeNS(null, "x", x);
|
|
label.setAttributeNS(null, "y", -y);
|
|
|
|
if (style.fontColor) {
|
|
label.setAttributeNS(null, "fill", style.fontColor);
|
|
}
|
|
if (style.fontStrokeColor) {
|
|
label.setAttributeNS(null, "stroke", style.fontStrokeColor);
|
|
}
|
|
if (style.fontStrokeWidth) {
|
|
label.setAttributeNS(null, "stroke-width", style.fontStrokeWidth);
|
|
}
|
|
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.fontStyle) {
|
|
label.setAttributeNS(null, "font-style", style.fontStyle);
|
|
}
|
|
if (style.labelSelect === true) {
|
|
label.setAttributeNS(null, "pointer-events", "visible");
|
|
label._featureId = featureId;
|
|
} else {
|
|
label.setAttributeNS(null, "pointer-events", "none");
|
|
}
|
|
var align = style.labelAlign || OpenLayers.Renderer.defaultSymbolizer.labelAlign;
|
|
label.setAttributeNS(null, "text-anchor",
|
|
OpenLayers.Renderer.SVG.LABEL_ALIGN[align[0]] || "middle");
|
|
|
|
if (OpenLayers.IS_GECKO === true) {
|
|
label.setAttributeNS(null, "dominant-baseline",
|
|
OpenLayers.Renderer.SVG.LABEL_ALIGN[align[1]] || "central");
|
|
}
|
|
|
|
var labelRows = style.label.split('\n');
|
|
var numRows = labelRows.length;
|
|
while (label.childNodes.length > numRows) {
|
|
label.removeChild(label.lastChild);
|
|
}
|
|
for (var i = 0; i < numRows; i++) {
|
|
var tspan = this.nodeFactory(featureId + suffix + "_tspan_" + i, "tspan");
|
|
if (style.labelSelect === true) {
|
|
tspan._featureId = featureId;
|
|
tspan._geometry = location;
|
|
tspan._geometryClass = location.CLASS_NAME;
|
|
}
|
|
if (OpenLayers.IS_GECKO === false) {
|
|
tspan.setAttributeNS(null, "baseline-shift",
|
|
OpenLayers.Renderer.SVG.LABEL_VSHIFT[align[1]] || "-35%");
|
|
}
|
|
tspan.setAttribute("x", x);
|
|
if (i == 0) {
|
|
var vfactor = OpenLayers.Renderer.SVG.LABEL_VFACTOR[align[1]];
|
|
if (vfactor == null) {
|
|
vfactor = -.5;
|
|
}
|
|
tspan.setAttribute("dy", (vfactor*(numRows-1)) + "em");
|
|
} else {
|
|
tspan.setAttribute("dy", "1em");
|
|
}
|
|
tspan.textContent = (labelRows[i] === '') ? ' ' : labelRows[i];
|
|
if (!tspan.parentNode) {
|
|
label.appendChild(tspan);
|
|
}
|
|
}
|
|
|
|
if (!label.parentNode) {
|
|
this.textRoot.appendChild(label);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Method: getComponentString
|
|
*
|
|
* Parameters:
|
|
* components - {Array(<OpenLayers.Geometry.Point>)} 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<len; i++) {
|
|
component = components[i];
|
|
renderCmp.push(component);
|
|
str = this.getShortString(component);
|
|
if (str) {
|
|
strings.push(str);
|
|
} else {
|
|
// The current component is outside the valid range. Let's
|
|
// see if the previous or next component is inside the range.
|
|
// If so, add the coordinate of the intersection with the
|
|
// valid range bounds.
|
|
if (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 - {<OpenLayers.Geometry.Point>} original geometry of the
|
|
* invalid point
|
|
* goodComponent - {<OpenLayers.Geometry.Point>} 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 - this.featureDx) / resolution + this.left;
|
|
var y1 = this.top - goodComponent.y / resolution;
|
|
var x2 = (badComponent.x - this.featureDx) / 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 - {<OpenLayers.Geometry.Point>}
|
|
*
|
|
* Returns:
|
|
* {String} or false if point is outside the valid range
|
|
*/
|
|
getShortString: function(point) {
|
|
var resolution = this.getResolution();
|
|
var x = ((point.x - this.featureDx) / 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:
|
|
* {DOMElement} - 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
|
|
var existing = document.getElementById(id);
|
|
if (existing != null) {
|
|
return existing;
|
|
}
|
|
|
|
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<symbol.length; i=i+2) {
|
|
x = symbol[i];
|
|
y = symbol[i+1];
|
|
symbolExtent.left = Math.min(symbolExtent.left, x);
|
|
symbolExtent.bottom = Math.min(symbolExtent.bottom, y);
|
|
symbolExtent.right = Math.max(symbolExtent.right, x);
|
|
symbolExtent.top = Math.max(symbolExtent.top, y);
|
|
points.push(x, ",", y);
|
|
}
|
|
|
|
node.setAttributeNS(null, "points", points.join(" "));
|
|
|
|
var width = symbolExtent.getWidth();
|
|
var height = symbolExtent.getHeight();
|
|
// create a viewBox three times as large as the symbol itself,
|
|
// to allow for strokeWidth being displayed correctly at the corners.
|
|
var viewBox = [symbolExtent.left - width,
|
|
symbolExtent.bottom - height, width * 3, height * 3];
|
|
symbolNode.setAttributeNS(null, "viewBox", viewBox.join(" "));
|
|
this.symbolMetrics[id] = [
|
|
Math.max(width, height),
|
|
symbolExtent.getCenterLonLat().lon,
|
|
symbolExtent.getCenterLonLat().lat
|
|
];
|
|
|
|
this.defs.appendChild(symbolNode);
|
|
return symbolNode;
|
|
},
|
|
|
|
/**
|
|
* Method: getFeatureIdFromEvent
|
|
*
|
|
* Parameters:
|
|
* evt - {Object} An <OpenLayers.Event> object
|
|
*
|
|
* Returns:
|
|
* {String} A feature id or undefined.
|
|
*/
|
|
getFeatureIdFromEvent: function(evt) {
|
|
var featureId = OpenLayers.Renderer.Elements.prototype.getFeatureIdFromEvent.apply(this, arguments);
|
|
if(!featureId) {
|
|
var target = evt.target;
|
|
featureId = target.parentNode && target != this.rendererRoot ?
|
|
target.parentNode._featureId : undefined;
|
|
}
|
|
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"
|
|
};
|
|
|
|
/**
|
|
* Constant: OpenLayers.Renderer.SVG.LABEL_VFACTOR
|
|
* {Object}
|
|
*/
|
|
OpenLayers.Renderer.SVG.LABEL_VFACTOR = {
|
|
"t": 0,
|
|
"b": -1
|
|
};
|
|
|
|
/**
|
|
* Function: OpenLayers.Renderer.SVG.preventDefault
|
|
* Used to prevent default events (especially opening images in a new tab on
|
|
* ctrl-click) from being executed for externalGraphic symbols
|
|
*/
|
|
OpenLayers.Renderer.SVG.preventDefault = function(e) {
|
|
e.preventDefault && e.preventDefault();
|
|
};
|