/* 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.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 - {} * 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 - {} * 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 - {} * 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 - {} */ 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 - {} * style - {} * * 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 - {} * 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 - {} * 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 - {} * 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= 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 - {} 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 - {} * 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 - {} * 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 - {} * 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} * 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} * 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 (} */ 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 - {} * * Returns: * {)} */ eraseFeatures: function(features) { if(!(OpenLayers.Util.isArray(features))) { features = [features]; } for(var i=0; i