1 /* Copyright (c) 2006-2008 MetaCarta, Inc., published under the Clear BSD
2 * license. See http://svn.openlayers.org/trunk/openlayers/license.txt for the
3 * full text of the license. */
6 * @requires OpenLayers/Renderer/Elements.js
10 * Class: OpenLayers.Renderer.VML
11 * Render vector features in browsers with VML capability. Construct a new
12 * VML renderer with the <OpenLayers.Renderer.VML> constructor.
14 * Note that for all calculations in this class, we use toFixed() to round a
15 * float value to an integer. This is done because it seems that VML doesn't
16 * support float values.
19 * - <OpenLayers.Renderer.Elements>
21 OpenLayers.Renderer.VML = OpenLayers.Class(OpenLayers.Renderer.Elements, {
25 * {String} XML Namespace URN
27 xmlns: "urn:schemas-microsoft-com:vml",
30 * Property: symbolCache
31 * {DOMElement} node holding symbols. This hash is keyed by symbol name,
32 * and each value is a hash with a "path" and an "extent" property.
38 * {Object} Hash with "x" and "y" properties
43 * Constructor: OpenLayers.Renderer.VML
44 * Create a new VML renderer.
47 * containerID - {String} The id for the element that contains the renderer
49 initialize: function(containerID) {
50 if (!this.supported()) {
53 if (!document.namespaces.olv) {
54 document.namespaces.add("olv", this.xmlns);
55 var style = document.createStyleSheet();
56 var shapes = ['shape','rect', 'oval', 'fill', 'stroke', 'imagedata', 'group','textbox'];
57 for (var i = 0, len = shapes.length; i < len; i++) {
59 style.addRule('olv\\:' + shapes[i], "behavior: url(#default#VML); " +
60 "position: absolute; display: inline-block;");
64 OpenLayers.Renderer.Elements.prototype.initialize.apply(this,
66 this.offset = {x: 0, y: 0};
71 * Deconstruct the renderer.
74 OpenLayers.Renderer.Elements.prototype.destroy.apply(this, arguments);
78 * APIMethod: supported
79 * Determine whether a browser supports this renderer.
82 * {Boolean} The browser supports the VML renderer
84 supported: function() {
85 return !!(document.namespaces);
90 * Set the renderer's extent
93 * extent - {<OpenLayers.Bounds>}
94 * resolutionChanged - {Boolean}
97 * {Boolean} true to notify the layer that the new extent does not exceed
98 * the coordinate range, and the features will not need to be redrawn.
100 setExtent: function(extent, resolutionChanged) {
101 OpenLayers.Renderer.Elements.prototype.setExtent.apply(this,
103 var resolution = this.getResolution();
105 var left = extent.left/resolution;
106 var top = extent.top/resolution - this.size.h;
107 if (resolutionChanged) {
108 this.offset = {x: left, y: top};
112 left = left - this.offset.x;
113 top = top - this.offset.y;
117 var org = left + " " + top;
118 this.root.coordorigin = org;
119 var roots = [this.root, this.vectorRoot, this.textRoot];
121 for(var i=0, len=roots.length; i<len; ++i) {
124 var size = this.size.w + " " + this.size.h;
125 root.coordsize = size;
128 // flip the VML display Y axis upside down so it
129 // matches the display Y axis of the map
130 this.root.style.flip = "y";
138 * Set the size of the drawing surface
141 * size - {<OpenLayers.Size>} the size of the drawing surface
143 setSize: function(size) {
144 OpenLayers.Renderer.prototype.setSize.apply(this, arguments);
146 // setting width and height on all roots to avoid flicker which we
147 // would get with 100% width and height on child roots
154 var w = this.size.w + "px";
155 var h = this.size.h + "px";
157 for(var i=0, len=roots.length; i<len; ++i) {
159 root.style.width = w;
160 root.style.height = h;
165 * Method: getNodeType
166 * Get the node type for a geometry and style
169 * geometry - {<OpenLayers.Geometry>}
173 * {String} The corresponding node type for the specified geometry
175 getNodeType: function(geometry, style) {
177 switch (geometry.CLASS_NAME) {
178 case "OpenLayers.Geometry.Point":
179 if (style.externalGraphic) {
180 nodeType = "olv:rect";
181 } else if (this.isComplexSymbol(style.graphicName)) {
182 nodeType = "olv:shape";
184 nodeType = "olv:oval";
187 case "OpenLayers.Geometry.Rectangle":
188 nodeType = "olv:rect";
190 case "OpenLayers.Geometry.LineString":
191 case "OpenLayers.Geometry.LinearRing":
192 case "OpenLayers.Geometry.Polygon":
193 case "OpenLayers.Geometry.Curve":
194 case "OpenLayers.Geometry.Surface":
195 nodeType = "olv:shape";
205 * Use to set all the style attributes to a VML node.
208 * node - {DOMElement} An VML element to decorate
210 * options - {Object} Currently supported options include
211 * 'isFilled' {Boolean} and
212 * 'isStroked' {Boolean}
213 * geometry - {<OpenLayers.Geometry>}
215 setStyle: function(node, style, options, geometry) {
216 style = style || node._style;
217 options = options || node._options;
220 if (node._geometryClass == "OpenLayers.Geometry.Point") {
221 if (style.externalGraphic) {
222 if (style.graphicTitle) {
223 node.title=style.graphicTitle;
225 var width = style.graphicWidth || style.graphicHeight;
226 var height = style.graphicHeight || style.graphicWidth;
227 width = width ? width : style.pointRadius*2;
228 height = height ? height : style.pointRadius*2;
230 var resolution = this.getResolution();
231 var xOffset = (style.graphicXOffset != undefined) ?
232 style.graphicXOffset : -(0.5 * width);
233 var yOffset = (style.graphicYOffset != undefined) ?
234 style.graphicYOffset : -(0.5 * height);
236 node.style.left = ((geometry.x/resolution - this.offset.x)+xOffset).toFixed();
237 node.style.top = ((geometry.y/resolution - this.offset.y)-(yOffset+height)).toFixed();
238 node.style.width = width + "px";
239 node.style.height = height + "px";
240 node.style.flip = "y";
242 // modify style/options for fill and stroke styling below
243 style.fillColor = "none";
244 options.isStroked = false;
245 } else if (this.isComplexSymbol(style.graphicName)) {
246 var cache = this.importSymbol(style.graphicName);
247 node.path = cache.path;
248 node.coordorigin = cache.left + "," + cache.bottom;
249 var size = cache.size;
250 node.coordsize = size + "," + size;
251 this.drawCircle(node, geometry, style.pointRadius);
252 node.style.flip = "y";
254 this.drawCircle(node, geometry, style.pointRadius);
259 if (options.isFilled) {
260 node.fillcolor = style.fillColor;
262 node.filled = "false";
264 var fills = node.getElementsByTagName("fill");
265 var fill = (fills.length == 0) ? null : fills[0];
266 if (!options.isFilled) {
268 node.removeChild(fill);
272 fill = this.createNode('olv:fill', node.id + "_fill");
274 fill.opacity = style.fillOpacity;
276 if (node._geometryClass == "OpenLayers.Geometry.Point" &&
277 style.externalGraphic) {
279 // override fillOpacity
280 if (style.graphicOpacity) {
281 fill.opacity = style.graphicOpacity;
284 fill.src = style.externalGraphic;
287 if (!(style.graphicWidth && style.graphicHeight)) {
288 fill.aspect = "atmost";
291 if (fill.parentNode != node) {
292 node.appendChild(fill);
296 // additional rendering for rotated graphics or symbols
297 if (typeof style.rotation != "undefined") {
298 if (style.externalGraphic) {
299 this.graphicRotate(node, xOffset, yOffset);
300 // make the fill fully transparent, because we now have
301 // the graphic as imagedata element. We cannot just remove
302 // the fill, because this is part of the hack described
306 node.style.rotation = style.rotation;
311 if (options.isStroked) {
312 node.strokecolor = style.strokeColor;
313 node.strokeweight = style.strokeWidth + "px";
315 node.stroked = false;
317 var strokes = node.getElementsByTagName("stroke");
318 var stroke = (strokes.length == 0) ? null : strokes[0];
319 if (!options.isStroked) {
321 node.removeChild(stroke);
325 stroke = this.createNode('olv:stroke', node.id + "_stroke");
326 node.appendChild(stroke);
328 stroke.opacity = style.strokeOpacity;
329 stroke.endcap = !style.strokeLinecap || style.strokeLinecap == 'butt' ? 'flat' : style.strokeLinecap;
330 stroke.dashstyle = this.dashStyle(style);
333 if (style.cursor != "inherit" && style.cursor != null) {
334 node.style.cursor = style.cursor;
340 * Method: graphicRotate
341 * If a point is to be styled with externalGraphic and rotation, VML fills
342 * cannot be used to display the graphic, because rotation of graphic
343 * fills is not supported by the VML implementation of Internet Explorer.
344 * This method creates a olv:imagedata element inside the VML node,
345 * DXImageTransform.Matrix and BasicImage filters for rotation and
346 * opacity, and a 3-step hack to remove rendering artefacts from the
347 * graphic and preserve the ability of graphics to trigger events.
348 * Finally, OpenLayers methods are used to determine the correct
349 * insertion point of the rotated image, because DXImageTransform.Matrix
350 * does the rotation without the ability to specify a rotation center
354 * node - {DOMElement}
355 * xOffset - {Number} rotation center relative to image, x coordinate
356 * yOffset - {Number} rotation center relative to image, y coordinate
358 graphicRotate: function(node, xOffset, yOffset) {
359 var style = style || node._style;
360 var options = node._options;
362 var aspectRatio, size;
363 if (!(style.graphicWidth && style.graphicHeight)) {
364 // load the image to determine its size
365 var img = new Image();
366 img.onreadystatechange = OpenLayers.Function.bind(function() {
367 if(img.readyState == "complete" ||
368 img.readyState == "interactive") {
369 aspectRatio = img.width / img.height;
370 size = Math.max(style.pointRadius * 2,
371 style.graphicWidth || 0,
372 style.graphicHeight || 0);
373 xOffset = xOffset * aspectRatio;
374 style.graphicWidth = size * aspectRatio;
375 style.graphicHeight = size;
376 this.graphicRotate(node, xOffset, yOffset);
379 img.src = style.externalGraphic;
381 // will be called again by the onreadystate handler
384 size = Math.max(style.graphicWidth, style.graphicHeight);
385 aspectRatio = style.graphicWidth / style.graphicHeight;
388 var width = Math.round(style.graphicWidth || size * aspectRatio);
389 var height = Math.round(style.graphicHeight || size);
390 node.style.width = width + "px";
391 node.style.height = height + "px";
393 // Three steps are required to remove artefacts for images with
394 // transparent backgrounds (resulting from using DXImageTransform
395 // filters on svg objects), while preserving awareness for browser
397 // - Use the fill as usual (like for unrotated images) to handle
399 // - specify an imagedata element with the same src as the fill
400 // - style the imagedata element with an AlphaImageLoader filter
402 var image = document.getElementById(node.id + "_image");
404 image = this.createNode("olv:imagedata", node.id + "_image");
405 node.appendChild(image);
407 image.style.width = width + "px";
408 image.style.height = height + "px";
409 image.src = style.externalGraphic;
411 "progid:DXImageTransform.Microsoft.AlphaImageLoader(" +
412 "src='', sizingMethod='scale')";
414 var rotation = style.rotation * Math.PI / 180;
415 var sintheta = Math.sin(rotation);
416 var costheta = Math.cos(rotation);
418 // do the rotation on the image
420 "progid:DXImageTransform.Microsoft.Matrix(M11=" + costheta +
421 ",M12=" + (-sintheta) + ",M21=" + sintheta + ",M22=" + costheta +
422 ",SizingMethod='auto expand')\n";
424 // set the opacity (needed for the imagedata)
425 var opacity = style.graphicOpacity || style.fillOpacity;
426 if (opacity && opacity != 1) {
428 "progid:DXImageTransform.Microsoft.BasicImage(opacity=" +
431 node.style.filter = filter;
433 // do the rotation again on a box, so we know the insertion point
434 var centerPoint = new OpenLayers.Geometry.Point(-xOffset, -yOffset);
435 var imgBox = new OpenLayers.Bounds(0, 0, width, height).toGeometry();
436 imgBox.rotate(style.rotation, centerPoint);
437 var imgBounds = imgBox.getBounds();
439 node.style.left = Math.round(
440 parseInt(node.style.left) + imgBounds.left) + "px";
441 node.style.top = Math.round(
442 parseInt(node.style.top) - imgBounds.bottom) + "px";
447 * Some versions of Internet Explorer seem to be unable to set fillcolor
448 * and strokecolor to "none" correctly before the fill node is appended to
449 * a visible vml node. This method takes care of that and sets fillcolor
450 * and strokecolor again if needed.
453 * node - {DOMElement}
455 postDraw: function(node) {
456 var fillColor = node._style.fillColor;
457 var strokeColor = node._style.strokeColor;
458 if (fillColor == "none" &&
459 node.fillcolor != fillColor) {
460 node.fillcolor = fillColor;
462 if (strokeColor == "none" &&
463 node.strokecolor != strokeColor) {
464 node.strokecolor = strokeColor;
470 * Method: setNodeDimension
471 * Get the geometry's bounds, convert it to our vml coordinate system,
472 * then set the node's position, size, and local coordinate system.
475 * node - {DOMElement}
476 * geometry - {<OpenLayers.Geometry>}
478 setNodeDimension: function(node, geometry) {
480 var bbox = geometry.getBounds();
482 var resolution = this.getResolution();
485 new OpenLayers.Bounds((bbox.left/resolution - this.offset.x).toFixed(),
486 (bbox.bottom/resolution - this.offset.y).toFixed(),
487 (bbox.right/resolution - this.offset.x).toFixed(),
488 (bbox.top/resolution - this.offset.y).toFixed());
490 // Set the internal coordinate system to draw the path
491 node.style.left = scaledBox.left + "px";
492 node.style.top = scaledBox.top + "px";
493 node.style.width = scaledBox.getWidth() + "px";
494 node.style.height = scaledBox.getHeight() + "px";
496 node.coordorigin = scaledBox.left + " " + scaledBox.top;
497 node.coordsize = scaledBox.getWidth()+ " " + scaledBox.getHeight();
508 * {String} A VML compliant 'stroke-dasharray' value
510 dashStyle: function(style) {
511 var dash = style.strokeDashstyle;
521 // very basic guessing of dash style patterns
522 var parts = dash.split(/[ ,]/);
523 if (parts.length == 2) {
524 if (1*parts[0] >= 2*parts[1]) {
527 return (parts[0] == 1 || parts[1] == 1) ? "dot" : "dash";
528 } else if (parts.length == 4) {
529 return (1*parts[0] >= 2*parts[1]) ? "longdashdot" :
541 * type - {String} Kind of node to draw
542 * id - {String} Id for node
545 * {DOMElement} A new node of the given type and id
547 createNode: function(type, id) {
548 var node = document.createElement(type);
553 // IE hack to make elements unselectable, to prevent 'blue flash'
554 // while dragging vectors; #1410
555 node.unselectable = 'on';
556 node.onselectstart = function() { return(false); };
562 * Method: nodeTypeCompare
563 * Determine whether a node is of a given type
566 * node - {DOMElement} An VML element
567 * type - {String} Kind of node
570 * {Boolean} Whether or not the specified node is of the specified type
572 nodeTypeCompare: function(node, type) {
576 var splitIndex = subType.indexOf(":");
577 if (splitIndex != -1) {
578 subType = subType.substr(splitIndex+1);
582 var nodeName = node.nodeName;
583 splitIndex = nodeName.indexOf(":");
584 if (splitIndex != -1) {
585 nodeName = nodeName.substr(splitIndex+1);
588 return (subType == nodeName);
592 * Method: createRenderRoot
593 * Create the renderer root
596 * {DOMElement} The specific render engine's root element
598 createRenderRoot: function() {
599 return this.nodeFactory(this.container.id + "_vmlRoot", "div");
604 * Create the main root element
607 * suffix - {String} suffix to append to the id
612 createRoot: function(suffix) {
613 return this.nodeFactory(this.container.id + suffix, "olv:group");
616 /**************************************
618 * GEOMETRY DRAWING FUNCTIONS *
620 **************************************/
627 * node - {DOMElement}
628 * geometry - {<OpenLayers.Geometry>}
631 * {DOMElement} or false if the point could not be drawn
633 drawPoint: function(node, geometry) {
634 return this.drawCircle(node, geometry, 1);
640 * Size and Center a circle given geometry (x,y center) and radius
643 * node - {DOMElement}
644 * geometry - {<OpenLayers.Geometry>}
648 * {DOMElement} or false if the circle could not ne drawn
650 drawCircle: function(node, geometry, radius) {
651 if(!isNaN(geometry.x)&& !isNaN(geometry.y)) {
652 var resolution = this.getResolution();
654 node.style.left = ((geometry.x /resolution - this.offset.x).toFixed() - radius) + "px";
655 node.style.top = ((geometry.y /resolution - this.offset.y).toFixed() - radius) + "px";
657 var diameter = radius * 2;
659 node.style.width = diameter + "px";
660 node.style.height = diameter + "px";
668 * Method: drawLineString
669 * Render a linestring.
672 * node - {DOMElement}
673 * geometry - {<OpenLayers.Geometry>}
678 drawLineString: function(node, geometry) {
679 return this.drawLine(node, geometry, false);
683 * Method: drawLinearRing
684 * Render a linearring
687 * node - {DOMElement}
688 * geometry - {<OpenLayers.Geometry>}
693 drawLinearRing: function(node, geometry) {
694 return this.drawLine(node, geometry, true);
702 * node - {DOMElement}
703 * geometry - {<OpenLayers.Geometry>}
704 * closeLine - {Boolean} Close the line? (make it a ring?)
709 drawLine: function(node, geometry, closeLine) {
711 this.setNodeDimension(node, geometry);
713 var resolution = this.getResolution();
714 var numComponents = geometry.components.length;
715 var parts = new Array(numComponents);
718 for (var i = 0; i < numComponents; i++) {
719 comp = geometry.components[i];
720 x = (comp.x/resolution - this.offset.x);
721 y = (comp.y/resolution - this.offset.y);
722 parts[i] = " " + x.toFixed() + "," + y.toFixed() + " l ";
724 var end = (closeLine) ? " x e" : " e";
725 node.path = "m" + parts.join("") + end;
730 * Method: drawPolygon
734 * node - {DOMElement}
735 * geometry - {<OpenLayers.Geometry>}
740 drawPolygon: function(node, geometry) {
741 this.setNodeDimension(node, geometry);
743 var resolution = this.getResolution();
746 var linearRing, i, j, len, ilen, comp, x, y;
747 for (j = 0, len=geometry.components.length; j<len; j++) {
748 linearRing = geometry.components[j];
751 for (i=0, ilen=linearRing.components.length; i<ilen; i++) {
752 comp = linearRing.components[i];
753 x = comp.x / resolution - this.offset.x;
754 y = comp.y / resolution - this.offset.y;
755 path.push(" " + x.toFixed() + "," + y.toFixed());
763 node.path = path.join("");
768 * Method: drawRectangle
772 * node - {DOMElement}
773 * geometry - {<OpenLayers.Geometry>}
778 drawRectangle: function(node, geometry) {
779 var resolution = this.getResolution();
781 node.style.left = (geometry.x/resolution - this.offset.x) + "px";
782 node.style.top = (geometry.y/resolution - this.offset.y) + "px";
783 node.style.width = geometry.width/resolution + "px";
784 node.style.height = geometry.height/resolution + "px";
791 * This method is only called by the renderer itself.
794 * featureId - {String}
796 * location - {<OpenLayers.Geometry.Point>}
798 drawText: function(featureId, style, location) {
799 var label = this.nodeFactory(featureId + this.LABEL_ID_SUFFIX, "olv:rect");
800 var textbox = this.nodeFactory(featureId + this.LABEL_ID_SUFFIX + "_textbox", "olv:textbox");
802 var resolution = this.getResolution();
803 label.style.left = (location.x/resolution - this.offset.x).toFixed() + "px";
804 label.style.top = (location.y/resolution - this.offset.y).toFixed() + "px";
805 label.style.flip = "y";
807 textbox.innerText = style.label;
809 if (style.fillColor) {
810 textbox.style.color = style.fontColor;
812 if (style.fontFamily) {
813 textbox.style.fontFamily = style.fontFamily;
815 if (style.fontSize) {
816 textbox.style.fontSize = style.fontSize;
818 if (style.fontWeight) {
819 textbox.style.fontWeight = style.fontWeight;
821 textbox.style.whiteSpace = "nowrap";
822 // fun with IE: IE7 in standards compliant mode does not display any
823 // text with a left inset of 0. So we set this to 1px and subtract one
824 // pixel later when we set label.style.left
825 textbox.inset = "1px,0px,0px,0px";
827 if(!label.parentNode) {
828 label.appendChild(textbox);
829 this.textRoot.appendChild(label);
832 var align = style.labelAlign || "cm";
833 var xshift = textbox.clientWidth *
834 (OpenLayers.Renderer.VML.LABEL_SHIFT[align.substr(0,1)]);
835 var yshift = textbox.clientHeight *
836 (OpenLayers.Renderer.VML.LABEL_SHIFT[align.substr(1,1)]);
837 label.style.left = parseInt(label.style.left)-xshift-1+"px";
838 label.style.top = parseInt(label.style.top)+yshift+"px";
842 * Method: drawSurface
845 * node - {DOMElement}
846 * geometry - {<OpenLayers.Geometry>}
851 drawSurface: function(node, geometry) {
853 this.setNodeDimension(node, geometry);
855 var resolution = this.getResolution();
859 for (var i=0, len=geometry.components.length; i<len; i++) {
860 comp = geometry.components[i];
861 x = comp.x / resolution - this.offset.x;
862 y = comp.y / resolution - this.offset.y;
863 if ((i%3)==0 && (i/3)==0) {
865 } else if ((i%3)==1) {
868 path.push(" " + x + "," + y);
872 node.path = path.join("");
878 * moves this renderer's root to a different renderer.
881 * renderer - {<OpenLayers.Renderer>} target renderer for the moved root
882 * root - {DOMElement} optional root node. To be used when this renderer
883 * holds roots from multiple layers to tell this method which one to
887 * {Boolean} true if successful, false otherwise
889 moveRoot: function(renderer) {
890 var layer = this.map.getLayer(renderer.container.id);
891 if(layer instanceof OpenLayers.Layer.Vector.RootContainer) {
892 layer = this.map.getLayer(this.container.id);
894 layer && layer.renderer.clear();
895 OpenLayers.Renderer.Elements.prototype.moveRoot.apply(this, arguments);
896 layer && layer.redraw();
900 * Method: importSymbol
901 * add a new symbol definition from the rendererer's symbol hash
904 * graphicName - {String} name of the symbol to import
907 * {Object} - hash of {DOMElement} "symbol" and {Number} "size"
909 importSymbol: function (graphicName) {
910 var id = this.container.id + "-" + graphicName;
912 // check if symbol already exists in the cache
913 var cache = this.symbolCache[id];
918 var symbol = OpenLayers.Renderer.symbol[graphicName];
920 throw new Error(graphicName + ' is not a valid symbol name');
924 var symbolExtent = new OpenLayers.Bounds(
925 Number.MAX_VALUE, Number.MAX_VALUE, 0, 0);
927 var pathitems = ["m"];
928 for (var i=0; i<symbol.length; i=i+2) {
931 symbolExtent.left = Math.min(symbolExtent.left, x);
932 symbolExtent.bottom = Math.min(symbolExtent.bottom, y);
933 symbolExtent.right = Math.max(symbolExtent.right, x);
934 symbolExtent.top = Math.max(symbolExtent.top, y);
942 pathitems.push("x e");
943 var path = pathitems.join(" ");
945 var diff = (symbolExtent.getWidth() - symbolExtent.getHeight()) / 2;
947 symbolExtent.bottom = symbolExtent.bottom - diff;
948 symbolExtent.top = symbolExtent.top + diff;
950 symbolExtent.left = symbolExtent.left - diff;
951 symbolExtent.right = symbolExtent.right + diff;
956 size: symbolExtent.getWidth(), // equals getHeight() now
957 left: symbolExtent.left,
958 bottom: symbolExtent.bottom
960 this.symbolCache[id] = cache;
965 CLASS_NAME: "OpenLayers.Renderer.VML"
969 * Constant: OpenLayers.Renderer.VML.LABEL_SHIFT
972 OpenLayers.Renderer.VML.LABEL_SHIFT = {