From d938583b980132adcc0298791761bcd6fc6e2fcf Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Thu, 4 Feb 2016 17:09:17 -0800 Subject: [PATCH] style-property-aware hit testing, fix #316 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 --- js/data/feature_tree.js | 150 ++++++++++++++++++++++-------- js/source/source.js | 12 ++- js/source/worker.js | 40 +++++++- js/style/style.js | 12 +-- js/ui/map.js | 4 +- test/js/data/feature_tree.test.js | 126 +++++++++++++------------ test/js/style/style.test.js | 20 ++-- test/js/ui/map.test.js | 11 ++- 8 files changed, 251 insertions(+), 124 deletions(-) diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index 70f8631e325..847b72857f2 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -40,20 +40,48 @@ 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 @@ -61,34 +89,18 @@ FeatureTree.prototype.query = function(args, callback) { 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; } @@ -96,18 +108,86 @@ FeatureTree.prototype.addFeatures = function(matching, params, checkIntersection 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 @@ -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); diff --git a/js/source/source.js b/js/source/source.js index 47ece602958..731da76656f 100644 --- a/js/source/source.js +++ b/js/source/source.js @@ -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, []); @@ -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, []); @@ -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) { diff --git a/js/source/worker.js b/js/source/worker.js index 452365ea346..15af224c1aa 100644 --- a/js/source/worker.js +++ b/js/source/worker.js @@ -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'); @@ -29,6 +30,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) { @@ -167,7 +187,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, []); } diff --git a/js/style/style.js b/js/style/style.js index 3c419af38c1..d8e6a40a1f4 100644 --- a/js/style/style.js +++ b/js/style/style.js @@ -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; @@ -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(); diff --git a/js/ui/map.js b/js/ui/map.js index 19a738ea2f7..c0b1403929e 100644 --- a/js/ui/map.js +++ b/js/ui/map.js @@ -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; }, @@ -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; }, diff --git a/test/js/data/feature_tree.test.js b/test/js/data/feature_tree.test.js index da564d99bf5..76b3da10d5f 100644 --- a/test/js/data/feature_tree.test.js +++ b/test/js/data/feature_tree.test.js @@ -8,6 +8,23 @@ var FeatureTree = require('../../../js/data/feature_tree'); var path = require('path'); var CollisionTile = require('../../../js/symbol/collision_tile'); +var styleLayers = { + water: { + type: 'fill', + paint: { + 'fill-translate': [0, 0] + } + }, + road: { + type: 'line', + paint: { + 'line-offset': 0, + 'line-translate': [0, 0], + 'line-width': 0 + } + } +}; + test('featuretree', function(t) { var tile = new vt.VectorTile(new Protobuf(new Uint8Array(fs.readFileSync(path.join(__dirname, '/../../fixtures/mbsv5-6-18-23.vector.pbf'))))); function getType(feature) { @@ -20,20 +37,15 @@ test('featuretree', function(t) { var feature = tile.layers.road.feature(0); t.ok(feature); t.ok(ft, 'can be created'); - ft.insert(feature.bbox(), 'road', feature); - ft.query({ + ft.insert(feature.bbox(), ['road'], feature); + t.deepEqual(ft.query({ scale: 1, tileSize: 512, params: { }, - scale: 1, - tileSize: 512, x: 0, y: 0 - }, function(err, features) { - t.deepEqual(features, []); - t.equal(err, null); - t.end(); - }); + }, styleLayers), []); + t.end(); }); test('featuretree with args', function(t) { @@ -48,8 +60,8 @@ test('featuretree with args', function(t) { var feature = tile.layers.road.feature(0); t.ok(feature); t.ok(ft, 'can be created'); - ft.insert(feature.bbox(), 'road', feature); - ft.query({ + ft.insert(feature.bbox(), ['road'], feature); + t.deepEqual(ft.query({ params: { radius: 5 }, @@ -57,11 +69,8 @@ test('featuretree with args', function(t) { tileSize: 512, x: 0, y: 0 - }, function(err, features) { - t.deepEqual(features, []); - t.equal(err, null); - t.end(); - }); + }, styleLayers), []); + t.end(); }); test('featuretree point query', function(t) { @@ -72,8 +81,7 @@ test('featuretree point query', function(t) { var feature = tile.layers.water.feature(i); ft.insert(feature.bbox(), ['water'], feature); } - - ft.query({ + var features = ft.query({ source: "mapbox.mapbox-streets-v5", scale: 1.4142135624, tileSize: 512, @@ -83,18 +91,16 @@ test('featuretree point query', function(t) { }, x: 1842, y: 2014 - }, function(err, features) { - t.notEqual(features.length, 0, 'non-empty results for queryFeatures'); - features.forEach(function(f) { - t.equal(f.type, 'Feature'); - t.equal(f.geometry.type, 'Polygon'); - t.equal(f.layer, 'water'); - t.ok(f.properties, 'result has properties'); - t.notEqual(f.properties.osm_id, undefined, 'properties has osm_id by default'); - }); - t.equal(err, null); - t.end(); + }, styleLayers); + t.notEqual(features.length, 0, 'non-empty results for queryFeatures'); + features.forEach(function(f) { + t.equal(f.type, 'Feature'); + t.equal(f.geometry.type, 'Polygon'); + t.equal(f.layer, 'water'); + t.ok(f.properties, 'result has properties'); + t.notEqual(f.properties.osm_id, undefined, 'properties has osm_id by default'); }); + t.end(); }); test('featuretree rect query', function(t) { @@ -106,7 +112,7 @@ test('featuretree rect query', function(t) { ft.insert(feature.bbox(), ['water'], feature); } - ft.query({ + var features = ft.query({ source: "mapbox.mapbox-streets-v5", scale: 1.4142135624, tileSize: 512, @@ -117,28 +123,26 @@ test('featuretree rect query', function(t) { minY: 3072, maxX: 2048, maxY: 4096 - }, function(err, features) { - t.notEqual(features.length, 0, 'non-empty results for queryFeatures'); - features.forEach(function(f) { - t.equal(f.type, 'Feature'); - t.equal(f.geometry.type, 'Polygon'); - t.equal(f.layer, 'water'); - t.ok(f.properties, 'result has properties'); - t.notEqual(f.properties.osm_id, undefined, 'properties has osm_id by default'); - var points = Array.prototype.concat.apply([], f.geometry.coordinates); - var isInBox = points.reduce(function (isInBox, point) { - return isInBox || ( - point[0] >= -78.9 && - point[0] <= -72.6 && - point[1] >= 40.7 && - point[1] <= 43.2 - ); - }, false); - t.ok(isInBox, 'feature has at least one point in queried box'); - }); - t.equal(err, null); - t.end(); + }, styleLayers); + t.notEqual(features.length, 0, 'non-empty results for queryFeatures'); + features.forEach(function(f) { + t.equal(f.type, 'Feature'); + t.equal(f.geometry.type, 'Polygon'); + t.equal(f.layer, 'water'); + t.ok(f.properties, 'result has properties'); + t.notEqual(f.properties.osm_id, undefined, 'properties has osm_id by default'); + var points = Array.prototype.concat.apply([], f.geometry.coordinates); + var isInBox = points.reduce(function (isInBox, point) { + return isInBox || ( + point[0] >= -78.9 && + point[0] <= -72.6 && + point[1] >= 40.7 && + point[1] <= 43.2 + ); + }, false); + t.ok(isInBox, 'feature has at least one point in queried box'); }); + t.end(); }); test('featuretree query with layerIds', function(t) { @@ -156,7 +160,7 @@ test('featuretree query with layerIds', function(t) { ft.insert(feature.bbox(), ['water'], feature); } - ft.query({ + var features = ft.query({ source: "mapbox.mapbox-streets-v5", scale: 1.4142135624, tileSize: 512, @@ -166,12 +170,11 @@ test('featuretree query with layerIds', function(t) { }, x: 1842, y: 2014 - }, function(err, features) { - t.ifError(err); - t.equal(features.length, 2); - }); + }, styleLayers); + + t.equal(features.length, 2); - ft.query({ + var features2 = ft.query({ source: "mapbox.mapbox-streets-v5", scale: 1.4142135624, tileSize: 512, @@ -181,9 +184,8 @@ test('featuretree query with layerIds', function(t) { }, x: 1842, y: 2014 - }, function(err, features) { - t.ifError(err); - t.equal(features.length, 0); - t.end(); - }); + }, styleLayers); + + t.equal(features2.length, 0); + t.end(); }); diff --git a/test/js/style/style.test.js b/test/js/style/style.test.js index 5f0583f7ac6..32af5fa7310 100644 --- a/test/js/style/style.test.js +++ b/test/js/style/style.test.js @@ -883,7 +883,7 @@ test('Style#featuresAt - race condition', function(t) { style._cascade([]); style._recalculate(0); - style.sources.mapbox.featuresAt = function(position, params, callback) { + style.sources.mapbox.featuresAt = function(position, params, classes, zoom, bearing, callback) { var features = [{ type: 'Feature', layer: 'land', @@ -896,7 +896,7 @@ test('Style#featuresAt - race condition', function(t) { }; t.test('featuresAt race condition', function(t) { - style.featuresAt([256, 256], {}, function(err, results) { + style.featuresAt([256, 256], {}, {}, 0, 0, function(err, results) { t.error(err); t.equal(results.length, 0); t.end(); @@ -942,7 +942,7 @@ test('Style#featuresAt', function(t) { style._cascade([]); style._recalculate(0); - style.sources.mapbox.featuresAt = style.sources.mapbox.featuresIn = function(position, params, callback) { + style.sources.mapbox.featuresAt = style.sources.mapbox.featuresIn = function(position, params, classes, zoom, bearing, callback) { var features = [{ type: 'Feature', layer: 'land', @@ -979,7 +979,7 @@ test('Style#featuresAt', function(t) { style.featuresIn.bind(style, [256, 256, 512, 512]) ].forEach(function (featuresInOrAt) { t.test('returns feature type', function(t) { - featuresInOrAt({}, function(err, results) { + featuresInOrAt({}, {}, 0, 0, function(err, results) { t.error(err); t.equal(results[0].geometry.type, 'Polygon'); t.end(); @@ -987,7 +987,7 @@ test('Style#featuresAt', function(t) { }); t.test('filters by `layer` option', function(t) { - featuresInOrAt({layer: 'land'}, function(err, results) { + featuresInOrAt({layer: 'land'}, {}, 0, 0, function(err, results) { t.error(err); t.equal(results.length, 2); t.end(); @@ -995,7 +995,7 @@ test('Style#featuresAt', function(t) { }); t.test('includes layout properties', function(t) { - featuresInOrAt({}, function(err, results) { + featuresInOrAt({}, {}, 0, 0, function(err, results) { t.error(err); var layout = results[0].layer.layout; t.deepEqual(layout['line-cap'], 'round'); @@ -1004,7 +1004,7 @@ test('Style#featuresAt', function(t) { }); t.test('includes paint properties', function(t) { - featuresInOrAt({}, function(err, results) { + featuresInOrAt({}, {}, 0, 0, function(err, results) { t.error(err); t.deepEqual(results[0].layer.paint['line-color'], 'red'); t.end(); @@ -1012,7 +1012,7 @@ test('Style#featuresAt', function(t) { }); t.test('ref layer inherits properties', function(t) { - featuresInOrAt({}, function(err, results) { + featuresInOrAt({}, {}, 0, 0, function(err, results) { t.error(err); var layer = results[1].layer; @@ -1027,7 +1027,7 @@ test('Style#featuresAt', function(t) { }); t.test('includes metadata', function(t) { - featuresInOrAt({}, function(err, results) { + featuresInOrAt({}, {}, 0, 0, function(err, results) { t.error(err); var layer = results[0].layer; @@ -1038,7 +1038,7 @@ test('Style#featuresAt', function(t) { }); t.test('include multiple layers', function(t) { - featuresInOrAt({layer: ['land', 'landref']}, function(err, results) { + featuresInOrAt({layer: ['land', 'landref']}, {}, 0, 0, function(err, results) { t.error(err); t.equals(results.length, 3); t.end(); diff --git a/test/js/ui/map.test.js b/test/js/ui/map.test.js index aea20c6c058..9b52011b5a1 100644 --- a/test/js/ui/map.test.js +++ b/test/js/ui/map.test.js @@ -491,11 +491,13 @@ test('Map', function(t) { var opts = {}; t.test('normal coords', function(t) { - map.style.featuresAt = function (coords, o, cb) { + map.style.featuresAt = function (coords, o, classes, zoom, bearing, cb) { t.deepEqual(coords, { column: 0.5, row: 0.5, zoom: 0 }); t.equal(o, opts); t.equal(cb, callback); - + t.deepEqual(classes, map._classes); + t.equal(bearing, map.transform.angle); + t.equal(zoom, map.getZoom()); t.end(); }; @@ -503,13 +505,16 @@ test('Map', function(t) { }); t.test('wraps coords', function(t) { - map.style.featuresAt = function (coords, o, cb) { + map.style.featuresAt = function (coords, o, classes, zoom, bearing, cb) { // avoid floating point issues t.equal(parseFloat(coords.column.toFixed(4)), 0.5); t.equal(coords.row, 0.5); t.equal(coords.zoom, 0); t.equal(o, opts); + t.deepEqual(classes, map._classes); + t.equal(zoom, map.transform.angle); + t.equal(zoom, map.getZoom()); t.equal(cb, callback); t.end();