Accurate hit detection

With this change, hit detection for lines and points gets very
accurate, because the vector renderer instance keeps track of
line widths and point symbol sizes. After doing a bbox query in
the RTree, returned lines and points are evaluated against the
thresholds of their line width or symbol size. The KML example
with its different symbolizers now has getFeatureInfo too to
show this in action.
This commit is contained in:
ahocevar
2013-05-15 23:44:22 +02:00
parent 32ea32521d
commit 037e44e084
5 changed files with 136 additions and 26 deletions

View File

@@ -43,6 +43,11 @@
</div>
<div id="tags">KML</div>
</div>
<div class="span4 pull-right">
<div id="info" class="alert alert-success">
&nbsp;
</div>
</div>
</div>

View File

@@ -43,6 +43,20 @@ var map = new ol.Map({
var kml = new ol.parser.KML({
maxDepth: 1, dimension: 2, extractStyles: true, extractAttributes: true});
map.on('mousemove', function(evt) {
map.getFeatureInfo({
pixel: evt.getPixel(),
layers: [vector],
success: function(features) {
var info = [];
for (var i = 0, ii = features.length; i < ii; ++i) {
info.push(features[i].get('name'));
}
document.getElementById('info').innerHTML = info.join(', ') || '&nbsp;';
}
});
});
var url = 'data/kml/lines.kml';
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);

View File

@@ -57,16 +57,26 @@ ol.extent.boundingExtentXYs_ = function(xs, ys, opt_extent) {
/**
* Checks if the passed coordinate is contained or on the edge
* Checks if (one of) the passed coordinate(s) is contained or on the edge
* of the extent.
*
* @param {ol.Extent} extent Extent.
* @param {ol.Coordinate} coordinate Coordinate.
* @param {ol.Coordinate|Array.<ol.Coordinate>} coordinates Coordinate(s).
* @return {boolean} Contains.
*/
ol.extent.containsCoordinate = function(extent, coordinate) {
return extent[0] <= coordinate[0] && coordinate[0] <= extent[1] &&
extent[2] <= coordinate[1] && coordinate[1] <= extent[3];
ol.extent.containsCoordinate = function(extent, coordinates) {
coordinates = goog.isArray(coordinates[0]) ? coordinates : [coordinates];
var contains = false,
coordinate, i;
for (i = coordinates.length - 1; i >= 0; --i) {
coordinate = coordinates[i];
if (extent[0] <= coordinate[0] && coordinate[0] <= extent[1] &&
extent[2] <= coordinate[1] && coordinate[1] <= extent[3]) {
contains = true;
break;
}
}
return contains;
};

View File

@@ -137,6 +137,18 @@ ol.renderer.canvas.VectorLayer = function(mapRenderer, layer) {
*/
this.tileGrid_ = null;
/**
* @type {Object.<number, Array.<number>>}
* @private
*/
this.symbolSizes_ = {};
/**
* @type {Array.<number>}
* @private
*/
this.maxSymbolSize_ = [0, 0];
/**
* @private
* @type {function()}
@@ -161,6 +173,8 @@ ol.renderer.canvas.VectorLayer.prototype.expireTiles_ = function(opt_extent) {
// TODO: implement this
}
this.tileCache_.clear();
this.symbolSizes_ = {};
this.maxSymbolSize_ = [0, 0];
};
@@ -198,40 +212,50 @@ ol.renderer.canvas.VectorLayer.prototype.getTransform = function() {
*/
ol.renderer.canvas.VectorLayer.prototype.getFeatureInfoForPixel =
function(pixel, success) {
// TODO adjust pixel tolerance for applied styles
var minPixel = new ol.Pixel(pixel.x - 1, pixel.y - 1);
var maxPixel = new ol.Pixel(pixel.x + 1, pixel.y + 1);
var map = this.getMap();
var locationMin = map.getCoordinateFromPixel(minPixel);
var locationMax = map.getCoordinateFromPixel(maxPixel);
var hw = this.maxSymbolSize_[0] / 2;
var hh = this.maxSymbolSize_[1] / 2;
var location = map.getCoordinateFromPixel(pixel);
var locationMin = [location[0] - hw, location[1] - hh];
var locationMax = [location[0] + hw, location[1] + hh];
var locationBbox = ol.extent.boundingExtent([locationMin, locationMax]);
var filter = new ol.filter.Extent(locationBbox);
// TODO do a real intersect against the filtered result for exact matches
var candidates = this.getLayer().getFeatures(filter);
var location = map.getCoordinateFromPixel(pixel);
// TODO adjust tolerance for stroke width or use configurable tolerance
var tolerance = map.getView().getView2D().getResolution() * 3;
var result = [];
var candidate, geom;
var candidate, geom, type, symbolBounds, symbolSize, halfWidth, halfHeight,
coordinates, j;
for (var i = 0, ii = candidates.length; i < ii; ++i) {
candidate = candidates[i];
geom = candidate.getGeometry();
if (goog.isFunction(geom.containsCoordinate)) {
type = geom.getType();
if (type === ol.geom.GeometryType.POINT ||
type === ol.geom.GeometryType.MULTIPOINT) {
// For points, check if the pixel coordinate is inside the candidate's
// symbol
symbolSize = this.symbolSizes_[goog.getUid(candidate)];
halfWidth = symbolSize[0] / 2;
halfHeight = symbolSize[1] / 2;
symbolBounds = ol.extent.boundingExtent(
[[location[0] - halfWidth, location[1] - halfHeight],
[location[0] + halfWidth, location[1] + halfHeight]]);
coordinates = geom.getCoordinates();
if (ol.extent.containsCoordinate(symbolBounds, coordinates)) {
result.push(candidate);
}
} else if (goog.isFunction(geom.containsCoordinate)) {
// For polygons, check if the pixel location is inside the polygon
if (geom.containsCoordinate(location)) {
result.push(candidate);
}
} else if (goog.isFunction(geom.distanceFromCoordinate)) {
// For lines, check if the ditance to the pixel location is within the
// tolerance threshold
if (geom.distanceFromCoordinate(location) < tolerance) {
// For lines, check if the distance to the pixel location is
// within the rendered line width
if (2 * geom.distanceFromCoordinate(location) <=
this.symbolSizes_[goog.getUid(candidate)][0]) {
result.push(candidate);
}
} else {
// For points, the bbox filter is all we need
result.push(candidate);
}
}
goog.global.setTimeout(function() { success(result); }, 0);
@@ -397,6 +421,7 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame =
}
}
var renderedNew = false;
renderByGeometryType:
for (type in featuresToRender) {
groups = layer.groupFeaturesBySymbolizerLiteral(featuresToRender[type]);
@@ -409,9 +434,16 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame =
if (deferred) {
break renderByGeometryType;
}
renderedNew = true;
}
}
if (renderedNew) {
goog.object.extend(this.symbolSizes_,
sketchCanvasRenderer.getSymbolSizes());
this.maxSymbolSize_ = sketchCanvasRenderer.getMaxSymbolSize();
}
if (!deferred) {
goog.object.extend(tilesToRender, tilesOnSketchCanvas);
}

View File

@@ -79,6 +79,34 @@ ol.renderer.canvas.VectorRenderer =
*/
this.iconLoadedCallback_ = opt_iconLoadedCallback;
/**
* @type {Object.<number, Array.<number>>}
* @private
*/
this.symbolSizes_ = {};
/**
* @type {Array.<number>}
* @private
*/
this.maxSymbolSize_ = [0, 0];
};
/**
* @return {Object.<string, Array.<number>>} Symbolizer sizes.
*/
ol.renderer.canvas.VectorRenderer.prototype.getSymbolSizes = function() {
return this.symbolSizes_;
};
/**
* @return {Array.<number>} Maximum symbolizer size.
*/
ol.renderer.canvas.VectorRenderer.prototype.getMaxSymbolSize = function() {
return this.maxSymbolSize_;
};
@@ -129,7 +157,8 @@ ol.renderer.canvas.VectorRenderer.prototype.renderLineStringFeatures_ =
function(features, symbolizer) {
var context = this.context_,
i, ii, geometry, components, j, jj, line, dim, k, kk, x, y;
i, ii, feature, id, currentSize, geometry, components, j, jj, line, dim,
k, kk, x, y;
context.globalAlpha = symbolizer.opacity;
context.strokeStyle = symbolizer.strokeColor;
@@ -138,7 +167,15 @@ ol.renderer.canvas.VectorRenderer.prototype.renderLineStringFeatures_ =
context.lineJoin = 'round'; // TODO: accept this as a symbolizer property
context.beginPath();
for (i = 0, ii = features.length; i < ii; ++i) {
geometry = features[i].getGeometry();
feature = features[i];
id = goog.getUid(feature);
currentSize = goog.isDef(this.symbolSizes_[id]) ?
this.symbolSizes_[id] : [0];
currentSize[0] = Math.max(currentSize[0], context.lineWidth);
this.symbolSizes_[id] = currentSize;
this.maxSymbolSize_ = [Math.max(currentSize[0], this.maxSymbolSize_[0]),
Math.max(currentSize[0], this.maxSymbolSize_[1])];
geometry = feature.getGeometry();
if (geometry instanceof ol.geom.LineString) {
components = [geometry];
} else {
@@ -175,7 +212,8 @@ ol.renderer.canvas.VectorRenderer.prototype.renderPointFeatures_ =
function(features, symbolizer) {
var context = this.context_,
content, alpha, i, ii, geometry, components, j, jj, point, vec;
content, alpha, i, ii, feature, id, size, geometry, components, j, jj,
point, vec;
if (symbolizer instanceof ol.style.ShapeLiteral) {
content = ol.renderer.canvas.VectorRenderer.renderShape(symbolizer);
@@ -198,7 +236,18 @@ ol.renderer.canvas.VectorRenderer.prototype.renderPointFeatures_ =
context.setTransform(1, 0, 0, 1, -midWidth, -midHeight);
context.globalAlpha = alpha;
for (i = 0, ii = features.length; i < ii; ++i) {
geometry = features[i].getGeometry();
feature = features[i];
id = goog.getUid(feature);
size = this.symbolSizes_[id];
this.symbolSizes_[id] = goog.isDef(size) ?
[Math.max(size[0], content.width * this.inverseScale_),
Math.max(size[1], content.height * this.inverseScale_)] :
[content.width * this.inverseScale_,
content.height * this.inverseScale_];
this.maxSymbolSize_ =
[Math.max(this.maxSymbolSize_[0], this.symbolSizes_[id][0]),
Math.max(this.maxSymbolSize_[1], this.symbolSizes_[id][1])];
geometry = feature.getGeometry();
if (geometry instanceof ol.geom.Point) {
components = [geometry];
} else {