Skip to content

Commit

Permalink
style-property-aware hit testing, fix #316
Browse files Browse the repository at this point in the history
map.featuresAt now includes features whose rendered representation
matches the query, not features whose geometry matches the query.

A query with `radius: 0` will now match lines and circles if the point
is within the rendered line.

also fix #2053
It now checks intersection based on the render type not the geometry
type. A polygon that is rendered as a line will only match if the query
matches the line. It will not include it if the query only matches in
the internal part of the polygon.

implemented:
circle-radius
circle-translate
fill-translate
line-width
line-offset
line-translate

not implemented yet (hard with the current symbol index):
text-translate
icon-translate
  • Loading branch information
ansis committed Mar 17, 2016
1 parent 726c093 commit e7f028e
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 124 deletions.
150 changes: 112 additions & 38 deletions js/data/feature_tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,74 +40,154 @@ FeatureTree.prototype.setCollisionTile = function(collisionTile) {
this.collisionTile = collisionTile;
};

function translateDistance(translate) {
return Math.sqrt(translate[0] * translate[0] + translate[1] * translate[1]);
}

// Finds features in this tile at a particular position.
FeatureTree.prototype.query = function(args, callback) {
FeatureTree.prototype.query = function(args, styleLayersByID) {
if (this.toBeInserted.length) this._load();

var params = args.params || {},
x = args.x,
y = args.y,
p = new Point(x, y),
pixelsToTileUnits = EXTENT / args.tileSize / args.scale,
result = [];

// Features are indexed their original geometries. The rendered geometries may
// be buffered, translated or offset. Figure out how much the search radius needs to be
// expanded by to include these features.
var additionalRadius = 0;
var styleLayer;
for (var id in styleLayersByID) {
styleLayer = styleLayersByID[id];

var styleLayerDistance = 0;
if (styleLayer.type === 'line') {
styleLayerDistance = styleLayer.paint['line-width'] / 2 + Math.abs(styleLayer.paint['line-offset']) + translateDistance(styleLayer.paint['line-translate']);
} else if (styleLayer.type === 'fill') {
styleLayerDistance = translateDistance(styleLayer.paint['fill-translate']);
} else if (styleLayer.type === 'circle') {
styleLayerDistance = styleLayer.paint['circle-radius'] + translateDistance(styleLayer.paint['circle-translate']);
}
additionalRadius = Math.max(additionalRadius, styleLayerDistance * pixelsToTileUnits);
}

var radiusSearch = typeof x !== 'undefined' && typeof y !== 'undefined';

var radius, bounds, symbolQueryBox;
if (typeof x !== 'undefined' && typeof y !== 'undefined') {
if (radiusSearch) {
// a point (or point+radius) query
radius = (params.radius || 0) * EXTENT / args.tileSize / args.scale;
bounds = [x - radius, y - radius, x + radius, y + radius];
radius = (params.radius || 0) * pixelsToTileUnits;
var searchRadius = radius + additionalRadius;
bounds = [x - searchRadius, y - searchRadius, x + searchRadius, y + searchRadius];
symbolQueryBox = new CollisionBox(new Point(x, y), -radius, -radius, radius, radius, args.scale, null);
} else {
// a rectangle query
bounds = [ args.minX, args.minY, args.maxX, args.maxY ];
symbolQueryBox = new CollisionBox(new Point(args.minX, args.minY), 0, 0, args.maxX - args.minX, args.maxY - args.minY, args.scale, null);
}

function checkIntersection(feature) {
var type = vt.VectorTileFeature.types[feature.type];
if (params.$type && type !== params.$type)
return false;
var matching = this.rtree.search(bounds).concat(this.collisionTile.getFeaturesAt(symbolQueryBox, args.scale));

return radius ?
geometryContainsPoint(loadGeometry(feature), type, new Point(x, y), radius) :
geometryIntersectsBox(loadGeometry(feature), type, bounds);
}
for (var k = 0; k < matching.length; k++) {
var feature = matching[k].feature,
layerIDs = matching[k].layerIDs,
type = vt.VectorTileFeature.types[feature.type];

function checkSymbolIntersection() {
return true;
}

this.addFeatures(this.rtree.search(bounds), params, checkIntersection, result);
this.addFeatures(this.collisionTile.getFeaturesAt(symbolQueryBox, args.scale), params, checkSymbolIntersection, result);

callback(null, result);
};
if (params.$type && type !== params.$type)
continue;

FeatureTree.prototype.addFeatures = function(matching, params, checkIntersection, result) {
for (var i = 0; i < matching.length; i++) {
var feature = matching[i].feature,
layerIDs = matching[i].layerIDs;
var geoJSON = feature.toGeoJSON(this.x, this.y, this.z);

if (!checkIntersection(feature)) continue;

if (!params.includeGeometry) {
geoJSON.geometry = null;
}

for (var l = 0; l < layerIDs.length; l++) {
var layerID = layerIDs[l];

if (params.layerIds && params.layerIds.indexOf(layerID) < 0)
if (params.layerIds && params.layerIds.indexOf(layerID) < 0) {
continue;
}

styleLayer = styleLayersByID[layerID];
var geometry = loadGeometry(feature);

var translatedPoint;
if (styleLayer.type === 'symbol') {
// all symbols already match the style

} else if (styleLayer.type === 'line') {
translatedPoint = translate(styleLayer.paint['line-translate'], styleLayer.paint['line-translate-anchor']);
var halfWidth = styleLayer.paint['line-width'] / 2 * pixelsToTileUnits;
if (styleLayer.paint['line-offset']) {
geometry = offsetLine(geometry, styleLayer.paint['line-offset'] * pixelsToTileUnits);
}
if (radiusSearch ?
!lineContainsPoint(geometry, translatedPoint, radius + halfWidth) :
!lineIntersectsBox(geometry, bounds)) {
continue;
}

} else if (styleLayer.type === 'fill') {
translatedPoint = translate(styleLayer.paint['fill-translate'], styleLayer.paint['fill-translate-anchor']);
if (radiusSearch ?
!(polyContainsPoint(geometry, translatedPoint) || lineContainsPoint(geometry, translatedPoint, radius)) :
!polyIntersectsBox(geometry, bounds)) {
continue;
}

} else if (styleLayer.type === 'circle') {
translatedPoint = translate(styleLayer.paint['circle-translate'], styleLayer.paint['circle-translate-anchor']);
var circleRadius = styleLayer.paint['circle-radius'] * pixelsToTileUnits;
if (radiusSearch ?
!pointContainsPoint(geometry, translatedPoint, radius + circleRadius) :
!pointIntersectsBox(geometry, bounds)) {
continue;
}
}

result.push(util.extend({layer: layerID}, geoJSON));
}
}

function translate(translate, translateAnchor) {
translate = Point.convert(translate);

if (translateAnchor === "viewport") {
translate._rotate(-args.bearing);
}

return p.sub(translate._mult(pixelsToTileUnits));
}

return result;
};

function geometryIntersectsBox(rings, type, bounds) {
return type === 'Point' ? pointIntersectsBox(rings, bounds) :
type === 'LineString' ? lineIntersectsBox(rings, bounds) :
type === 'Polygon' ? polyIntersectsBox(rings, bounds) || lineIntersectsBox(rings, bounds) : false;
function offsetLine(rings, offset) {
var newRings = [];
var zero = new Point(0, 0);
for (var k = 0; k < rings.length; k++) {
var ring = rings[k];
var newRing = [];
for (var i = 0; i < ring.length; i++) {
var a = ring[i - 1];
var b = ring[i];
var c = ring[i + 1];
var aToB = i === 0 ? zero : b.sub(a)._unit()._perp();
var bToC = i === ring.length - 1 ? zero : c.sub(b)._unit()._perp();
var extrude = aToB._add(bToC)._unit();

var cosHalfAngle = extrude.x * bToC.x + extrude.y * bToC.y;
extrude._mult(1 / cosHalfAngle);

newRing.push(extrude._mult(offset)._add(b));
}
newRings.push(newRing);
}
return newRings;
}

// Tests whether any of the four corners of the bbox are contained in the
Expand Down Expand Up @@ -175,12 +255,6 @@ function pointIntersectsBox(rings, bounds) {
return false;
}

function geometryContainsPoint(rings, type, p, radius) {
return type === 'Point' ? pointContainsPoint(rings, p, radius) :
type === 'LineString' ? lineContainsPoint(rings, p, radius) :
type === 'Polygon' ? polyContainsPoint(rings, p) || lineContainsPoint(rings, p, radius) : false;
}

// Code from http://stackoverflow.com/a/1501725/331379.
function distToSegmentSquared(p, v, w) {
var l2 = v.distSqr(w);
Expand Down
12 changes: 10 additions & 2 deletions js/source/source.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ exports._getVisibleCoordinates = function() {
else return this._pyramid.renderedIDs().map(TileCoord.fromID);
};

exports._vectorFeaturesAt = function(coord, params, callback) {
exports._vectorFeaturesAt = function(coord, params, classes, zoom, bearing, callback) {
if (!this._pyramid)
return callback(null, []);

Expand All @@ -81,13 +81,16 @@ exports._vectorFeaturesAt = function(coord, params, callback) {
y: result.y,
scale: result.scale,
tileSize: result.tileSize,
classes: classes,
zoom: zoom,
bearing: bearing,
source: this.id,
params: params
}, callback, result.tile.workerID);
};


exports._vectorFeaturesIn = function(bounds, params, callback) {
exports._vectorFeaturesIn = function(bounds, params, classes, zoom, bearing, callback) {
if (!this._pyramid)
return callback(null, []);

Expand All @@ -103,6 +106,11 @@ exports._vectorFeaturesIn = function(bounds, params, callback) {
maxX: result.maxX,
minY: result.minY,
maxY: result.maxY,
scale: result.scale,
tileSize: result.tileSize,
classes: classes,
zoom: zoom,
bearing: bearing,
params: params
}, cb, result.tile.workerID);
}.bind(this), function done(err, features) {
Expand Down
40 changes: 39 additions & 1 deletion js/source/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var Actor = require('../util/actor');
var WorkerTile = require('./worker_tile');
var util = require('../util/util');
var ajax = require('../util/ajax');
var StyleLayer = require('../style/style_layer');
var vt = require('vector-tile');
var Protobuf = require('pbf');
var supercluster = require('supercluster');
Expand All @@ -28,6 +29,25 @@ function Worker(self) {
util.extend(Worker.prototype, {
'set layers': function(layers) {
this.layers = layers;
this.styleLayersByID = {};

var layer;
this._recalculatedZoom = null;
this._cascadedClasses = null;

for (var i = 0; i < layers.length; i++) {
layer = layers[i];
if (!layer.ref) {
this.styleLayersByID[layer.id] = StyleLayer.create(layer);
}
}

for (var k = 0; k < layers.length; k++) {
layer = layers[k];
if (layer.ref) {
this.styleLayersByID[layer.id] = StyleLayer.create(layer, this.styleLayersByID[layer.ref]);
}
}
},

'update layers': function(layers) {
Expand Down Expand Up @@ -165,7 +185,25 @@ util.extend(Worker.prototype, {
'query features': function(params, callback) {
var tile = this.loaded[params.source] && this.loaded[params.source][params.uid];
if (tile) {
tile.featureTree.query(params, callback);

var id;

var classString = Object.keys(params.classes).join(' ');
if (this._cascadedClasses !== classString) {
this._cascadedClasses = classString;
for (id in this.styleLayersByID) {
this.styleLayersByID[id].cascade(params.classes, {transition: false}, {});
}
}

if (this._recalculatedZoom !== params.zoom) {
this._recalculatedZoom = params.zoom;
for (id in this.styleLayersByID) {
this.styleLayersByID[id].recalculate(params.zoom, { lastIntegerZoom: Infinity, lastIntegerZoomTime: 0, lastZoom: 0 });
}
}

callback(null, tile.featureTree.query(params, this.styleLayersByID));
} else {
callback(null, []);
}
Expand Down
12 changes: 6 additions & 6 deletions js/style/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -410,15 +410,15 @@ Style.prototype = util.inherit(Evented, {
}, function(value) { return value !== undefined; });
},

featuresAt: function(coord, params, callback) {
this._queryFeatures('featuresAt', coord, params, callback);
featuresAt: function(coord, params, classes, zoom, bearing, callback) {
this._queryFeatures('featuresAt', coord, params, classes, zoom, bearing, callback);
},

featuresIn: function(bbox, params, callback) {
this._queryFeatures('featuresIn', bbox, params, callback);
featuresIn: function(bbox, params, classes, zoom, bearing, callback) {
this._queryFeatures('featuresIn', bbox, params, classes, zoom, bearing, callback);
},

_queryFeatures: function(queryType, bboxOrCoords, params, callback) {
_queryFeatures: function(queryType, bboxOrCoords, params, classes, zoom, bearing, callback) {
var features = [];
var error = null;

Expand All @@ -428,7 +428,7 @@ Style.prototype = util.inherit(Evented, {

util.asyncAll(Object.keys(this.sources), function(id, callback) {
var source = this.sources[id];
source[queryType](bboxOrCoords, params, function(err, result) {
source[queryType](bboxOrCoords, params, classes, zoom, bearing, function(err, result) {
if (result) features = features.concat(result);
if (err) error = err;
callback();
Expand Down
4 changes: 2 additions & 2 deletions js/ui/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ util.extend(Map.prototype, /** @lends Map.prototype */{
featuresAt: function(point, params, callback) {
var location = this.unproject(point).wrap();
var coord = this.transform.locationCoordinate(location);
this.style.featuresAt(coord, params, callback);
this.style.featuresAt(coord, params, this._classes, this.transform.zoom, this.transform.angle, callback);
return this;
},

Expand Down Expand Up @@ -437,7 +437,7 @@ util.extend(Map.prototype, /** @lends Map.prototype */{
Math.max(bounds[0].y, bounds[1].y)
)
].map(this.transform.pointCoordinate.bind(this.transform));
this.style.featuresIn(bounds, params, callback);
this.style.featuresIn(bounds, params, this._classes, this.transform.zoom, this.transform.angle, callback);
return this;
},

Expand Down
Loading

0 comments on commit e7f028e

Please sign in to comment.