Adding in support for hit detection with canvas (and rendering holes in polygons). r=fredj (closes #3207)

git-svn-id: http://svn.openlayers.org/trunk/openlayers@11849 dc9f47b5-9b13-0410-9fdd-eb0c1a62fdaf
This commit is contained in:
Tim Schaub
2011-03-31 21:00:03 +00:00
parent 66dabae11b
commit 3ef0b05949
8 changed files with 733 additions and 120 deletions

View File

@@ -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 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, {
* 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 @@ 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 - {<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 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, {
* 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 @@ 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 - {<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
@@ -210,32 +345,50 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, {
* 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();
}
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 - {<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,33 +410,52 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, {
* 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
@@ -301,17 +464,40 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, {
* 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 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, {
* 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 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, {
* 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 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, {
*/
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 @@ OpenLayers.Renderer.Canvas = OpenLayers.Class(OpenLayers.Renderer, {
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]);
}