diff --git a/examples/canvas-hit-detection.html b/examples/canvas-hit-detection.html new file mode 100644 index 0000000000..fc4bda69a7 --- /dev/null +++ b/examples/canvas-hit-detection.html @@ -0,0 +1,24 @@ + + + + OpenLayers Canvas Hit Detection Example + + + + + + +

Feature Hit Detection with Canvas

+

+ Demonstrates detection of feature hits with the canvas renderer. +

+
+
+

+ View the canvas-hit-detection.js + source to see how this is done. +

+
+ + + diff --git a/examples/canvas-hit-detection.js b/examples/canvas-hit-detection.js new file mode 100644 index 0000000000..b45562eab0 --- /dev/null +++ b/examples/canvas-hit-detection.js @@ -0,0 +1,88 @@ + +// create some sample features +var Feature = OpenLayers.Feature.Vector; +var Geometry = OpenLayers.Geometry; +var features = [ + new Feature(new Geometry.Point(-90, 45)), + new Feature( + new Geometry.Point(0, 45), + {cls: "one"} + ), + new Feature( + new Geometry.Point(90, 45), + {cls: "two"} + ), + new Feature( + Geometry.fromWKT("LINESTRING(-110 -60, -80 -40, -50 -60, -20 -40)") + ), + new Feature( + Geometry.fromWKT("POLYGON((20 -20, 110 -20, 110 -80, 20 -80, 20 -20), (40 -40, 90 -40, 90 -60, 40 -60, 40 -40))") + ) +]; + +// create rule based styles +var Rule = OpenLayers.Rule; +var Filter = OpenLayers.Filter; +var style = new OpenLayers.Style({ + pointRadius: 10, + strokeWidth: 2, + strokeOpacity: 0.7, + strokeColor: "navy", + fillColor: "#ffcc66", + fillOpacity: 1 +}, { + rules: [ + new Rule({ + filter: new Filter.Comparison({ + type: "==", + property: "cls", + value: "one" + }), + symbolizer: { + externalGraphic: "../img/marker-blue.png" + } + }), + new Rule({ + filter: new Filter.Comparison({ + type: "==", + property: "cls", + value: "two" + }), + symbolizer: { + externalGraphic: "../img/marker-green.png" + } + }), + new Rule({ + elseFilter: true, + symbolizer: { + graphicName: "circle" + } + }) + ] +}); + +var layer = new OpenLayers.Layer.Vector(null, { + styleMap: new OpenLayers.StyleMap({ + "default": style, + select: { + fillColor: "red", + pointRadius: 13, + strokeColor: "yellow", + strokeWidth: 3 + } + }), + isBaseLayer: true, + renderers: ["Canvas"] +}); +layer.addFeatures(features); + +var map = new OpenLayers.Map({ + div: "map", + layers: [layer], + center: new OpenLayers.LonLat(0, 0), + zoom: 0 +}); + +var select = new OpenLayers.Control.SelectFeature(layer, {hover: true}); +map.addControl(select); +select.activate(); diff --git a/examples/canvas-inspector.html b/examples/canvas-inspector.html new file mode 100644 index 0000000000..8244dc512d --- /dev/null +++ b/examples/canvas-inspector.html @@ -0,0 +1,52 @@ + + + + OpenLayers Canvas Inspector + + + + + + + + +

Canvas Inspector

+

+ Displays pixel values for canvas context. +

+
+
+

+ View the canvas-inspector.js + source to see how this is done. +

+
+
+
+ + + + +
+   +
+ + + diff --git a/examples/canvas-inspector.js b/examples/canvas-inspector.js new file mode 100644 index 0000000000..064b4d5b56 --- /dev/null +++ b/examples/canvas-inspector.js @@ -0,0 +1,91 @@ + +var features = [ + + new OpenLayers.Feature.Vector( + OpenLayers.Geometry.fromWKT( + "LINESTRING(-90 90, 90 -90)" + ), + {color: "#0f0000"} + ), + + new OpenLayers.Feature.Vector( + OpenLayers.Geometry.fromWKT( + "LINESTRING(100 50, -100 -50)" + ), + {color: "#00ff00"} + ) + +]; + +var layer = new OpenLayers.Layer.Vector(null, { + styleMap: new OpenLayers.StyleMap({ + strokeWidth: 3, + strokeColor: "${color}" + }), + isBaseLayer: true, + renderers: ["Canvas"], + rendererOptions: {hitDetection: true} +}); +layer.addFeatures(features); + +var map = new OpenLayers.Map({ + div: "map", + layers: [layer], + center: new OpenLayers.LonLat(0, 0), + zoom: 0 +}); + +var xOff = 2, yOff = 2; + +var rows = 1 + (2 * yOff); +var cols = 1 + (2 * xOff); + +var template = new jugl.Template("template"); +template.process({ + clone: true, + parent: "inspector", + context: { + rows: rows, + cols: cols + } +}); + +function isDark(r, g, b, a) { + a = a / 255; + var da = 1 - a; + // convert color values to decimal (assume white background) + r = (a * r / 255) + da; + g = (a * g / 255) + da; + b = (a * b / 255) + da; + // use w3C brightness measure + var brightness = (r * 0.299) + (g * 0.587) + (b * 0.144); + return brightness < 0.5; +} + +var context = layer.renderer.canvas; //layer.renderer.hitContext; +var size = map.getSize(); +map.events.on({ + mousemove: function(event) { + var x = event.xy.x - 1; // TODO: fix this elsewhere + var y = event.xy.y; + if ((x >= xOff) && (x < size.w - xOff) && (y >= yOff) && (y < size.h - yOff)) { + var data = context.getImageData(x - xOff, y - yOff, rows, cols).data; + var offset, red, green, blue, alpha, cell; + for (var i=0; iG: " + green + "
B: " + blue + "
A: " + alpha; + cell.style.backgroundColor = "rgba(" + red + ", " + green + ", " + blue + ", " + (alpha / 255) + ")"; + cell.style.color = isDark(red, green, blue, alpha) ? "#ffffff" : "#000000"; + } + } + } + } +}); + + diff --git a/lib/OpenLayers/Layer/Vector.js b/lib/OpenLayers/Layer/Vector.js index d395d0e067..d09a183dc3 100644 --- a/lib/OpenLayers/Layer/Vector.js +++ b/lib/OpenLayers/Layer/Vector.js @@ -843,9 +843,17 @@ OpenLayers.Layer.Vector = OpenLayers.Class(OpenLayers.Layer, { if (!this.renderer) { OpenLayers.Console.error(OpenLayers.i18n("getFeatureError")); return null; - } + } + var feature = null; var featureId = this.renderer.getFeatureIdFromEvent(evt); - return this.getFeatureById(featureId); + if (featureId) { + if (typeof featureId === "string") { + feature = this.getFeatureById(featureId); + } else { + feature = featureId; + } + } + return feature; }, /** diff --git a/lib/OpenLayers/Renderer.js b/lib/OpenLayers/Renderer.js index 4a48dfe0a4..500e120b26 100644 --- a/lib/OpenLayers/Renderer.js +++ b/lib/OpenLayers/Renderer.js @@ -83,6 +83,7 @@ OpenLayers.Renderer = OpenLayers.Class({ */ initialize: function(containerID, options) { this.container = OpenLayers.Util.getElement(containerID); + OpenLayers.Util.extend(this, options); }, /** diff --git a/lib/OpenLayers/Renderer/Canvas.js b/lib/OpenLayers/Renderer/Canvas.js index 3a5de7ce56..3f885ebe22 100644 --- a/lib/OpenLayers/Renderer/Canvas.js +++ b/lib/OpenLayers/Renderer/Canvas.js @@ -15,6 +15,22 @@ * - */ 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 @@ -32,14 +48,21 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, { * Constructor: OpenLayers.Renderer.Canvas * * Parameters: - * containerID - {} + * containerID - {} + * options - {Object} Optional properties to be set on the renderer. */ - initialize: function(containerID) { + 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"); + this.hitGraphicCanvas = document.createElement("canvas"); + this.hitGraphicContext = this.hitGraphicCanvas.getContext("2d"); + } }, /** @@ -78,11 +101,24 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, { */ setSize: function(size) { this.size = size.clone(); - this.root.style.width = size.w + "px"; - this.root.style.height = size.h + "px"; - this.root.width = size.w; - this.root.height = size.h; + 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; + var hitGraphicCanvas = this.hitGraphicCanvas; + hitGraphicCanvas.style.width = size.w + "px"; + hitGraphicCanvas.style.height = size.h + "px"; + hitGraphicCanvas.width = size.w; + hitGraphicCanvas.height = size.h; + } }, /** @@ -112,29 +148,29 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, { * geometry - {} * style - {Object} */ - drawGeometry: function(geometry, style) { + 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); + this.drawGeometry(geometry.components[i], style, featureId); } return; } switch (geometry.CLASS_NAME) { case "OpenLayers.Geometry.Point": - this.drawPoint(geometry, style); + this.drawPoint(geometry, style, featureId); break; case "OpenLayers.Geometry.LineString": - this.drawLineString(geometry, style); + this.drawLineString(geometry, style, featureId); break; case "OpenLayers.Geometry.LinearRing": - this.drawLinearRing(geometry, style); + this.drawLinearRing(geometry, style, featureId); break; case "OpenLayers.Geometry.Polygon": - this.drawPolygon(geometry, style); + this.drawPolygon(geometry, style, featureId); break; default: break; @@ -148,37 +184,70 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, { * Parameters: * geometry - {} * style - {Object} + * featureId - {String} */ - drawExternalGraphic: function(pt, style) { - var img = new Image(); - - if(style.graphicTitle) { - img.title=style.graphicTitle; - } + drawExternalGraphic: function(pt, style, featureId) { + var img = new Image(); + + if (style.graphicTitle) { + img.title = style.graphicTitle; + } - 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) ? + 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) ? + var yOffset = (style.graphicYOffset != undefined) ? style.graphicYOffset : -(0.5 * height); - - var context = { img: img, - x: (pt[0]+xOffset), - y: (pt[1]+yOffset), - width: width, - height: height, - opacity: style.graphicOpacity || style.fillOpacity, - canvas: this.canvas }; - img.onload = OpenLayers.Function.bind( function() { - this.canvas.globalAlpha = this.opacity; - this.canvas.drawImage(this.img, this.x, - this.y, this.width, this.height); - }, context); - img.src = style.externalGraphic; + var x = pt[0] + xOffset; + var y = pt[1] + yOffset; + + var numRows = this.root.width; + var numCols = this.root.height; + + var opacity = style.graphicOpacity || style.fillOpacity; + + var rgb = this.featureIdToRGB(featureId); + var red = rgb[0]; + var green = rgb[1]; + var blue = rgb[2]; + + var onLoad = function() { + // TODO: check that we haven't moved + var canvas = this.canvas; + canvas.globalAlpha = opacity; + canvas.drawImage( + img, x, y, width, height + ); + if (this.hitDetection) { + var hitGraphicContext = this.hitGraphicContext; + var hitContext = this.hitContext; + hitGraphicContext.clearRect(0, 0, numRows, numCols); + hitGraphicContext.drawImage( + img, 0, 0, width, height + ); + var imagePixels = hitGraphicContext.getImageData(0, 0, width, height).data; + var indexData = hitContext.createImageData(width, height); + var indexPixels = indexData.data; + var pixelIndex; + for (var i=0, len=imagePixels.length; i 0) { + indexData[i] = red; + indexPixels[i+1] = green; + indexPixels[i+2] = blue; + indexPixels[i+3] = 255; + } + } + hitContext.putImageData(indexData, x, y); + } + }; + + img.onload = OpenLayers.Function.bind(onLoad, this); + img.src = style.externalGraphic; }, /** @@ -190,10 +259,10 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, { * style - {Object} Symbolizer hash */ setCanvasStyle: function(type, style) { - if (type == "fill") { + if (type === "fill") { this.canvas.globalAlpha = style['fillOpacity']; this.canvas.fillStyle = style['fillColor']; - } else if (type == "stroke") { + } else if (type === "stroke") { this.canvas.globalAlpha = style['strokeOpacity']; this.canvas.strokeStyle = style['strokeColor']; this.canvas.lineWidth = style['strokeWidth']; @@ -202,6 +271,72 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, { 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: featureIdToRGB + * Convert a feature ID string into an RGB array. + * + * Parameters: + * featureId - {String} Feature id + * + * Returns: + * {Array} RGB values. + */ + featureIdToRGB: function(featureId) { + var hex = this.featureIdToHex(featureId); + return [ + parseInt(hex.substring(1, 3), 16), + parseInt(hex.substring(3, 5), 16), + parseInt(hex.substring(5, 7), 16) + ]; + }, + + /** + * 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) { + 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 + this.hitContext.lineWidth = symbolizer.strokeWidth + 2; + } else { + this.hitContext.globalAlpha = 0; + this.hitContext.lineWidth = 1; + } + }, /** * Method: drawPoint @@ -210,32 +345,50 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, { * Parameters: * geometry - {} * style - {Object} + * featureId - {String} */ - drawPoint: function(geometry, style) { + drawPoint: function(geometry, style, featureId) { if(style.graphic !== false) { var pt = this.getLocalXY(geometry); - - if (style.externalGraphic) { - this.drawExternalGraphic(pt, style); - } else { - if(style.fill !== false) { - this.setCanvasStyle("fill", style); - this.canvas.beginPath(); - this.canvas.arc(pt[0], pt[1], style.pointRadius, 0, Math.PI*2, true); - this.canvas.fill(); - } - - if(style.stroke !== false) { - this.setCanvasStyle("stroke", style); - this.canvas.beginPath(); - this.canvas.arc(pt[0], pt[1], style.pointRadius, 0, Math.PI*2, true); - this.canvas.stroke(); - this.setCanvasStyle("reset"); + var p0 = pt[0]; + var p1 = pt[1]; + if (!isNaN(p0) && !isNaN(p1)) { + if (style.externalGraphic) { + this.drawExternalGraphic(pt, style, featureId); + } else { + 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. @@ -243,20 +396,11 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, { * Parameters: * geometry - {} * style - {Object} + * featureId - {String} */ - drawLineString: function(geometry, style) { - if(style.stroke !== false) { - this.setCanvasStyle("stroke", style); - this.canvas.beginPath(); - var start = this.getLocalXY(geometry.components[0]); - this.canvas.moveTo(start[0], start[1]); - for(var i = 1; i < geometry.components.length; i++) { - var pt = this.getLocalXY(geometry.components[i]); - this.canvas.lineTo(pt[0], pt[1]); - } - this.canvas.stroke(); - } - this.setCanvasStyle("reset"); + drawLineString: function(geometry, style, featureId) { + style = OpenLayers.Util.applyDefaults({fill: false}, style); + this.drawLinearRing(geometry, style, featureId); }, /** @@ -266,33 +410,52 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, { * Parameters: * geometry - {} * style - {Object} + * featureId - {String} */ - drawLinearRing: function(geometry, style) { - if(style.fill !== false) { + drawLinearRing: function(geometry, style, featureId) { + if (style.fill !== false) { this.setCanvasStyle("fill", style); - this.canvas.beginPath(); - var start = this.getLocalXY(geometry.components[0]); - this.canvas.moveTo(start[0], start[1]); - for(var i = 1; i < geometry.components.length - 1 ; i++) { - var pt = this.getLocalXY(geometry.components[i]); - this.canvas.lineTo(pt[0], pt[1]); + this.renderPath(this.canvas, geometry, style, featureId, "fill"); + if (this.hitDetection) { + this.setHitContextStyle("fill", featureId, style); + this.renderPath(this.hitContext, geometry, style, featureId, "fill"); } - this.canvas.fill(); } - - if(style.stroke !== false) { + if (style.stroke !== false) { this.setCanvasStyle("stroke", style); - this.canvas.beginPath(); - var start = this.getLocalXY(geometry.components[0]); - this.canvas.moveTo(start[0], start[1]); - for(var i = 1; i < geometry.components.length; i++) { - var pt = this.getLocalXY(geometry.components[i]); - this.canvas.lineTo(pt[0], pt[1]); + 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.canvas.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) { - this.drawLinearRing(geometry.components[0], style); - for (var i = 1; i < geometry.components.length; i++) { - this.drawLinearRing(geometry.components[i], { - fillOpacity: 0, - strokeWidth: 0, - strokeOpacity: 0, - strokeColor: '#000000', - fillColor: '#000000'} - ); // inner rings are 'empty' + 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} * * Returns: - * {String} A feature id or null. + * {