[OpenLayers-Commits] r11849 - in trunk/openlayers: examples lib/OpenLayers lib/OpenLayers/Layer lib/OpenLayers/Renderer tests/Renderer

commits-20090109 at openlayers.org commits-20090109 at openlayers.org
Thu Mar 31 17:00:06 EDT 2011


Author: tschaub
Date: 2011-03-31 14:00:03 -0700 (Thu, 31 Mar 2011)
New Revision: 11849

Added:
   trunk/openlayers/examples/canvas-hit-detection.html
   trunk/openlayers/examples/canvas-hit-detection.js
   trunk/openlayers/examples/canvas-inspector.html
   trunk/openlayers/examples/canvas-inspector.js
Modified:
   trunk/openlayers/lib/OpenLayers/Layer/Vector.js
   trunk/openlayers/lib/OpenLayers/Renderer.js
   trunk/openlayers/lib/OpenLayers/Renderer/Canvas.js
   trunk/openlayers/tests/Renderer/Canvas.html
Log:
Adding in support for hit detection with canvas (and rendering holes in polygons). r=fredj (closes #3207)

Added: trunk/openlayers/examples/canvas-hit-detection.html
===================================================================
--- trunk/openlayers/examples/canvas-hit-detection.html	                        (rev 0)
+++ trunk/openlayers/examples/canvas-hit-detection.html	2011-03-31 21:00:03 UTC (rev 11849)
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>OpenLayers Canvas Hit Detection Example</title>
+        <link rel="stylesheet" href="../theme/default/style.css" type="text/css">
+        <link rel="stylesheet" href="../theme/default/google.css" type="text/css">
+        <link rel="stylesheet" href="style.css" type="text/css">
+        <script src="../lib/OpenLayers.js"></script>
+    </head>
+    <body>
+        <h1 id="title">Feature Hit Detection with Canvas</h1>
+        <p id="shortdesc">
+            Demonstrates detection of feature hits with the canvas renderer.
+        </p>
+        <div id="map" class="smallmap"></div>
+        <div id="docs">
+            <p>
+                View the <a href="canvas-hit-detection.js" target="_blank">canvas-hit-detection.js</a>
+                source to see how this is done.
+            </p>
+        </div>
+        <script src="canvas-hit-detection.js"></script>
+    </body>
+</html>

Added: trunk/openlayers/examples/canvas-hit-detection.js
===================================================================
--- trunk/openlayers/examples/canvas-hit-detection.js	                        (rev 0)
+++ trunk/openlayers/examples/canvas-hit-detection.js	2011-03-31 21:00:03 UTC (rev 11849)
@@ -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();

Added: trunk/openlayers/examples/canvas-inspector.html
===================================================================
--- trunk/openlayers/examples/canvas-inspector.html	                        (rev 0)
+++ trunk/openlayers/examples/canvas-inspector.html	2011-03-31 21:00:03 UTC (rev 11849)
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>OpenLayers Canvas Inspector</title>
+        <link rel="stylesheet" href="../theme/default/style.css" type="text/css">
+        <link rel="stylesheet" href="../theme/default/google.css" type="text/css">
+        <link rel="stylesheet" href="style.css" type="text/css">
+        <script src="../lib/OpenLayers.js"></script>
+        <script src="Jugl.js"></script>
+        <style>
+            #template {
+                display: none;
+            }
+            #inspector table {
+                border-right: 1px solid #666;
+                border-bottom: 1px solid #666;
+            }
+            #inspector table td {
+                font-size: 9px;
+                text-align: center;
+                width: 60px;
+                height: 60px;
+                border-top: 1px solid #666;
+                border-left: 1px solid #666;
+            }
+        </style>
+    </head>
+    <body>
+        <h1 id="title">Canvas Inspector</h1>
+        <p id="shortdesc">
+            Displays pixel values for canvas context.
+        </p>
+        <div id="map" class="smallmap"></div>
+        <div id="docs">
+            <p>
+                View the <a href="canvas-inspector.js" target="_blank">canvas-inspector.js</a>
+                source to see how this is done.
+            </p>
+        </div>
+        <div id="inspector">
+        </div>
+        <table id="template">
+            <tr jugl:repeat="row new Array(rows)">
+                <td jugl:repeat="col new Array(cols)" 
+                    jugl:attributes="id 'c' + repeat.col.index + 'r' + repeat.row.index">
+                    &nbsp;
+                </td>
+            </tr>
+        </table>
+        <script src="canvas-inspector.js"></script>
+    </body>
+</html>

Added: trunk/openlayers/examples/canvas-inspector.js
===================================================================
--- trunk/openlayers/examples/canvas-inspector.js	                        (rev 0)
+++ trunk/openlayers/examples/canvas-inspector.js	2011-03-31 21:00:03 UTC (rev 11849)
@@ -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; i<cols; ++i) {
+                for (var j=0; j<rows; ++j) {
+                    offset = (i * 4) + (j * 4 * cols);
+                    red = data[offset];
+                    green = data[offset + 1];
+                    blue = data[offset + 2];
+                    alpha = data[offset + 3];
+                    cell = document.getElementById("c" + i + "r" + j);
+                    cell.innerHTML = "R: " + red + "<br>G: " + green + "<br>B: " + blue + "<br>A: " + alpha;
+                    cell.style.backgroundColor = "rgba(" + red + ", " + green + ", " + blue + ", " + (alpha / 255) + ")";
+                    cell.style.color = isDark(red, green, blue, alpha) ? "#ffffff" : "#000000";
+                }
+            }
+        }
+    }
+});
+
+

Modified: trunk/openlayers/lib/OpenLayers/Layer/Vector.js
===================================================================
--- trunk/openlayers/lib/OpenLayers/Layer/Vector.js	2011-03-31 16:26:42 UTC (rev 11848)
+++ trunk/openlayers/lib/OpenLayers/Layer/Vector.js	2011-03-31 21:00:03 UTC (rev 11849)
@@ -843,9 +843,17 @@
         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;
     },
 
     /**

Modified: trunk/openlayers/lib/OpenLayers/Renderer/Canvas.js
===================================================================
--- trunk/openlayers/lib/OpenLayers/Renderer/Canvas.js	2011-03-31 16:26:42 UTC (rev 11848)
+++ trunk/openlayers/lib/OpenLayers/Renderer/Canvas.js	2011-03-31 21:00:03 UTC (rev 11849)
@@ -15,6 +15,22 @@
  *  - <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
@@ -32,14 +48,21 @@
      * Constructor: OpenLayers.Renderer.Canvas
      *
      * Parameters:
-     * containerID - {<String>} 
+     * containerID - {<String>}
+     * 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 @@
      */
     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 @@
      * geometry - {<OpenLayers.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 @@
      * Parameters: 
      * geometry - {<OpenLayers.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();
 
-       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) ?
+        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) ?
            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<len; i+=4) {
+                    // look for visible pixels
+                    if (imagePixels[i+3] > 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 @@
      * 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,40 +271,124 @@
             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 - {<OpenLayers.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
      * This method is only called by the renderer itself.
      * 
      * Parameters: 
      * geometry - {<OpenLayers.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();
+            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");
+                    }
                 }
-                
-                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");
-                }
             }
         }
     },
-
+    
     /**
      * Method: drawLineString
      * This method is only called by the renderer itself.
@@ -243,20 +396,11 @@
      * Parameters: 
      * geometry - {<OpenLayers.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,52 +410,94 @@
      * Parameters: 
      * geometry - {<OpenLayers.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<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) {
-        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<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
+            );
         }
     },
     
@@ -408,8 +594,13 @@
      * Clear all vectors from the renderer.
      */    
     clear: function() {
-        this.canvas.clearRect(0, 0, this.root.width, this.root.height);
+        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);
+        }
     },
 
     /**
@@ -420,23 +611,28 @@
      * evt - {<OpenLayers.Event>} 
      *
      * Returns:
-     * {String} A feature id or null.
+     * {<OpenLayers.Feature.Vector} A feature or null.  This method returns a 
+     *     feature instead of a feature id to avoid an unnecessary lookup on the
+     *     layer.
      */
     getFeatureIdFromEvent: function(evt) {
-        var loc = this.map.getLonLatFromPixel(evt.xy);
-        var resolution = this.getResolution();
-        var bounds = new OpenLayers.Bounds(loc.lon - resolution * 5, 
-                                           loc.lat - resolution * 5, 
-                                           loc.lon + resolution * 5, 
-                                           loc.lat + resolution * 5);
-        var geom = bounds.toGeometry();
-        for (var feat in this.features) {
-            if (!this.features.hasOwnProperty(feat)) { continue; }
-            if (this.features[feat][0].geometry.intersects(geom)) {
-                return feat;
+        var feature = null;
+        if (this.hitDetection) {
+            // 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) {
+                        feature = this.features["OpenLayers.Feature.Vector_" + (id - 1 + this.hitOverflow)][0];
+                    }
+                }
             }
-        }   
-        return null;
+        }
+        return feature;
     },
     
     /**
@@ -467,7 +663,12 @@
      */
     redraw: function() {
         if (!this.locked) {
-            this.canvas.clearRect(0, 0, this.root.width, this.root.height);
+            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, style;
             for (var id in this.features) {
@@ -475,7 +676,7 @@
                 feature = this.features[id][0];
                 style = this.features[id][1];
                 if (!feature.geometry) { continue; }
-                this.drawGeometry(feature.geometry, style);
+                this.drawGeometry(feature.geometry, style, feature.id);
                 if(style.label) {
                     labelMap.push([feature, style]);
                 }

Modified: trunk/openlayers/lib/OpenLayers/Renderer.js
===================================================================
--- trunk/openlayers/lib/OpenLayers/Renderer.js	2011-03-31 16:26:42 UTC (rev 11848)
+++ trunk/openlayers/lib/OpenLayers/Renderer.js	2011-03-31 21:00:03 UTC (rev 11849)
@@ -83,6 +83,7 @@
      */
     initialize: function(containerID, options) {
         this.container = OpenLayers.Util.getElement(containerID);
+        OpenLayers.Util.extend(this, options);
     },
     
     /**

Modified: trunk/openlayers/tests/Renderer/Canvas.html
===================================================================
--- trunk/openlayers/tests/Renderer/Canvas.html	2011-03-31 16:26:42 UTC (rev 11848)
+++ trunk/openlayers/tests/Renderer/Canvas.html	2011-03-31 21:00:03 UTC (rev 11849)
@@ -56,6 +56,75 @@
         t.eq(r.resolution, resolution, "resolution is correctly set");
     }
 
+    function test_featureIdToRGB(t) {
+        if (!supported) {
+            t.plan(0); 
+            return;
+        }
+        t.plan(2);
+        var el = document.body;
+        el.id = "foo";
+        var renderer = new OpenLayers.Renderer.Canvas(el.id);
+        
+        var cases = [{
+            id: "foo_0", rgb: [0, 0, 1]
+        }, {
+            id: "foo_10", rgb: [0, 0, 11]
+        }, {
+            id: "foo_100", rgb: [0, 0, 101]
+        }, {
+            id: "foo_1000000", rgb: [15, 66, 65]
+        }, {
+            id: "foo_16777214", rgb: [255, 255, 255]
+        }, {
+            id: "foo_16777215", rgb: [0, 0, 1]
+        }];
+        t.plan(cases.length);
+        
+        var c;
+        for (var i=0; i<cases.length; ++i) {
+            c = cases[i];
+            t.eq(renderer.featureIdToRGB(c.id), c.rgb, c.id);
+        }
+        
+        renderer.destroy();
+    }
+
+    function test_featureIdToHex(t) {
+        if (!supported) {
+            t.plan(0); 
+            return;
+        }
+        t.plan(2);
+        var el = document.body;
+        el.id = "foo";
+        var renderer = new OpenLayers.Renderer.Canvas(el.id);
+        
+        var cases = [{
+            id: "foo_0", hex: "#000001"
+        }, {
+            id: "foo_10", hex: "#00000b"
+        }, {
+            id: "foo_100", hex: "#000065"
+        }, {
+            id: "foo_1000000", hex: "#0f4241"
+        }, {
+            id: "foo_16777214", hex: "#ffffff"
+        }, {
+            id: "foo_16777215", hex: "#000001"
+        }];
+        t.plan(cases.length);
+        
+        var c;
+        for (var i=0; i<cases.length; ++i) {
+            c = cases[i];
+            t.eq(renderer.featureIdToHex(c.id), c.hex, c.id);
+        }
+        
+        renderer.destroy();
+    }
+
+
     function test_Renderer_Canvas_destroy(t) {
         if (!supported) { t.plan(0); return; }
         t.plan(5);
@@ -77,7 +146,86 @@
         t.eq(r.resolution, null, "resolution nullified");
         t.eq(r.map, null, "map nullified");
     }
+    
+    function test_hitDetection(t) {
+        if (!supported) {
+            t.plan(0); 
+            return; 
+        }
+        
+        var layer = new OpenLayers.Layer.Vector(null, {
+            isBaseLayer: true,
+            resolutions: [1],
+            styleMap: new OpenLayers.StyleMap({
+                pointRadius: 5,
+                strokeWidth: 3,
+                fillColor: "red",
+                fillOpacity: 0.5,
+                strokeColor: "blue",
+                strokeOpacity: 0.75
+            }),
+            renderers: ["Canvas"]
+        });
+        
+        var map = new OpenLayers.Map({
+            div: "map",
+            controls: [],
+            layers: [layer],
+            center: new OpenLayers.LonLat(0, 0),
+            zoom: 0
+        });
+        
+        layer.addFeatures([
+            new OpenLayers.Feature.Vector(
+                new OpenLayers.Geometry.Point(-100, 0)
+            ),
+            new OpenLayers.Feature.Vector(
+                OpenLayers.Geometry.fromWKT("LINESTRING(-50 0, 50 0)")
+            ),
+            new OpenLayers.Feature.Vector(
+                OpenLayers.Geometry.fromWKT("POLYGON((100 -25, 150 -25, 150 25, 100 25, 100 -25), (120 -5, 130 -5, 130 5, 120 5, 120 -5))")
+            )
+        ]);
+        
+        var cases = [{
+            msg: "center of point", x: -100, y: 0, id: layer.features[0].id
+        }, {
+            msg: "edge of point", x: -103, y: 3, id: layer.features[0].id
+        }, {
+            msg: "outside point", x: -110, y: 3, id: null
+        }, {
+            msg: "center of line", x: 0, y: 0, id: layer.features[1].id
+        }, {
+            msg: "edge of line", x: 0, y: 1, id: layer.features[1].id
+        }, {
+            msg: "outside line", x: 0, y: 5, id: null
+        }, {
+            msg: "inside polygon", x: 110, y: 0, id: layer.features[2].id
+        }, {
+            msg: "edge of polygon", x: 99, y: 0, id: layer.features[2].id
+        }, {
+            msg: "inside polygon hole", x: 125, y: 0, id: null
+        }, {
+            msg: "outside polygon", x: 155, y: 0, id: null
+        }];
 
+        function px(x, y) {
+            return map.getPixelFromLonLat(
+                new OpenLayers.LonLat(x, y)
+            );
+        }        
+        
+        var num = cases.length;
+        t.plan(num);
+        var c, feature;
+        for (var i=0; i<num; ++i) {
+            c = cases[i];
+            feature = layer.renderer.getFeatureIdFromEvent({xy: px(c.x, c.y)});
+            t.eq(feature && feature.id, c.id, c.msg);
+        }
+        
+    }
+
   </script>
 </head>
 <body>



More information about the Commits mailing list