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:
@@ -43,6 +43,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="tags">KML</div>
|
<div id="tags">KML</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="span4 pull-right">
|
||||||
|
<div id="info" class="alert alert-success">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,20 @@ var map = new ol.Map({
|
|||||||
var kml = new ol.parser.KML({
|
var kml = new ol.parser.KML({
|
||||||
maxDepth: 1, dimension: 2, extractStyles: true, extractAttributes: true});
|
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(', ') || ' ';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
var url = 'data/kml/lines.kml';
|
var url = 'data/kml/lines.kml';
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.open('GET', url, true);
|
xhr.open('GET', url, true);
|
||||||
|
|||||||
@@ -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.
|
* of the extent.
|
||||||
*
|
*
|
||||||
* @param {ol.Extent} extent Extent.
|
* @param {ol.Extent} extent Extent.
|
||||||
* @param {ol.Coordinate} coordinate Coordinate.
|
* @param {ol.Coordinate|Array.<ol.Coordinate>} coordinates Coordinate(s).
|
||||||
* @return {boolean} Contains.
|
* @return {boolean} Contains.
|
||||||
*/
|
*/
|
||||||
ol.extent.containsCoordinate = function(extent, coordinate) {
|
ol.extent.containsCoordinate = function(extent, coordinates) {
|
||||||
return extent[0] <= coordinate[0] && coordinate[0] <= extent[1] &&
|
coordinates = goog.isArray(coordinates[0]) ? coordinates : [coordinates];
|
||||||
extent[2] <= coordinate[1] && coordinate[1] <= extent[3];
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -137,6 +137,18 @@ ol.renderer.canvas.VectorLayer = function(mapRenderer, layer) {
|
|||||||
*/
|
*/
|
||||||
this.tileGrid_ = null;
|
this.tileGrid_ = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Object.<number, Array.<number>>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.symbolSizes_ = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array.<number>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.maxSymbolSize_ = [0, 0];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @type {function()}
|
* @type {function()}
|
||||||
@@ -161,6 +173,8 @@ ol.renderer.canvas.VectorLayer.prototype.expireTiles_ = function(opt_extent) {
|
|||||||
// TODO: implement this
|
// TODO: implement this
|
||||||
}
|
}
|
||||||
this.tileCache_.clear();
|
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 =
|
ol.renderer.canvas.VectorLayer.prototype.getFeatureInfoForPixel =
|
||||||
function(pixel, success) {
|
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 map = this.getMap();
|
||||||
|
|
||||||
var locationMin = map.getCoordinateFromPixel(minPixel);
|
var hw = this.maxSymbolSize_[0] / 2;
|
||||||
var locationMax = map.getCoordinateFromPixel(maxPixel);
|
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 locationBbox = ol.extent.boundingExtent([locationMin, locationMax]);
|
||||||
var filter = new ol.filter.Extent(locationBbox);
|
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 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 result = [];
|
||||||
var candidate, geom;
|
var candidate, geom, type, symbolBounds, symbolSize, halfWidth, halfHeight,
|
||||||
|
coordinates, j;
|
||||||
for (var i = 0, ii = candidates.length; i < ii; ++i) {
|
for (var i = 0, ii = candidates.length; i < ii; ++i) {
|
||||||
candidate = candidates[i];
|
candidate = candidates[i];
|
||||||
geom = candidate.getGeometry();
|
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
|
// For polygons, check if the pixel location is inside the polygon
|
||||||
if (geom.containsCoordinate(location)) {
|
if (geom.containsCoordinate(location)) {
|
||||||
result.push(candidate);
|
result.push(candidate);
|
||||||
}
|
}
|
||||||
} else if (goog.isFunction(geom.distanceFromCoordinate)) {
|
} else if (goog.isFunction(geom.distanceFromCoordinate)) {
|
||||||
// For lines, check if the ditance to the pixel location is within the
|
// For lines, check if the distance to the pixel location is
|
||||||
// tolerance threshold
|
// within the rendered line width
|
||||||
if (geom.distanceFromCoordinate(location) < tolerance) {
|
if (2 * geom.distanceFromCoordinate(location) <=
|
||||||
|
this.symbolSizes_[goog.getUid(candidate)][0]) {
|
||||||
result.push(candidate);
|
result.push(candidate);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// For points, the bbox filter is all we need
|
|
||||||
result.push(candidate);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
goog.global.setTimeout(function() { success(result); }, 0);
|
goog.global.setTimeout(function() { success(result); }, 0);
|
||||||
@@ -397,6 +421,7 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var renderedNew = false;
|
||||||
renderByGeometryType:
|
renderByGeometryType:
|
||||||
for (type in featuresToRender) {
|
for (type in featuresToRender) {
|
||||||
groups = layer.groupFeaturesBySymbolizerLiteral(featuresToRender[type]);
|
groups = layer.groupFeaturesBySymbolizerLiteral(featuresToRender[type]);
|
||||||
@@ -409,9 +434,16 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame =
|
|||||||
if (deferred) {
|
if (deferred) {
|
||||||
break renderByGeometryType;
|
break renderByGeometryType;
|
||||||
}
|
}
|
||||||
|
renderedNew = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (renderedNew) {
|
||||||
|
goog.object.extend(this.symbolSizes_,
|
||||||
|
sketchCanvasRenderer.getSymbolSizes());
|
||||||
|
this.maxSymbolSize_ = sketchCanvasRenderer.getMaxSymbolSize();
|
||||||
|
}
|
||||||
|
|
||||||
if (!deferred) {
|
if (!deferred) {
|
||||||
goog.object.extend(tilesToRender, tilesOnSketchCanvas);
|
goog.object.extend(tilesToRender, tilesOnSketchCanvas);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,34 @@ ol.renderer.canvas.VectorRenderer =
|
|||||||
*/
|
*/
|
||||||
this.iconLoadedCallback_ = opt_iconLoadedCallback;
|
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) {
|
function(features, symbolizer) {
|
||||||
|
|
||||||
var context = this.context_,
|
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.globalAlpha = symbolizer.opacity;
|
||||||
context.strokeStyle = symbolizer.strokeColor;
|
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.lineJoin = 'round'; // TODO: accept this as a symbolizer property
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
for (i = 0, ii = features.length; i < ii; ++i) {
|
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) {
|
if (geometry instanceof ol.geom.LineString) {
|
||||||
components = [geometry];
|
components = [geometry];
|
||||||
} else {
|
} else {
|
||||||
@@ -175,7 +212,8 @@ ol.renderer.canvas.VectorRenderer.prototype.renderPointFeatures_ =
|
|||||||
function(features, symbolizer) {
|
function(features, symbolizer) {
|
||||||
|
|
||||||
var context = this.context_,
|
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) {
|
if (symbolizer instanceof ol.style.ShapeLiteral) {
|
||||||
content = ol.renderer.canvas.VectorRenderer.renderShape(symbolizer);
|
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.setTransform(1, 0, 0, 1, -midWidth, -midHeight);
|
||||||
context.globalAlpha = alpha;
|
context.globalAlpha = alpha;
|
||||||
for (i = 0, ii = features.length; i < ii; ++i) {
|
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) {
|
if (geometry instanceof ol.geom.Point) {
|
||||||
components = [geometry];
|
components = [geometry];
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user