diff options
Diffstat (limited to 'misc/openlayers/lib/OpenLayers/Renderer')
-rw-r--r-- | misc/openlayers/lib/OpenLayers/Renderer/Canvas.js | 906 | ||||
-rw-r--r-- | misc/openlayers/lib/OpenLayers/Renderer/Elements.js | 1053 | ||||
-rw-r--r-- | misc/openlayers/lib/OpenLayers/Renderer/SVG.js | 1012 | ||||
-rw-r--r-- | misc/openlayers/lib/OpenLayers/Renderer/VML.js | 985 |
4 files changed, 3956 insertions, 0 deletions
diff --git a/misc/openlayers/lib/OpenLayers/Renderer/Canvas.js b/misc/openlayers/lib/OpenLayers/Renderer/Canvas.js new file mode 100644 index 0000000..61a327c --- /dev/null +++ b/misc/openlayers/lib/OpenLayers/Renderer/Canvas.js @@ -0,0 +1,906 @@ +/* Copyright (c) 2006-2013 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.js + */ + +/** + * Class: OpenLayers.Renderer.Canvas + * A renderer based on the 2D 'canvas' drawing element. + * + * Inherits: + * - <OpenLayers.Renderer> + */ +OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, { + + /** + * APIProperty: hitDetection + * {Boolean} Allow for hit detection of features. Default is true. + */ + hitDetection: true, + + /** + * Property: hitOverflow + * {Number} The method for converting feature identifiers to color values + * supports 16777215 sequential values. Two features cannot be + * predictably detected if their identifiers differ by more than this + * value. The hitOverflow allows for bigger numbers (but the + * difference in values is still limited). + */ + hitOverflow: 0, + + /** + * Property: canvas + * {Canvas} The canvas context object. + */ + canvas: null, + + /** + * Property: features + * {Object} Internal object of feature/style pairs for use in redrawing the layer. + */ + features: null, + + /** + * Property: pendingRedraw + * {Boolean} The renderer needs a redraw call to render features added while + * the renderer was locked. + */ + pendingRedraw: false, + + /** + * Property: cachedSymbolBounds + * {Object} Internal cache of calculated symbol extents. + */ + cachedSymbolBounds: {}, + + /** + * Constructor: OpenLayers.Renderer.Canvas + * + * Parameters: + * containerID - {<String>} + * options - {Object} Optional properties to be set on the renderer. + */ + initialize: function(containerID, options) { + OpenLayers.Renderer.prototype.initialize.apply(this, arguments); + this.root = document.createElement("canvas"); + this.container.appendChild(this.root); + this.canvas = this.root.getContext("2d"); + this.features = {}; + if (this.hitDetection) { + this.hitCanvas = document.createElement("canvas"); + this.hitContext = this.hitCanvas.getContext("2d"); + } + }, + + /** + * Method: setExtent + * Set the visible part of the layer. + * + * 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() { + OpenLayers.Renderer.prototype.setExtent.apply(this, arguments); + // always redraw features + return false; + }, + + /** + * Method: eraseGeometry + * Erase a geometry from the renderer. Because the Canvas renderer has + * 'memory' of the features that it has drawn, we have to remove the + * feature so it doesn't redraw. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * featureId - {String} + */ + eraseGeometry: function(geometry, featureId) { + this.eraseFeatures(this.features[featureId][0]); + }, + + /** + * APIMethod: supported + * + * Returns: + * {Boolean} Whether or not the browser supports the renderer class + */ + supported: function() { + return OpenLayers.CANVAS_SUPPORTED; + }, + + /** + * Method: setSize + * Sets the size of the drawing surface. + * + * Once the size is updated, redraw the canvas. + * + * Parameters: + * size - {<OpenLayers.Size>} + */ + setSize: function(size) { + this.size = size.clone(); + var root = this.root; + root.style.width = size.w + "px"; + root.style.height = size.h + "px"; + root.width = size.w; + root.height = size.h; + this.resolution = null; + if (this.hitDetection) { + var hitCanvas = this.hitCanvas; + hitCanvas.style.width = size.w + "px"; + hitCanvas.style.height = size.h + "px"; + hitCanvas.width = size.w; + hitCanvas.height = size.h; + } + }, + + /** + * Method: drawFeature + * Draw the feature. Stores the feature in the features list, + * then redraws the layer. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + * style - {<Object>} + * + * Returns: + * {Boolean} The feature has been drawn completely. If the feature has no + * geometry, undefined will be returned. If the feature is not rendered + * for other reasons, false will be returned. + */ + drawFeature: function(feature, style) { + var rendered; + if (feature.geometry) { + style = this.applyDefaultSymbolizer(style || feature.style); + // don't render if display none or feature outside extent + var bounds = feature.geometry.getBounds(); + + var worldBounds; + if (this.map.baseLayer && this.map.baseLayer.wrapDateLine) { + worldBounds = this.map.getMaxExtent(); + } + + var intersects = bounds && bounds.intersectsBounds(this.extent, {worldBounds: worldBounds}); + + rendered = (style.display !== "none") && !!bounds && intersects; + if (rendered) { + // keep track of what we have rendered for redraw + this.features[feature.id] = [feature, style]; + } + else { + // remove from features tracked for redraw + delete(this.features[feature.id]); + } + this.pendingRedraw = true; + } + if (this.pendingRedraw && !this.locked) { + this.redraw(); + this.pendingRedraw = false; + } + return rendered; + }, + + /** + * Method: drawGeometry + * Used when looping (in redraw) over the features; draws + * the canvas. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + */ + drawGeometry: function(geometry, style, featureId) { + var className = geometry.CLASS_NAME; + if ((className == "OpenLayers.Geometry.Collection") || + (className == "OpenLayers.Geometry.MultiPoint") || + (className == "OpenLayers.Geometry.MultiLineString") || + (className == "OpenLayers.Geometry.MultiPolygon")) { + for (var i = 0; i < geometry.components.length; i++) { + this.drawGeometry(geometry.components[i], style, featureId); + } + return; + } + switch (geometry.CLASS_NAME) { + case "OpenLayers.Geometry.Point": + this.drawPoint(geometry, style, featureId); + break; + case "OpenLayers.Geometry.LineString": + this.drawLineString(geometry, style, featureId); + break; + case "OpenLayers.Geometry.LinearRing": + this.drawLinearRing(geometry, style, featureId); + break; + case "OpenLayers.Geometry.Polygon": + this.drawPolygon(geometry, style, featureId); + break; + default: + break; + } + }, + + /** + * Method: drawExternalGraphic + * Called to draw External graphics. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + */ + drawExternalGraphic: function(geometry, style, featureId) { + var img = new Image(); + + var title = style.title || style.graphicTitle; + if (title) { + img.title = title; + } + + 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; + + var onLoad = function() { + if(!this.features[featureId]) { + return; + } + var pt = this.getLocalXY(geometry); + var p0 = pt[0]; + var p1 = pt[1]; + if(!isNaN(p0) && !isNaN(p1)) { + var x = (p0 + xOffset) | 0; + var y = (p1 + yOffset) | 0; + var canvas = this.canvas; + canvas.globalAlpha = opacity; + var factor = OpenLayers.Renderer.Canvas.drawImageScaleFactor || + (OpenLayers.Renderer.Canvas.drawImageScaleFactor = + /android 2.1/.test(navigator.userAgent.toLowerCase()) ? + // 320 is the screen width of the G1 phone, for + // which drawImage works out of the box. + 320 / window.screen.width : 1 + ); + canvas.drawImage( + img, x*factor, y*factor, width*factor, height*factor + ); + if (this.hitDetection) { + this.setHitContextStyle("fill", featureId); + this.hitContext.fillRect(x, y, width, height); + } + } + }; + + img.onload = OpenLayers.Function.bind(onLoad, this); + img.src = style.externalGraphic; + }, + + /** + * Method: drawNamedSymbol + * Called to draw Well Known Graphic Symbol Name. + * This method is only called by the renderer itself. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + */ + drawNamedSymbol: function(geometry, style, featureId) { + var x, y, cx, cy, i, symbolBounds, scaling, angle; + var unscaledStrokeWidth; + var deg2rad = Math.PI / 180.0; + + var symbol = OpenLayers.Renderer.symbol[style.graphicName]; + + if (!symbol) { + throw new Error(style.graphicName + ' is not a valid symbol name'); + } + + if (!symbol.length || symbol.length < 2) return; + + var pt = this.getLocalXY(geometry); + var p0 = pt[0]; + var p1 = pt[1]; + + if (isNaN(p0) || isNaN(p1)) return; + + // Use rounded line caps + this.canvas.lineCap = "round"; + this.canvas.lineJoin = "round"; + + if (this.hitDetection) { + this.hitContext.lineCap = "round"; + this.hitContext.lineJoin = "round"; + } + + // Scale and rotate symbols, using precalculated bounds whenever possible. + if (style.graphicName in this.cachedSymbolBounds) { + symbolBounds = this.cachedSymbolBounds[style.graphicName]; + } else { + symbolBounds = new OpenLayers.Bounds(); + for(i = 0; i < symbol.length; i+=2) { + symbolBounds.extend(new OpenLayers.LonLat(symbol[i], symbol[i+1])); + } + this.cachedSymbolBounds[style.graphicName] = symbolBounds; + } + + // Push symbol scaling, translation and rotation onto the transformation stack in reverse order. + // Don't forget to apply all canvas transformations to the hitContext canvas as well(!) + this.canvas.save(); + if (this.hitDetection) { this.hitContext.save(); } + + // Step 3: place symbol at the desired location + this.canvas.translate(p0,p1); + if (this.hitDetection) { this.hitContext.translate(p0,p1); } + + // Step 2a. rotate the symbol if necessary + angle = deg2rad * style.rotation; // will be NaN when style.rotation is undefined. + if (!isNaN(angle)) { + this.canvas.rotate(angle); + if (this.hitDetection) { this.hitContext.rotate(angle); } + } + + // // Step 2: scale symbol such that pointRadius equals half the maximum symbol dimension. + scaling = 2.0 * style.pointRadius / Math.max(symbolBounds.getWidth(), symbolBounds.getHeight()); + this.canvas.scale(scaling,scaling); + if (this.hitDetection) { this.hitContext.scale(scaling,scaling); } + + // Step 1: center the symbol at the origin + cx = symbolBounds.getCenterLonLat().lon; + cy = symbolBounds.getCenterLonLat().lat; + this.canvas.translate(-cx,-cy); + if (this.hitDetection) { this.hitContext.translate(-cx,-cy); } + + // Don't forget to scale stroke widths, because they are affected by canvas scale transformations as well(!) + // Alternative: scale symbol coordinates manually, so stroke width scaling is not needed anymore. + unscaledStrokeWidth = style.strokeWidth; + style.strokeWidth = unscaledStrokeWidth / scaling; + + if (style.fill !== false) { + this.setCanvasStyle("fill", style); + this.canvas.beginPath(); + for (i=0; i<symbol.length; i=i+2) { + x = symbol[i]; + y = symbol[i+1]; + if (i == 0) this.canvas.moveTo(x,y); + this.canvas.lineTo(x,y); + } + this.canvas.closePath(); + this.canvas.fill(); + + if (this.hitDetection) { + this.setHitContextStyle("fill", featureId, style); + this.hitContext.beginPath(); + for (i=0; i<symbol.length; i=i+2) { + x = symbol[i]; + y = symbol[i+1]; + if (i == 0) this.canvas.moveTo(x,y); + this.hitContext.lineTo(x,y); + } + this.hitContext.closePath(); + this.hitContext.fill(); + } + } + + if (style.stroke !== false) { + this.setCanvasStyle("stroke", style); + this.canvas.beginPath(); + for (i=0; i<symbol.length; i=i+2) { + x = symbol[i]; + y = symbol[i+1]; + if (i == 0) this.canvas.moveTo(x,y); + this.canvas.lineTo(x,y); + } + this.canvas.closePath(); + this.canvas.stroke(); + + + if (this.hitDetection) { + this.setHitContextStyle("stroke", featureId, style, scaling); + this.hitContext.beginPath(); + for (i=0; i<symbol.length; i=i+2) { + x = symbol[i]; + y = symbol[i+1]; + if (i == 0) this.hitContext.moveTo(x,y); + this.hitContext.lineTo(x,y); + } + this.hitContext.closePath(); + this.hitContext.stroke(); + } + + } + + style.strokeWidth = unscaledStrokeWidth; + this.canvas.restore(); + if (this.hitDetection) { this.hitContext.restore(); } + this.setCanvasStyle("reset"); + }, + + /** + * Method: setCanvasStyle + * Prepare the canvas for drawing by setting various global settings. + * + * Parameters: + * type - {String} one of 'stroke', 'fill', or 'reset' + * style - {Object} Symbolizer hash + */ + setCanvasStyle: function(type, style) { + if (type === "fill") { + this.canvas.globalAlpha = style['fillOpacity']; + this.canvas.fillStyle = style['fillColor']; + } else if (type === "stroke") { + this.canvas.globalAlpha = style['strokeOpacity']; + this.canvas.strokeStyle = style['strokeColor']; + this.canvas.lineWidth = style['strokeWidth']; + } else { + this.canvas.globalAlpha = 0; + this.canvas.lineWidth = 1; + } + }, + + /** + * Method: featureIdToHex + * Convert a feature ID string into an RGB hex string. + * + * Parameters: + * featureId - {String} Feature id + * + * Returns: + * {String} RGB hex string. + */ + featureIdToHex: function(featureId) { + var id = Number(featureId.split("_").pop()) + 1; // zero for no feature + if (id >= 16777216) { + this.hitOverflow = id - 16777215; + id = id % 16777216 + 1; + } + var hex = "000000" + id.toString(16); + var len = hex.length; + hex = "#" + hex.substring(len-6, len); + return hex; + }, + + /** + * Method: setHitContextStyle + * Prepare the hit canvas for drawing by setting various global settings. + * + * Parameters: + * type - {String} one of 'stroke', 'fill', or 'reset' + * featureId - {String} The feature id. + * symbolizer - {<OpenLayers.Symbolizer>} The symbolizer. + */ + setHitContextStyle: function(type, featureId, symbolizer, strokeScaling) { + var hex = this.featureIdToHex(featureId); + if (type == "fill") { + this.hitContext.globalAlpha = 1.0; + this.hitContext.fillStyle = hex; + } else if (type == "stroke") { + this.hitContext.globalAlpha = 1.0; + this.hitContext.strokeStyle = hex; + // bump up stroke width to deal with antialiasing. If strokeScaling is defined, we're rendering a symbol + // on a transformed canvas, so the antialias width bump has to scale as well. + if (typeof strokeScaling === "undefined") { + this.hitContext.lineWidth = symbolizer.strokeWidth + 2; + } else { + if (!isNaN(strokeScaling)) { this.hitContext.lineWidth = symbolizer.strokeWidth + 2.0 / strokeScaling; } + } + } else { + this.hitContext.globalAlpha = 0; + this.hitContext.lineWidth = 1; + } + }, + + /** + * Method: drawPoint + * This method is only called by the renderer itself. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + */ + drawPoint: function(geometry, style, featureId) { + if(style.graphic !== false) { + if(style.externalGraphic) { + this.drawExternalGraphic(geometry, style, featureId); + } else if (style.graphicName && (style.graphicName != "circle")) { + this.drawNamedSymbol(geometry, style, featureId); + } else { + var pt = this.getLocalXY(geometry); + var p0 = pt[0]; + var p1 = pt[1]; + if(!isNaN(p0) && !isNaN(p1)) { + var twoPi = Math.PI*2; + var radius = style.pointRadius; + if(style.fill !== false) { + this.setCanvasStyle("fill", style); + this.canvas.beginPath(); + this.canvas.arc(p0, p1, radius, 0, twoPi, true); + this.canvas.fill(); + if (this.hitDetection) { + this.setHitContextStyle("fill", featureId, style); + this.hitContext.beginPath(); + this.hitContext.arc(p0, p1, radius, 0, twoPi, true); + this.hitContext.fill(); + } + } + + if(style.stroke !== false) { + this.setCanvasStyle("stroke", style); + this.canvas.beginPath(); + this.canvas.arc(p0, p1, radius, 0, twoPi, true); + this.canvas.stroke(); + if (this.hitDetection) { + this.setHitContextStyle("stroke", featureId, style); + this.hitContext.beginPath(); + this.hitContext.arc(p0, p1, radius, 0, twoPi, true); + this.hitContext.stroke(); + } + this.setCanvasStyle("reset"); + } + } + } + } + }, + + /** + * Method: drawLineString + * This method is only called by the renderer itself. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + */ + drawLineString: function(geometry, style, featureId) { + style = OpenLayers.Util.applyDefaults({fill: false}, style); + this.drawLinearRing(geometry, style, featureId); + }, + + /** + * Method: drawLinearRing + * This method is only called by the renderer itself. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + */ + drawLinearRing: function(geometry, style, featureId) { + if (style.fill !== false) { + this.setCanvasStyle("fill", style); + this.renderPath(this.canvas, geometry, style, featureId, "fill"); + if (this.hitDetection) { + this.setHitContextStyle("fill", featureId, style); + this.renderPath(this.hitContext, geometry, style, featureId, "fill"); + } + } + if (style.stroke !== false) { + this.setCanvasStyle("stroke", style); + this.renderPath(this.canvas, geometry, style, featureId, "stroke"); + if (this.hitDetection) { + this.setHitContextStyle("stroke", featureId, style); + this.renderPath(this.hitContext, geometry, style, featureId, "stroke"); + } + } + this.setCanvasStyle("reset"); + }, + + /** + * Method: renderPath + * Render a path with stroke and optional fill. + */ + renderPath: function(context, geometry, style, featureId, type) { + var components = geometry.components; + var len = components.length; + context.beginPath(); + var start = this.getLocalXY(components[0]); + var x = start[0]; + var y = start[1]; + if (!isNaN(x) && !isNaN(y)) { + context.moveTo(start[0], start[1]); + for (var i=1; i<len; ++i) { + var pt = this.getLocalXY(components[i]); + context.lineTo(pt[0], pt[1]); + } + if (type === "fill") { + context.fill(); + } else { + context.stroke(); + } + } + }, + + /** + * Method: drawPolygon + * This method is only called by the renderer itself. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + */ + drawPolygon: function(geometry, style, featureId) { + var components = geometry.components; + var len = components.length; + this.drawLinearRing(components[0], style, featureId); + // erase inner rings + for (var i=1; i<len; ++i) { + /** + * Note that this is overly agressive. Here we punch holes through + * all previously rendered features on the same canvas. A better + * solution for polygons with interior rings would be to draw the + * polygon on a sketch canvas first. We could erase all holes + * there and then copy the drawing to the layer canvas. + * TODO: http://trac.osgeo.org/openlayers/ticket/3130 + */ + this.canvas.globalCompositeOperation = "destination-out"; + if (this.hitDetection) { + this.hitContext.globalCompositeOperation = "destination-out"; + } + this.drawLinearRing( + components[i], + OpenLayers.Util.applyDefaults({stroke: false, fillOpacity: 1.0}, style), + featureId + ); + this.canvas.globalCompositeOperation = "source-over"; + if (this.hitDetection) { + this.hitContext.globalCompositeOperation = "source-over"; + } + this.drawLinearRing( + components[i], + OpenLayers.Util.applyDefaults({fill: false}, style), + featureId + ); + } + }, + + /** + * Method: drawText + * This method is only called by the renderer itself. + * + * Parameters: + * location - {<OpenLayers.Point>} + * style - {Object} + */ + drawText: function(location, style) { + var pt = this.getLocalXY(location); + + this.setCanvasStyle("reset"); + this.canvas.fillStyle = style.fontColor; + this.canvas.globalAlpha = style.fontOpacity || 1.0; + var fontStyle = [style.fontStyle ? style.fontStyle : "normal", + "normal", // "font-variant" not supported + style.fontWeight ? style.fontWeight : "normal", + style.fontSize ? style.fontSize : "1em", + style.fontFamily ? style.fontFamily : "sans-serif"].join(" "); + var labelRows = style.label.split('\n'); + var numRows = labelRows.length; + if (this.canvas.fillText) { + // HTML5 + this.canvas.font = fontStyle; + this.canvas.textAlign = + OpenLayers.Renderer.Canvas.LABEL_ALIGN[style.labelAlign[0]] || + "center"; + this.canvas.textBaseline = + OpenLayers.Renderer.Canvas.LABEL_ALIGN[style.labelAlign[1]] || + "middle"; + var vfactor = + OpenLayers.Renderer.Canvas.LABEL_FACTOR[style.labelAlign[1]]; + if (vfactor == null) { + vfactor = -.5; + } + var lineHeight = + this.canvas.measureText('Mg').height || + this.canvas.measureText('xx').width; + pt[1] += lineHeight*vfactor*(numRows-1); + for (var i = 0; i < numRows; i++) { + if (style.labelOutlineWidth) { + this.canvas.save(); + this.canvas.globalAlpha = style.labelOutlineOpacity || style.fontOpacity || 1.0; + this.canvas.strokeStyle = style.labelOutlineColor; + this.canvas.lineWidth = style.labelOutlineWidth; + this.canvas.strokeText(labelRows[i], pt[0], pt[1] + (lineHeight*i) + 1); + this.canvas.restore(); + } + this.canvas.fillText(labelRows[i], pt[0], pt[1] + (lineHeight*i)); + } + } else if (this.canvas.mozDrawText) { + // Mozilla pre-Gecko1.9.1 (<FF3.1) + this.canvas.mozTextStyle = fontStyle; + // No built-in text alignment, so we measure and adjust the position + var hfactor = + OpenLayers.Renderer.Canvas.LABEL_FACTOR[style.labelAlign[0]]; + if (hfactor == null) { + hfactor = -.5; + } + var vfactor = + OpenLayers.Renderer.Canvas.LABEL_FACTOR[style.labelAlign[1]]; + if (vfactor == null) { + vfactor = -.5; + } + var lineHeight = this.canvas.mozMeasureText('xx'); + pt[1] += lineHeight*(1 + (vfactor*numRows)); + for (var i = 0; i < numRows; i++) { + var x = pt[0] + (hfactor*this.canvas.mozMeasureText(labelRows[i])); + var y = pt[1] + (i*lineHeight); + this.canvas.translate(x, y); + this.canvas.mozDrawText(labelRows[i]); + this.canvas.translate(-x, -y); + } + } + this.setCanvasStyle("reset"); + }, + + /** + * Method: getLocalXY + * transform geographic xy into pixel xy + * + * Parameters: + * point - {<OpenLayers.Geometry.Point>} + */ + getLocalXY: function(point) { + var resolution = this.getResolution(); + var extent = this.extent; + var x = ((point.x - this.featureDx) / resolution + (-extent.left / resolution)); + var y = ((extent.top / resolution) - point.y / resolution); + return [x, y]; + }, + + /** + * Method: clear + * Clear all vectors from the renderer. + */ + clear: function() { + var height = this.root.height; + var width = this.root.width; + this.canvas.clearRect(0, 0, width, height); + this.features = {}; + if (this.hitDetection) { + this.hitContext.clearRect(0, 0, width, height); + } + }, + + /** + * Method: getFeatureIdFromEvent + * Returns a feature id from an event on the renderer. + * + * Parameters: + * evt - {<OpenLayers.Event>} + * + * Returns: + * {<OpenLayers.Feature.Vector} A feature or undefined. This method returns a + * feature instead of a feature id to avoid an unnecessary lookup on the + * layer. + */ + getFeatureIdFromEvent: function(evt) { + var featureId, feature; + + if (this.hitDetection && this.root.style.display !== "none") { + // this dragging check should go in the feature handler + if (!this.map.dragging) { + var xy = evt.xy; + var x = xy.x | 0; + var y = xy.y | 0; + var data = this.hitContext.getImageData(x, y, 1, 1).data; + if (data[3] === 255) { // antialiased + var id = data[2] + (256 * (data[1] + (256 * data[0]))); + if (id) { + featureId = "OpenLayers_Feature_Vector_" + (id - 1 + this.hitOverflow); + try { + feature = this.features[featureId][0]; + } catch(err) { + // Because of antialiasing on the canvas, when the hit location is at a point where the edge of + // one symbol intersects the interior of another symbol, a wrong hit color (and therefore id) results. + // todo: set Antialiasing = 'off' on the hitContext as soon as browsers allow it. + } + } + } + } + } + return feature; + }, + + /** + * Method: eraseFeatures + * This is called by the layer to erase features; removes the feature from + * the list, then redraws the layer. + * + * Parameters: + * features - {Array(<OpenLayers.Feature.Vector>)} + */ + eraseFeatures: function(features) { + if(!(OpenLayers.Util.isArray(features))) { + features = [features]; + } + for(var i=0; i<features.length; ++i) { + delete this.features[features[i].id]; + } + this.redraw(); + }, + + /** + * Method: redraw + * The real 'meat' of the function: any time things have changed, + * redraw() can be called to loop over all the data and (you guessed + * it) redraw it. Unlike Elements-based Renderers, we can't interact + * with things once they're drawn, to remove them, for example, so + * instead we have to just clear everything and draw from scratch. + */ + redraw: function() { + if (!this.locked) { + var height = this.root.height; + var width = this.root.width; + this.canvas.clearRect(0, 0, width, height); + if (this.hitDetection) { + this.hitContext.clearRect(0, 0, width, height); + } + var labelMap = []; + var feature, geometry, style; + var worldBounds = (this.map.baseLayer && this.map.baseLayer.wrapDateLine) && this.map.getMaxExtent(); + for (var id in this.features) { + if (!this.features.hasOwnProperty(id)) { continue; } + feature = this.features[id][0]; + geometry = feature.geometry; + this.calculateFeatureDx(geometry.getBounds(), worldBounds); + style = this.features[id][1]; + this.drawGeometry(geometry, style, feature.id); + if(style.label) { + labelMap.push([feature, style]); + } + } + var item; + for (var i=0, len=labelMap.length; i<len; ++i) { + item = labelMap[i]; + this.drawText(item[0].geometry.getCentroid(), item[1]); + } + } + }, + + CLASS_NAME: "OpenLayers.Renderer.Canvas" +}); + +/** + * Constant: OpenLayers.Renderer.Canvas.LABEL_ALIGN + * {Object} + */ +OpenLayers.Renderer.Canvas.LABEL_ALIGN = { + "l": "left", + "r": "right", + "t": "top", + "b": "bottom" +}; + +/** + * Constant: OpenLayers.Renderer.Canvas.LABEL_FACTOR + * {Object} + */ +OpenLayers.Renderer.Canvas.LABEL_FACTOR = { + "l": 0, + "r": -1, + "t": 0, + "b": -1 +}; + +/** + * Constant: OpenLayers.Renderer.Canvas.drawImageScaleFactor + * {Number} Scale factor to apply to the canvas drawImage arguments. This + * is always 1 except for Android 2.1 devices, to work around + * http://code.google.com/p/android/issues/detail?id=5141. + */ +OpenLayers.Renderer.Canvas.drawImageScaleFactor = null; diff --git a/misc/openlayers/lib/OpenLayers/Renderer/Elements.js b/misc/openlayers/lib/OpenLayers/Renderer/Elements.js new file mode 100644 index 0000000..18a0d79 --- /dev/null +++ b/misc/openlayers/lib/OpenLayers/Renderer/Elements.js @@ -0,0 +1,1053 @@ +/* Copyright (c) 2006-2013 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.js + */ + +/** + * Class: OpenLayers.ElementsIndexer + * This class takes care of figuring out which order elements should be + * placed in the DOM based on given indexing methods. + */ +OpenLayers.ElementsIndexer = OpenLayers.Class({ + + /** + * Property: maxZIndex + * {Integer} This is the largest-most z-index value for a node + * contained within the indexer. + */ + maxZIndex: null, + + /** + * Property: order + * {Array<String>} This is an array of node id's stored in the + * order that they should show up on screen. Id's higher up in the + * array (higher array index) represent nodes with higher z-indeces. + */ + order: null, + + /** + * Property: indices + * {Object} This is a hash that maps node ids to their z-index value + * stored in the indexer. This is done to make finding a nodes z-index + * value O(1). + */ + indices: null, + + /** + * Property: compare + * {Function} This is the function used to determine placement of + * of a new node within the indexer. If null, this defaults to to + * the Z_ORDER_DRAWING_ORDER comparison method. + */ + compare: null, + + /** + * APIMethod: initialize + * Create a new indexer with + * + * Parameters: + * yOrdering - {Boolean} Whether to use y-ordering. + */ + initialize: function(yOrdering) { + + this.compare = yOrdering ? + OpenLayers.ElementsIndexer.IndexingMethods.Z_ORDER_Y_ORDER : + OpenLayers.ElementsIndexer.IndexingMethods.Z_ORDER_DRAWING_ORDER; + + this.clear(); + }, + + /** + * APIMethod: insert + * Insert a new node into the indexer. In order to find the correct + * positioning for the node to be inserted, this method uses a binary + * search. This makes inserting O(log(n)). + * + * Parameters: + * newNode - {DOMElement} The new node to be inserted. + * + * Returns + * {DOMElement} the node before which we should insert our newNode, or + * null if newNode can just be appended. + */ + insert: function(newNode) { + // If the node is known to the indexer, remove it so we can + // recalculate where it should go. + if (this.exists(newNode)) { + this.remove(newNode); + } + + var nodeId = newNode.id; + + this.determineZIndex(newNode); + + var leftIndex = -1; + var rightIndex = this.order.length; + var middle; + + while (rightIndex - leftIndex > 1) { + middle = parseInt((leftIndex + rightIndex) / 2); + + var placement = this.compare(this, newNode, + OpenLayers.Util.getElement(this.order[middle])); + + if (placement > 0) { + leftIndex = middle; + } else { + rightIndex = middle; + } + } + + this.order.splice(rightIndex, 0, nodeId); + this.indices[nodeId] = this.getZIndex(newNode); + + // If the new node should be before another in the index + // order, return the node before which we have to insert the new one; + // else, return null to indicate that the new node can be appended. + return this.getNextElement(rightIndex); + }, + + /** + * APIMethod: remove + * + * Parameters: + * node - {DOMElement} The node to be removed. + */ + remove: function(node) { + var nodeId = node.id; + var arrayIndex = OpenLayers.Util.indexOf(this.order, nodeId); + if (arrayIndex >= 0) { + // Remove it from the order array, as well as deleting the node + // from the indeces hash. + this.order.splice(arrayIndex, 1); + delete this.indices[nodeId]; + + // Reset the maxium z-index based on the last item in the + // order array. + if (this.order.length > 0) { + var lastId = this.order[this.order.length - 1]; + this.maxZIndex = this.indices[lastId]; + } else { + this.maxZIndex = 0; + } + } + }, + + /** + * APIMethod: clear + */ + clear: function() { + this.order = []; + this.indices = {}; + this.maxZIndex = 0; + }, + + /** + * APIMethod: exists + * + * Parameters: + * node - {DOMElement} The node to test for existence. + * + * Returns: + * {Boolean} Whether or not the node exists in the indexer? + */ + exists: function(node) { + return (this.indices[node.id] != null); + }, + + /** + * APIMethod: getZIndex + * Get the z-index value for the current node from the node data itself. + * + * Parameters: + * node - {DOMElement} The node whose z-index to get. + * + * Returns: + * {Integer} The z-index value for the specified node (from the node + * data itself). + */ + getZIndex: function(node) { + return node._style.graphicZIndex; + }, + + /** + * Method: determineZIndex + * Determine the z-index for the current node if there isn't one, + * and set the maximum value if we've found a new maximum. + * + * Parameters: + * node - {DOMElement} + */ + determineZIndex: function(node) { + var zIndex = node._style.graphicZIndex; + + // Everything must have a zIndex. If none is specified, + // this means the user *must* (hint: assumption) want this + // node to succomb to drawing order. To enforce drawing order + // over all indexing methods, we'll create a new z-index that's + // greater than any currently in the indexer. + if (zIndex == null) { + zIndex = this.maxZIndex; + node._style.graphicZIndex = zIndex; + } else if (zIndex > this.maxZIndex) { + this.maxZIndex = zIndex; + } + }, + + /** + * APIMethod: getNextElement + * Get the next element in the order stack. + * + * Parameters: + * index - {Integer} The index of the current node in this.order. + * + * Returns: + * {DOMElement} the node following the index passed in, or + * null. + */ + getNextElement: function(index) { + var nextIndex = index + 1; + if (nextIndex < this.order.length) { + var nextElement = OpenLayers.Util.getElement(this.order[nextIndex]); + if (nextElement == undefined) { + nextElement = this.getNextElement(nextIndex); + } + return nextElement; + } else { + return null; + } + }, + + CLASS_NAME: "OpenLayers.ElementsIndexer" +}); + +/** + * Namespace: OpenLayers.ElementsIndexer.IndexingMethods + * These are the compare methods for figuring out where a new node should be + * placed within the indexer. These methods are very similar to general + * sorting methods in that they return -1, 0, and 1 to specify the + * direction in which new nodes fall in the ordering. + */ +OpenLayers.ElementsIndexer.IndexingMethods = { + + /** + * Method: Z_ORDER + * This compare method is used by other comparison methods. + * It can be used individually for ordering, but is not recommended, + * because it doesn't subscribe to drawing order. + * + * Parameters: + * indexer - {<OpenLayers.ElementsIndexer>} + * newNode - {DOMElement} + * nextNode - {DOMElement} + * + * Returns: + * {Integer} + */ + Z_ORDER: function(indexer, newNode, nextNode) { + var newZIndex = indexer.getZIndex(newNode); + + var returnVal = 0; + if (nextNode) { + var nextZIndex = indexer.getZIndex(nextNode); + returnVal = newZIndex - nextZIndex; + } + + return returnVal; + }, + + /** + * APIMethod: Z_ORDER_DRAWING_ORDER + * This method orders nodes by their z-index, but does so in a way + * that, if there are other nodes with the same z-index, the newest + * drawn will be the front most within that z-index. This is the + * default indexing method. + * + * Parameters: + * indexer - {<OpenLayers.ElementsIndexer>} + * newNode - {DOMElement} + * nextNode - {DOMElement} + * + * Returns: + * {Integer} + */ + Z_ORDER_DRAWING_ORDER: function(indexer, newNode, nextNode) { + var returnVal = OpenLayers.ElementsIndexer.IndexingMethods.Z_ORDER( + indexer, + newNode, + nextNode + ); + + // Make Z_ORDER subscribe to drawing order by pushing it above + // all of the other nodes with the same z-index. + if (nextNode && returnVal == 0) { + returnVal = 1; + } + + return returnVal; + }, + + /** + * APIMethod: Z_ORDER_Y_ORDER + * This one should really be called Z_ORDER_Y_ORDER_DRAWING_ORDER, as it + * best describes which ordering methods have precedence (though, the + * name would be too long). This method orders nodes by their z-index, + * but does so in a way that, if there are other nodes with the same + * z-index, the nodes with the lower y position will be "closer" than + * those with a higher y position. If two nodes have the exact same y + * position, however, then this method will revert to using drawing + * order to decide placement. + * + * Parameters: + * indexer - {<OpenLayers.ElementsIndexer>} + * newNode - {DOMElement} + * nextNode - {DOMElement} + * + * Returns: + * {Integer} + */ + Z_ORDER_Y_ORDER: function(indexer, newNode, nextNode) { + var returnVal = OpenLayers.ElementsIndexer.IndexingMethods.Z_ORDER( + indexer, + newNode, + nextNode + ); + + if (nextNode && returnVal === 0) { + var result = nextNode._boundsBottom - newNode._boundsBottom; + returnVal = (result === 0) ? 1 : result; + } + + return returnVal; + } +}; + +/** + * Class: OpenLayers.Renderer.Elements + * This is another virtual class in that it should never be instantiated by + * itself as a Renderer. It exists because there is *tons* of shared + * functionality between different vector libraries which use nodes/elements + * as a base for rendering vectors. + * + * The highlevel bits of code that are implemented here are the adding and + * removing of geometries, which is essentially the same for any + * element-based renderer. The details of creating each node and drawing the + * paths are of course different, but the machinery is the same. + * + * Inherits: + * - <OpenLayers.Renderer> + */ +OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { + + /** + * Property: rendererRoot + * {DOMElement} + */ + rendererRoot: null, + + /** + * Property: root + * {DOMElement} + */ + root: null, + + /** + * Property: vectorRoot + * {DOMElement} + */ + vectorRoot: null, + + /** + * Property: textRoot + * {DOMElement} + */ + textRoot: null, + + /** + * Property: xmlns + * {String} + */ + xmlns: null, + + /** + * Property: xOffset + * {Number} Offset to apply to the renderer viewport translation in x + * direction. If the renderer extent's center is on the right of the + * dateline (i.e. exceeds the world bounds), we shift the viewport to the + * left by one world width. This avoids that features disappear from the + * map viewport. Because our dateline handling logic in other places + * ensures that extents crossing the dateline always have a center + * exceeding the world bounds on the left, we need this offset to make sure + * that the same is true for the renderer extent in pixel space as well. + */ + xOffset: 0, + + /** + * Property: rightOfDateLine + * {Boolean} Keeps track of the location of the map extent relative to the + * date line. The <setExtent> method compares this value (which is the one + * from the previous <setExtent> call) with the current position of the map + * extent relative to the date line and updates the xOffset when the extent + * has moved from one side of the date line to the other. + */ + + /** + * Property: Indexer + * {<OpenLayers.ElementIndexer>} An instance of OpenLayers.ElementsIndexer + * created upon initialization if the zIndexing or yOrdering options + * passed to this renderer's constructor are set to true. + */ + indexer: null, + + /** + * Constant: BACKGROUND_ID_SUFFIX + * {String} + */ + BACKGROUND_ID_SUFFIX: "_background", + + /** + * Constant: LABEL_ID_SUFFIX + * {String} + */ + LABEL_ID_SUFFIX: "_label", + + /** + * Constant: LABEL_OUTLINE_SUFFIX + * {String} + */ + LABEL_OUTLINE_SUFFIX: "_outline", + + /** + * Constructor: OpenLayers.Renderer.Elements + * + * Parameters: + * containerID - {String} + * options - {Object} options for this renderer. + * + * Supported options are: + * yOrdering - {Boolean} Whether to use y-ordering + * zIndexing - {Boolean} Whether to use z-indexing. Will be ignored + * if yOrdering is set to true. + */ + initialize: function(containerID, options) { + OpenLayers.Renderer.prototype.initialize.apply(this, arguments); + + this.rendererRoot = this.createRenderRoot(); + this.root = this.createRoot("_root"); + this.vectorRoot = this.createRoot("_vroot"); + this.textRoot = this.createRoot("_troot"); + + this.root.appendChild(this.vectorRoot); + this.root.appendChild(this.textRoot); + + this.rendererRoot.appendChild(this.root); + this.container.appendChild(this.rendererRoot); + + if(options && (options.zIndexing || options.yOrdering)) { + this.indexer = new OpenLayers.ElementsIndexer(options.yOrdering); + } + }, + + /** + * Method: destroy + */ + destroy: function() { + + this.clear(); + + this.rendererRoot = null; + this.root = null; + this.xmlns = null; + + OpenLayers.Renderer.prototype.destroy.apply(this, arguments); + }, + + /** + * Method: clear + * Remove all the elements from the root + */ + clear: function() { + var child; + var root = this.vectorRoot; + if (root) { + while (child = root.firstChild) { + root.removeChild(child); + } + } + root = this.textRoot; + if (root) { + while (child = root.firstChild) { + root.removeChild(child); + } + } + if (this.indexer) { + this.indexer.clear(); + } + }, + + /** + * Method: setExtent + * Set the visible part of the layer. + * + * 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.prototype.setExtent.apply(this, arguments); + var resolution = this.getResolution(); + if (this.map.baseLayer && this.map.baseLayer.wrapDateLine) { + var rightOfDateLine, + ratio = extent.getWidth() / this.map.getExtent().getWidth(), + extent = extent.scale(1 / ratio), + world = this.map.getMaxExtent(); + if (world.right > extent.left && world.right < extent.right) { + rightOfDateLine = true; + } else if (world.left > extent.left && world.left < extent.right) { + rightOfDateLine = false; + } + if (rightOfDateLine !== this.rightOfDateLine || resolutionChanged) { + coordSysUnchanged = false; + this.xOffset = rightOfDateLine === true ? + world.getWidth() / resolution : 0; + } + this.rightOfDateLine = rightOfDateLine; + } + return coordSysUnchanged; + }, + + /** + * Method: getNodeType + * This function is in charge of asking the specific renderer which type + * of node to create for the given geometry and style. All geometries + * in an Elements-based renderer consist of one node and some + * attributes. We have the nodeFactory() function which creates a node + * for us, but it takes a 'type' as input, and that is precisely what + * this function tells us. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * + * Returns: + * {String} The corresponding node type for the specified geometry + */ + getNodeType: function(geometry, style) { }, + + /** + * Method: drawGeometry + * Draw the geometry, creating new nodes, setting paths, setting style, + * setting featureId on the node. This method should only be called + * by the renderer itself. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + * + * Returns: + * {Boolean} true if the geometry has been drawn completely; null if + * incomplete; false otherwise + */ + drawGeometry: function(geometry, style, featureId) { + var className = geometry.CLASS_NAME; + var rendered = true; + if ((className == "OpenLayers.Geometry.Collection") || + (className == "OpenLayers.Geometry.MultiPoint") || + (className == "OpenLayers.Geometry.MultiLineString") || + (className == "OpenLayers.Geometry.MultiPolygon")) { + for (var i = 0, len=geometry.components.length; i<len; i++) { + rendered = this.drawGeometry( + geometry.components[i], style, featureId) && rendered; + } + return rendered; + } + + rendered = false; + var removeBackground = false; + if (style.display != "none") { + if (style.backgroundGraphic) { + this.redrawBackgroundNode(geometry.id, geometry, style, + featureId); + } else { + removeBackground = true; + } + rendered = this.redrawNode(geometry.id, geometry, style, + featureId); + } + if (rendered == false) { + var node = document.getElementById(geometry.id); + if (node) { + if (node._style.backgroundGraphic) { + removeBackground = true; + } + node.parentNode.removeChild(node); + } + } + if (removeBackground) { + var node = document.getElementById( + geometry.id + this.BACKGROUND_ID_SUFFIX); + if (node) { + node.parentNode.removeChild(node); + } + } + return rendered; + }, + + /** + * Method: redrawNode + * + * Parameters: + * id - {String} + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + * + * Returns: + * {Boolean} true if the complete geometry could be drawn, null if parts of + * the geometry could not be drawn, false otherwise + */ + redrawNode: function(id, geometry, style, featureId) { + style = this.applyDefaultSymbolizer(style); + // Get the node if it's already on the map. + var node = this.nodeFactory(id, this.getNodeType(geometry, style)); + + // Set the data for the node, then draw it. + node._featureId = featureId; + node._boundsBottom = geometry.getBounds().bottom; + node._geometryClass = geometry.CLASS_NAME; + node._style = style; + + var drawResult = this.drawGeometryNode(node, geometry, style); + if(drawResult === false) { + return false; + } + + node = drawResult.node; + + // Insert the node into the indexer so it can show us where to + // place it. Note that this operation is O(log(n)). If there's a + // performance problem (when dragging, for instance) this is + // likely where it would be. + if (this.indexer) { + var insert = this.indexer.insert(node); + if (insert) { + this.vectorRoot.insertBefore(node, insert); + } else { + this.vectorRoot.appendChild(node); + } + } else { + // if there's no indexer, simply append the node to root, + // but only if the node is a new one + if (node.parentNode !== this.vectorRoot){ + this.vectorRoot.appendChild(node); + } + } + + this.postDraw(node); + + return drawResult.complete; + }, + + /** + * Method: redrawBackgroundNode + * Redraws the node using special 'background' style properties. Basically + * just calls redrawNode(), but instead of directly using the + * 'externalGraphic', 'graphicXOffset', 'graphicYOffset', and + * 'graphicZIndex' properties directly from the specified 'style' + * parameter, we create a new style object and set those properties + * from the corresponding 'background'-prefixed properties from + * specified 'style' parameter. + * + * Parameters: + * id - {String} + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * featureId - {String} + * + * Returns: + * {Boolean} true if the complete geometry could be drawn, null if parts of + * the geometry could not be drawn, false otherwise + */ + redrawBackgroundNode: function(id, geometry, style, featureId) { + var backgroundStyle = OpenLayers.Util.extend({}, style); + + // Set regular style attributes to apply to the background styles. + backgroundStyle.externalGraphic = backgroundStyle.backgroundGraphic; + backgroundStyle.graphicXOffset = backgroundStyle.backgroundXOffset; + backgroundStyle.graphicYOffset = backgroundStyle.backgroundYOffset; + backgroundStyle.graphicZIndex = backgroundStyle.backgroundGraphicZIndex; + backgroundStyle.graphicWidth = backgroundStyle.backgroundWidth || backgroundStyle.graphicWidth; + backgroundStyle.graphicHeight = backgroundStyle.backgroundHeight || backgroundStyle.graphicHeight; + + // Erase background styles. + backgroundStyle.backgroundGraphic = null; + backgroundStyle.backgroundXOffset = null; + backgroundStyle.backgroundYOffset = null; + backgroundStyle.backgroundGraphicZIndex = null; + + return this.redrawNode( + id + this.BACKGROUND_ID_SUFFIX, + geometry, + backgroundStyle, + null + ); + }, + + /** + * Method: drawGeometryNode + * Given a node, draw a geometry on the specified layer. + * node and geometry are required arguments, style is optional. + * This method is only called by the render itself. + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * style - {Object} + * + * Returns: + * {Object} a hash with properties "node" (the drawn node) and "complete" + * (null if parts of the geometry could not be drawn, false if nothing + * could be drawn) + */ + drawGeometryNode: function(node, geometry, style) { + style = style || node._style; + + var options = { + 'isFilled': style.fill === undefined ? + true : + style.fill, + 'isStroked': style.stroke === undefined ? + !!style.strokeWidth : + style.stroke + }; + var drawn; + switch (geometry.CLASS_NAME) { + case "OpenLayers.Geometry.Point": + if(style.graphic === false) { + options.isFilled = false; + options.isStroked = false; + } + drawn = this.drawPoint(node, geometry); + break; + case "OpenLayers.Geometry.LineString": + options.isFilled = false; + drawn = this.drawLineString(node, geometry); + break; + case "OpenLayers.Geometry.LinearRing": + drawn = this.drawLinearRing(node, geometry); + break; + case "OpenLayers.Geometry.Polygon": + drawn = this.drawPolygon(node, geometry); + break; + case "OpenLayers.Geometry.Rectangle": + drawn = this.drawRectangle(node, geometry); + break; + default: + break; + } + + node._options = options; + + //set style + //TBD simplify this + if (drawn != false) { + return { + node: this.setStyle(node, style, options, geometry), + complete: drawn + }; + } else { + return false; + } + }, + + /** + * Method: postDraw + * Things that have do be done after the geometry node is appended + * to its parent node. To be overridden by subclasses. + * + * Parameters: + * node - {DOMElement} + */ + postDraw: function(node) {}, + + /** + * Method: drawPoint + * Virtual function for drawing Point Geometry. + * Should be implemented by subclasses. + * 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) {}, + + /** + * Method: drawLineString + * Virtual function for drawing LineString Geometry. + * Should be implemented by subclasses. + * 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) {}, + + /** + * Method: drawLinearRing + * Virtual function for drawing LinearRing Geometry. + * Should be implemented by subclasses. + * 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) {}, + + /** + * Method: drawPolygon + * Virtual function for drawing Polygon Geometry. + * Should be implemented by subclasses. + * 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) {}, + + /** + * Method: drawRectangle + * Virtual function for drawing Rectangle Geometry. + * Should be implemented by subclasses. + * 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) {}, + + /** + * Method: drawCircle + * Virtual function for drawing Circle Geometry. + * Should be implemented by subclasses. + * 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 circle + */ + drawCircle: function(node, geometry) {}, + + /** + * Method: removeText + * Removes a label + * + * Parameters: + * featureId - {String} + */ + removeText: function(featureId) { + var label = document.getElementById(featureId + this.LABEL_ID_SUFFIX); + if (label) { + this.textRoot.removeChild(label); + } + var outline = document.getElementById(featureId + this.LABEL_OUTLINE_SUFFIX); + if (outline) { + this.textRoot.removeChild(outline); + } + }, + + /** + * Method: getFeatureIdFromEvent + * + * Parameters: + * evt - {Object} An <OpenLayers.Event> object + * + * Returns: + * {String} A feature id or undefined. + */ + getFeatureIdFromEvent: function(evt) { + var target = evt.target; + var useElement = target && target.correspondingUseElement; + var node = useElement ? useElement : (target || evt.srcElement); + return node._featureId; + }, + + /** + * Method: eraseGeometry + * Erase a geometry from the renderer. In the case of a multi-geometry, + * we cycle through and recurse on ourselves. Otherwise, we look for a + * node with the geometry.id, destroy its geometry, and remove it from + * the DOM. + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * featureId - {String} + */ + eraseGeometry: function(geometry, featureId) { + if ((geometry.CLASS_NAME == "OpenLayers.Geometry.MultiPoint") || + (geometry.CLASS_NAME == "OpenLayers.Geometry.MultiLineString") || + (geometry.CLASS_NAME == "OpenLayers.Geometry.MultiPolygon") || + (geometry.CLASS_NAME == "OpenLayers.Geometry.Collection")) { + for (var i=0, len=geometry.components.length; i<len; i++) { + this.eraseGeometry(geometry.components[i], featureId); + } + } else { + var element = OpenLayers.Util.getElement(geometry.id); + if (element && element.parentNode) { + if (element.geometry) { + element.geometry.destroy(); + element.geometry = null; + } + element.parentNode.removeChild(element); + + if (this.indexer) { + this.indexer.remove(element); + } + + if (element._style.backgroundGraphic) { + var backgroundId = geometry.id + this.BACKGROUND_ID_SUFFIX; + var bElem = OpenLayers.Util.getElement(backgroundId); + if (bElem && bElem.parentNode) { + // No need to destroy the geometry since the element and the background + // node share the same geometry. + bElem.parentNode.removeChild(bElem); + } + } + } + } + }, + + /** + * Method: nodeFactory + * Create new node of the specified type, with the (optional) specified id. + * + * If node already exists with same ID and a different type, we remove it + * and then call ourselves again to recreate it. + * + * Parameters: + * id - {String} + * type - {String} type Kind of node to draw. + * + * Returns: + * {DOMElement} A new node of the given type and id. + */ + nodeFactory: function(id, type) { + var node = OpenLayers.Util.getElement(id); + if (node) { + if (!this.nodeTypeCompare(node, type)) { + node.parentNode.removeChild(node); + node = this.nodeFactory(id, type); + } + } else { + node = this.createNode(type, id); + } + return node; + }, + + /** + * Method: nodeTypeCompare + * + * Parameters: + * node - {DOMElement} + * type - {String} Kind of node + * + * Returns: + * {Boolean} Whether or not the specified node is of the specified type + * This function must be overridden by subclasses. + */ + nodeTypeCompare: function(node, type) {}, + + /** + * 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. + * This function must be overridden by subclasses. + */ + createNode: function(type, id) {}, + + /** + * Method: moveRoot + * moves this renderer's root to a different renderer. + * + * Parameters: + * renderer - {<OpenLayers.Renderer>} target renderer for the moved root + */ + moveRoot: function(renderer) { + var root = this.root; + if(renderer.root.parentNode == this.rendererRoot) { + root = renderer.root; + } + root.parentNode.removeChild(root); + renderer.rendererRoot.appendChild(root); + }, + + /** + * Method: getRenderLayerId + * Gets the layer that this renderer's output appears on. If moveRoot was + * used, this will be different from the id of the layer containing the + * features rendered by this renderer. + * + * Returns: + * {String} the id of the output layer. + */ + getRenderLayerId: function() { + return this.root.parentNode.parentNode.id; + }, + + /** + * Method: isComplexSymbol + * Determines if a symbol cannot be rendered using drawCircle + * + * Parameters: + * graphicName - {String} + * + * Returns + * {Boolean} true if the symbol is complex, false if not + */ + isComplexSymbol: function(graphicName) { + return (graphicName != "circle") && !!graphicName; + }, + + CLASS_NAME: "OpenLayers.Renderer.Elements" +}); + diff --git a/misc/openlayers/lib/OpenLayers/Renderer/SVG.js b/misc/openlayers/lib/OpenLayers/Renderer/SVG.js new file mode 100644 index 0000000..263aac0 --- /dev/null +++ b/misc/openlayers/lib/OpenLayers/Renderer/SVG.js @@ -0,0 +1,1012 @@ +/* Copyright (c) 2006-2013 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 title = style.title || style.graphicTitle; + if (title) { + node.setAttributeNS(null, "title", title); + //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 = title; + } else { + var label = this.nodeFactory(null, "title"); + label.textContent = title; + node.appendChild(label); + } + } + + 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.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, "xlink:href", style.externalGraphic); + node.setAttributeNS(null, "style", "opacity: "+opacity); + node.onclick = OpenLayers.Event.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.display = "block"; + 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; + if (style.labelOutlineOpacity) { + outlineStyle.fontOpacity = style.labelOutlineOpacity; + } + 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 + * *Deprecated*. Use <OpenLayers.Event.preventDefault> method instead. + * 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) { + OpenLayers.Event.preventDefault(e); +}; diff --git a/misc/openlayers/lib/OpenLayers/Renderer/VML.js b/misc/openlayers/lib/OpenLayers/Renderer/VML.js new file mode 100644 index 0000000..8f6374b --- /dev/null +++ b/misc/openlayers/lib/OpenLayers/Renderer/VML.js @@ -0,0 +1,985 @@ +/* Copyright (c) 2006-2013 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.VML + * Render vector features in browsers with VML capability. Construct a new + * VML renderer with the <OpenLayers.Renderer.VML> constructor. + * + * Note that for all calculations in this class, we use (num | 0) to truncate a + * float value to an integer. This is done because it seems that VML doesn't + * support float values. + * + * Inherits from: + * - <OpenLayers.Renderer.Elements> + */ +OpenLayers.Renderer.VML = OpenLayers.Class(OpenLayers.Renderer.Elements, { + + /** + * Property: xmlns + * {String} XML Namespace URN + */ + xmlns: "urn:schemas-microsoft-com:vml", + + /** + * Property: symbolCache + * {DOMElement} node holding symbols. This hash is keyed by symbol name, + * and each value is a hash with a "path" and an "extent" property. + */ + symbolCache: {}, + + /** + * Property: offset + * {Object} Hash with "x" and "y" properties + */ + offset: null, + + /** + * Constructor: OpenLayers.Renderer.VML + * Create a new VML renderer. + * + * Parameters: + * containerID - {String} The id for the element that contains the renderer + */ + initialize: function(containerID) { + if (!this.supported()) { + return; + } + if (!document.namespaces.olv) { + document.namespaces.add("olv", this.xmlns); + var style = document.createStyleSheet(); + var shapes = ['shape','rect', 'oval', 'fill', 'stroke', 'imagedata', 'group','textbox']; + for (var i = 0, len = shapes.length; i < len; i++) { + + style.addRule('olv\\:' + shapes[i], "behavior: url(#default#VML); " + + "position: absolute; display: inline-block;"); + } + } + + OpenLayers.Renderer.Elements.prototype.initialize.apply(this, + arguments); + }, + + /** + * APIMethod: supported + * Determine whether a browser supports this renderer. + * + * Returns: + * {Boolean} The browser supports the VML renderer + */ + supported: function() { + return !!(document.namespaces); + }, + + /** + * Method: setExtent + * Set the renderer's extent + * + * 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. + */ + setExtent: function(extent, resolutionChanged) { + var coordSysUnchanged = OpenLayers.Renderer.Elements.prototype.setExtent.apply(this, arguments); + var resolution = this.getResolution(); + + var left = (extent.left/resolution) | 0; + var top = (extent.top/resolution - this.size.h) | 0; + if (resolutionChanged || !this.offset) { + this.offset = {x: left, y: top}; + left = 0; + top = 0; + } else { + left = left - this.offset.x; + top = top - this.offset.y; + } + + + var org = (left - this.xOffset) + " " + top; + this.root.coordorigin = org; + var roots = [this.root, this.vectorRoot, this.textRoot]; + var root; + for(var i=0, len=roots.length; i<len; ++i) { + root = roots[i]; + + var size = this.size.w + " " + this.size.h; + root.coordsize = size; + + } + // flip the VML display Y axis upside down so it + // matches the display Y axis of the map + this.root.style.flip = "y"; + + return coordSysUnchanged; + }, + + + /** + * Method: setSize + * Set 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); + + // setting width and height on all roots to avoid flicker which we + // would get with 100% width and height on child roots + var roots = [ + this.rendererRoot, + this.root, + this.vectorRoot, + this.textRoot + ]; + var w = this.size.w + "px"; + var h = this.size.h + "px"; + var root; + for(var i=0, len=roots.length; i<len; ++i) { + root = roots[i]; + root.style.width = w; + root.style.height = h; + } + }, + + /** + * Method: getNodeType + * Get the node type for a geometry and style + * + * 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 = "olv:rect"; + } else if (this.isComplexSymbol(style.graphicName)) { + nodeType = "olv:shape"; + } else { + nodeType = "olv:oval"; + } + break; + case "OpenLayers.Geometry.Rectangle": + nodeType = "olv:rect"; + break; + case "OpenLayers.Geometry.LineString": + case "OpenLayers.Geometry.LinearRing": + case "OpenLayers.Geometry.Polygon": + case "OpenLayers.Geometry.Curve": + nodeType = "olv:shape"; + break; + default: + break; + } + return nodeType; + }, + + /** + * Method: setStyle + * Use to set all the style attributes to a VML node. + * + * Parameters: + * node - {DOMElement} An VML element to decorate + * style - {Object} + * options - {Object} Currently supported options include + * 'isFilled' {Boolean} and + * 'isStroked' {Boolean} + * geometry - {<OpenLayers.Geometry>} + */ + setStyle: function(node, style, options, geometry) { + style = style || node._style; + options = options || node._options; + var fillColor = style.fillColor; + + var title = style.title || style.graphicTitle; + if (title) { + node.title = title; + } + + if (node._geometryClass === "OpenLayers.Geometry.Point") { + if (style.externalGraphic) { + options.isFilled = true; + 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 resolution = this.getResolution(); + var xOffset = (style.graphicXOffset != undefined) ? + style.graphicXOffset : -(0.5 * width); + var yOffset = (style.graphicYOffset != undefined) ? + style.graphicYOffset : -(0.5 * height); + + node.style.left = ((((geometry.x - this.featureDx)/resolution - this.offset.x)+xOffset) | 0) + "px"; + node.style.top = (((geometry.y/resolution - this.offset.y)-(yOffset+height)) | 0) + "px"; + node.style.width = width + "px"; + node.style.height = height + "px"; + node.style.flip = "y"; + + // modify fillColor and options for stroke styling below + fillColor = "none"; + options.isStroked = false; + } else if (this.isComplexSymbol(style.graphicName)) { + var cache = this.importSymbol(style.graphicName); + node.path = cache.path; + node.coordorigin = cache.left + "," + cache.bottom; + var size = cache.size; + node.coordsize = size + "," + size; + this.drawCircle(node, geometry, style.pointRadius); + node.style.flip = "y"; + } else { + this.drawCircle(node, geometry, style.pointRadius); + } + } + + // fill + if (options.isFilled) { + node.fillcolor = fillColor; + } else { + node.filled = "false"; + } + var fills = node.getElementsByTagName("fill"); + var fill = (fills.length == 0) ? null : fills[0]; + if (!options.isFilled) { + if (fill) { + node.removeChild(fill); + } + } else { + if (!fill) { + fill = this.createNode('olv:fill', node.id + "_fill"); + } + fill.opacity = style.fillOpacity; + + if (node._geometryClass === "OpenLayers.Geometry.Point" && + style.externalGraphic) { + + // override fillOpacity + if (style.graphicOpacity) { + fill.opacity = style.graphicOpacity; + } + + fill.src = style.externalGraphic; + fill.type = "frame"; + + if (!(style.graphicWidth && style.graphicHeight)) { + fill.aspect = "atmost"; + } + } + if (fill.parentNode != node) { + node.appendChild(fill); + } + } + + // additional rendering for rotated graphics or symbols + var rotation = style.rotation; + if ((rotation !== undefined || node._rotation !== undefined)) { + node._rotation = rotation; + if (style.externalGraphic) { + this.graphicRotate(node, xOffset, yOffset, style); + // make the fill fully transparent, because we now have + // the graphic as imagedata element. We cannot just remove + // the fill, because this is part of the hack described + // in graphicRotate + fill.opacity = 0; + } else if(node._geometryClass === "OpenLayers.Geometry.Point") { + node.style.rotation = rotation || 0; + } + } + + // stroke + var strokes = node.getElementsByTagName("stroke"); + var stroke = (strokes.length == 0) ? null : strokes[0]; + if (!options.isStroked) { + node.stroked = false; + if (stroke) { + stroke.on = false; + } + } else { + if (!stroke) { + stroke = this.createNode('olv:stroke', node.id + "_stroke"); + node.appendChild(stroke); + } + stroke.on = true; + stroke.color = style.strokeColor; + stroke.weight = style.strokeWidth + "px"; + stroke.opacity = style.strokeOpacity; + stroke.endcap = style.strokeLinecap == 'butt' ? 'flat' : + (style.strokeLinecap || 'round'); + if (style.strokeDashstyle) { + stroke.dashstyle = this.dashStyle(style); + } + } + + if (style.cursor != "inherit" && style.cursor != null) { + node.style.cursor = style.cursor; + } + return node; + }, + + /** + * Method: graphicRotate + * If a point is to be styled with externalGraphic and rotation, VML fills + * cannot be used to display the graphic, because rotation of graphic + * fills is not supported by the VML implementation of Internet Explorer. + * This method creates a olv:imagedata element inside the VML node, + * DXImageTransform.Matrix and BasicImage filters for rotation and + * opacity, and a 3-step hack to remove rendering artefacts from the + * graphic and preserve the ability of graphics to trigger events. + * Finally, OpenLayers methods are used to determine the correct + * insertion point of the rotated image, because DXImageTransform.Matrix + * does the rotation without the ability to specify a rotation center + * point. + * + * Parameters: + * node - {DOMElement} + * xOffset - {Number} rotation center relative to image, x coordinate + * yOffset - {Number} rotation center relative to image, y coordinate + * style - {Object} + */ + graphicRotate: function(node, xOffset, yOffset, style) { + var style = style || node._style; + var rotation = style.rotation || 0; + + var aspectRatio, size; + if (!(style.graphicWidth && style.graphicHeight)) { + // load the image to determine its size + var img = new Image(); + img.onreadystatechange = OpenLayers.Function.bind(function() { + if(img.readyState == "complete" || + img.readyState == "interactive") { + aspectRatio = img.width / img.height; + size = Math.max(style.pointRadius * 2, + style.graphicWidth || 0, + style.graphicHeight || 0); + xOffset = xOffset * aspectRatio; + style.graphicWidth = size * aspectRatio; + style.graphicHeight = size; + this.graphicRotate(node, xOffset, yOffset, style); + } + }, this); + img.src = style.externalGraphic; + + // will be called again by the onreadystate handler + return; + } else { + size = Math.max(style.graphicWidth, style.graphicHeight); + aspectRatio = style.graphicWidth / style.graphicHeight; + } + + var width = Math.round(style.graphicWidth || size * aspectRatio); + var height = Math.round(style.graphicHeight || size); + node.style.width = width + "px"; + node.style.height = height + "px"; + + // Three steps are required to remove artefacts for images with + // transparent backgrounds (resulting from using DXImageTransform + // filters on svg objects), while preserving awareness for browser + // events on images: + // - Use the fill as usual (like for unrotated images) to handle + // events + // - specify an imagedata element with the same src as the fill + // - style the imagedata element with an AlphaImageLoader filter + // with empty src + var image = document.getElementById(node.id + "_image"); + if (!image) { + image = this.createNode("olv:imagedata", node.id + "_image"); + node.appendChild(image); + } + image.style.width = width + "px"; + image.style.height = height + "px"; + image.src = style.externalGraphic; + image.style.filter = + "progid:DXImageTransform.Microsoft.AlphaImageLoader(" + + "src='', sizingMethod='scale')"; + + var rot = rotation * Math.PI / 180; + var sintheta = Math.sin(rot); + var costheta = Math.cos(rot); + + // do the rotation on the image + var filter = + "progid:DXImageTransform.Microsoft.Matrix(M11=" + costheta + + ",M12=" + (-sintheta) + ",M21=" + sintheta + ",M22=" + costheta + + ",SizingMethod='auto expand')\n"; + + // set the opacity (needed for the imagedata) + var opacity = style.graphicOpacity || style.fillOpacity; + if (opacity && opacity != 1) { + filter += + "progid:DXImageTransform.Microsoft.BasicImage(opacity=" + + opacity+")\n"; + } + node.style.filter = filter; + + // do the rotation again on a box, so we know the insertion point + var centerPoint = new OpenLayers.Geometry.Point(-xOffset, -yOffset); + var imgBox = new OpenLayers.Bounds(0, 0, width, height).toGeometry(); + imgBox.rotate(style.rotation, centerPoint); + var imgBounds = imgBox.getBounds(); + + node.style.left = Math.round( + parseInt(node.style.left) + imgBounds.left) + "px"; + node.style.top = Math.round( + parseInt(node.style.top) - imgBounds.bottom) + "px"; + }, + + /** + * Method: postDraw + * Does some node postprocessing to work around browser issues: + * - Some versions of Internet Explorer seem to be unable to set fillcolor + * and strokecolor to "none" correctly before the fill node is appended + * to a visible vml node. This method takes care of that and sets + * fillcolor and strokecolor again if needed. + * - In some cases, a node won't become visible after being drawn. Setting + * style.visibility to "visible" works around that. + * + * Parameters: + * node - {DOMElement} + */ + postDraw: function(node) { + node.style.visibility = "visible"; + var fillColor = node._style.fillColor; + var strokeColor = node._style.strokeColor; + if (fillColor == "none" && + node.fillcolor != fillColor) { + node.fillcolor = fillColor; + } + if (strokeColor == "none" && + node.strokecolor != strokeColor) { + node.strokecolor = strokeColor; + } + }, + + + /** + * Method: setNodeDimension + * Get the geometry's bounds, convert it to our vml coordinate system, + * then set the node's position, size, and local coordinate system. + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + */ + setNodeDimension: function(node, geometry) { + + var bbox = geometry.getBounds(); + if(bbox) { + var resolution = this.getResolution(); + + var scaledBox = + new OpenLayers.Bounds(((bbox.left - this.featureDx)/resolution - this.offset.x) | 0, + (bbox.bottom/resolution - this.offset.y) | 0, + ((bbox.right - this.featureDx)/resolution - this.offset.x) | 0, + (bbox.top/resolution - this.offset.y) | 0); + + // Set the internal coordinate system to draw the path + node.style.left = scaledBox.left + "px"; + node.style.top = scaledBox.top + "px"; + node.style.width = scaledBox.getWidth() + "px"; + node.style.height = scaledBox.getHeight() + "px"; + + node.coordorigin = scaledBox.left + " " + scaledBox.top; + node.coordsize = scaledBox.getWidth()+ " " + scaledBox.getHeight(); + } + }, + + /** + * Method: dashStyle + * + * Parameters: + * style - {Object} + * + * Returns: + * {String} A VML compliant 'stroke-dasharray' value + */ + dashStyle: function(style) { + var dash = style.strokeDashstyle; + switch (dash) { + case 'solid': + case 'dot': + case 'dash': + case 'dashdot': + case 'longdash': + case 'longdashdot': + return dash; + default: + // very basic guessing of dash style patterns + var parts = dash.split(/[ ,]/); + if (parts.length == 2) { + if (1*parts[0] >= 2*parts[1]) { + return "longdash"; + } + return (parts[0] == 1 || parts[1] == 1) ? "dot" : "dash"; + } else if (parts.length == 4) { + return (1*parts[0] >= 2*parts[1]) ? "longdashdot" : + "dashdot"; + } + return "solid"; + } + }, + + /** + * Method: createNode + * Create a new node + * + * 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.createElement(type); + if (id) { + node.id = id; + } + + // IE hack to make elements unselectable, to prevent 'blue flash' + // while dragging vectors; #1410 + node.unselectable = 'on'; + node.onselectstart = OpenLayers.Function.False; + + return node; + }, + + /** + * Method: nodeTypeCompare + * Determine whether a node is of a given type + * + * Parameters: + * node - {DOMElement} An VML element + * type - {String} Kind of node + * + * Returns: + * {Boolean} Whether or not the specified node is of the specified type + */ + nodeTypeCompare: function(node, type) { + + //split type + var subType = type; + var splitIndex = subType.indexOf(":"); + if (splitIndex != -1) { + subType = subType.substr(splitIndex+1); + } + + //split nodeName + var nodeName = node.nodeName; + splitIndex = nodeName.indexOf(":"); + if (splitIndex != -1) { + nodeName = nodeName.substr(splitIndex+1); + } + + return (subType == nodeName); + }, + + /** + * Method: createRenderRoot + * Create the renderer root + * + * Returns: + * {DOMElement} The specific render engine's root element + */ + createRenderRoot: function() { + return this.nodeFactory(this.container.id + "_vmlRoot", "div"); + }, + + /** + * Method: createRoot + * Create the main root element + * + * Parameters: + * suffix - {String} suffix to append to the id + * + * Returns: + * {DOMElement} + */ + createRoot: function(suffix) { + return this.nodeFactory(this.container.id + suffix, "olv:group"); + }, + + /************************************** + * * + * GEOMETRY DRAWING FUNCTIONS * + * * + **************************************/ + + /** + * Method: drawPoint + * Render a point + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} or false if the point could not be drawn + */ + drawPoint: function(node, geometry) { + return this.drawCircle(node, geometry, 1); + }, + + /** + * Method: drawCircle + * Render a circle. + * Size and Center a circle given geometry (x,y center) and radius + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * radius - {float} + * + * Returns: + * {DOMElement} or false if the circle could not ne drawn + */ + drawCircle: function(node, geometry, radius) { + if(!isNaN(geometry.x)&& !isNaN(geometry.y)) { + var resolution = this.getResolution(); + + node.style.left = ((((geometry.x - this.featureDx) /resolution - this.offset.x) | 0) - radius) + "px"; + node.style.top = (((geometry.y /resolution - this.offset.y) | 0) - radius) + "px"; + + var diameter = radius * 2; + + node.style.width = diameter + "px"; + node.style.height = diameter + "px"; + return node; + } + return false; + }, + + + /** + * Method: drawLineString + * Render a linestring. + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} + */ + drawLineString: function(node, geometry) { + return this.drawLine(node, geometry, false); + }, + + /** + * Method: drawLinearRing + * Render a linearring + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} + */ + drawLinearRing: function(node, geometry) { + return this.drawLine(node, geometry, true); + }, + + /** + * Method: DrawLine + * Render a line. + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * closeLine - {Boolean} Close the line? (make it a ring?) + * + * Returns: + * {DOMElement} + */ + drawLine: function(node, geometry, closeLine) { + + this.setNodeDimension(node, geometry); + + var resolution = this.getResolution(); + var numComponents = geometry.components.length; + var parts = new Array(numComponents); + + var comp, x, y; + for (var i = 0; i < numComponents; i++) { + comp = geometry.components[i]; + x = ((comp.x - this.featureDx)/resolution - this.offset.x) | 0; + y = (comp.y/resolution - this.offset.y) | 0; + parts[i] = " " + x + "," + y + " l "; + } + var end = (closeLine) ? " x e" : " e"; + node.path = "m" + parts.join("") + end; + return node; + }, + + /** + * Method: drawPolygon + * Render a polygon + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} + */ + drawPolygon: function(node, geometry) { + this.setNodeDimension(node, geometry); + + var resolution = this.getResolution(); + + var path = []; + var j, jj, points, area, first, second, i, ii, comp, pathComp, x, y; + for (j=0, jj=geometry.components.length; j<jj; j++) { + path.push("m"); + points = geometry.components[j].components; + // we only close paths of interior rings with area + area = (j === 0); + first = null; + second = null; + for (i=0, ii=points.length; i<ii; i++) { + comp = points[i]; + x = ((comp.x - this.featureDx) / resolution - this.offset.x) | 0; + y = (comp.y / resolution - this.offset.y) | 0; + pathComp = " " + x + "," + y; + path.push(pathComp); + if (i==0) { + path.push(" l"); + } + if (!area) { + // IE improperly renders sub-paths that have no area. + // Instead of checking the area of every ring, we confirm + // the ring has at least three distinct points. This does + // not catch all non-zero area cases, but it greatly improves + // interior ring digitizing and is a minor performance hit + // when rendering rings with many points. + if (!first) { + first = pathComp; + } else if (first != pathComp) { + if (!second) { + second = pathComp; + } else if (second != pathComp) { + // stop looking + area = true; + } + } + } + } + path.push(area ? " x " : " "); + } + path.push("e"); + node.path = path.join(""); + return node; + }, + + /** + * Method: drawRectangle + * Render a rectangle + * + * Parameters: + * node - {DOMElement} + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {DOMElement} + */ + drawRectangle: function(node, geometry) { + var resolution = this.getResolution(); + + node.style.left = (((geometry.x - this.featureDx)/resolution - this.offset.x) | 0) + "px"; + node.style.top = ((geometry.y/resolution - this.offset.y) | 0) + "px"; + node.style.width = ((geometry.width/resolution) | 0) + "px"; + node.style.height = ((geometry.height/resolution) | 0) + "px"; + + return node; + }, + + /** + * 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 label = this.nodeFactory(featureId + this.LABEL_ID_SUFFIX, "olv:rect"); + var textbox = this.nodeFactory(featureId + this.LABEL_ID_SUFFIX + "_textbox", "olv:textbox"); + + var resolution = this.getResolution(); + label.style.left = (((location.x - this.featureDx)/resolution - this.offset.x) | 0) + "px"; + label.style.top = ((location.y/resolution - this.offset.y) | 0) + "px"; + label.style.flip = "y"; + + textbox.innerText = style.label; + + if (style.cursor != "inherit" && style.cursor != null) { + textbox.style.cursor = style.cursor; + } + if (style.fontColor) { + textbox.style.color = style.fontColor; + } + if (style.fontOpacity) { + textbox.style.filter = 'alpha(opacity=' + (style.fontOpacity * 100) + ')'; + } + if (style.fontFamily) { + textbox.style.fontFamily = style.fontFamily; + } + if (style.fontSize) { + textbox.style.fontSize = style.fontSize; + } + if (style.fontWeight) { + textbox.style.fontWeight = style.fontWeight; + } + if (style.fontStyle) { + textbox.style.fontStyle = style.fontStyle; + } + if(style.labelSelect === true) { + label._featureId = featureId; + textbox._featureId = featureId; + textbox._geometry = location; + textbox._geometryClass = location.CLASS_NAME; + } + textbox.style.whiteSpace = "nowrap"; + // fun with IE: IE7 in standards compliant mode does not display any + // text with a left inset of 0. So we set this to 1px and subtract one + // pixel later when we set label.style.left + textbox.inset = "1px,0px,0px,0px"; + + if(!label.parentNode) { + label.appendChild(textbox); + this.textRoot.appendChild(label); + } + + var align = style.labelAlign || "cm"; + if (align.length == 1) { + align += "m"; + } + var xshift = textbox.clientWidth * + (OpenLayers.Renderer.VML.LABEL_SHIFT[align.substr(0,1)]); + var yshift = textbox.clientHeight * + (OpenLayers.Renderer.VML.LABEL_SHIFT[align.substr(1,1)]); + label.style.left = parseInt(label.style.left)-xshift-1+"px"; + label.style.top = parseInt(label.style.top)+yshift+"px"; + + }, + + /** + * Method: moveRoot + * moves this renderer's root to a different renderer. + * + * Parameters: + * renderer - {<OpenLayers.Renderer>} target renderer for the moved root + * root - {DOMElement} optional root node. To be used when this renderer + * holds roots from multiple layers to tell this method which one to + * detach + * + * Returns: + * {Boolean} true if successful, false otherwise + */ + moveRoot: function(renderer) { + var layer = this.map.getLayer(renderer.container.id); + if(layer instanceof OpenLayers.Layer.Vector.RootContainer) { + layer = this.map.getLayer(this.container.id); + } + layer && layer.renderer.clear(); + OpenLayers.Renderer.Elements.prototype.moveRoot.apply(this, arguments); + layer && layer.redraw(); + }, + + /** + * Method: importSymbol + * add a new symbol definition from the rendererer's symbol hash + * + * Parameters: + * graphicName - {String} name of the symbol to import + * + * Returns: + * {Object} - hash of {DOMElement} "symbol" and {Number} "size" + */ + importSymbol: function (graphicName) { + var id = this.container.id + "-" + graphicName; + + // check if symbol already exists in the cache + var cache = this.symbolCache[id]; + if (cache) { + return cache; + } + + var symbol = OpenLayers.Renderer.symbol[graphicName]; + if (!symbol) { + throw new Error(graphicName + ' is not a valid symbol name'); + } + + var symbolExtent = new OpenLayers.Bounds( + Number.MAX_VALUE, Number.MAX_VALUE, 0, 0); + + var pathitems = ["m"]; + for (var i=0; i<symbol.length; i=i+2) { + var x = symbol[i]; + var 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); + + pathitems.push(x); + pathitems.push(y); + if (i == 0) { + pathitems.push("l"); + } + } + pathitems.push("x e"); + var path = pathitems.join(" "); + + var diff = (symbolExtent.getWidth() - symbolExtent.getHeight()) / 2; + if(diff > 0) { + symbolExtent.bottom = symbolExtent.bottom - diff; + symbolExtent.top = symbolExtent.top + diff; + } else { + symbolExtent.left = symbolExtent.left + diff; + symbolExtent.right = symbolExtent.right - diff; + } + + cache = { + path: path, + size: symbolExtent.getWidth(), // equals getHeight() now + left: symbolExtent.left, + bottom: symbolExtent.bottom + }; + this.symbolCache[id] = cache; + + return cache; + }, + + CLASS_NAME: "OpenLayers.Renderer.VML" +}); + +/** + * Constant: OpenLayers.Renderer.VML.LABEL_SHIFT + * {Object} + */ +OpenLayers.Renderer.VML.LABEL_SHIFT = { + "l": 0, + "c": .5, + "r": 1, + "t": 0, + "m": .5, + "b": 1 +}; |