From e34197e4df67a6acef525ddbccddc95351556b8c Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 24 Feb 2016 12:57:52 -0800 Subject: [PATCH 01/48] fix polygonIntersectsMultiPolygon --- js/data/feature_tree.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index cfd0607bd24..5d6733cda16 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -203,6 +203,15 @@ function polygonIntersectsMultiPolygon(polygon, multiPolygon) { for (var i = 0; i < polygon.length; i++) { if (multiPolygonContainsPoint(multiPolygon, polygon[i])) return true; } + + var polygon_ = [polygon]; + for (var m = 0; m < multiPolygon.length; m++) { + var ring = multiPolygon[m]; + for (var n = 0; n < ring.length; n++) { + if (multiPolygonContainsPoint(polygon_, ring[n])) return true; + } + } + for (var k = 0; k < multiPolygon.length; k++) { if (lineIntersectsLine(polygon, multiPolygon[k])) return true; } From b62895ab809b46a0341b943c29448dd5de788824 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Mon, 29 Feb 2016 16:56:20 -0800 Subject: [PATCH 02/48] index collision boxes with a grid instead of rtree The combined running time of placeCollisionFeature and insertCollisionFeature is now 2/3rds of what it used to be. The grid index divids a square into n x n cells. When a bbox is inserted, it is added to the array of every cell it intersects. When querying, look in all the cells that intersect the query box and then compare all the individual bboxes contained in that cell against the query box. This kind of index is faster in this specific case because it's characteristics better match the work we're using it for. Queries can take 1.3x as long (slower) with the grid index. But insertions often take < 0.1x as long with the grid index. Our collision detection code does a lot of insertions so this is a worthwhile tradeoff. The grid index also takes advantage of the fact that label boxes are fairly evenly sized and evenly distributed. The grid index can also be serialized to an ArrayBuffer. The ArrayBuffer version can be queried without deserializing all the individual objects first. This is not currently used. --- js/symbol/collision_tile.js | 43 +++++++++--- js/util/grid.js | 130 ++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 js/util/grid.js diff --git a/js/symbol/collision_tile.js b/js/symbol/collision_tile.js index 3c5eb1edfcf..7761abd5c0b 100644 --- a/js/symbol/collision_tile.js +++ b/js/symbol/collision_tile.js @@ -1,9 +1,9 @@ 'use strict'; -var rbush = require('rbush'); var CollisionBox = require('./collision_box'); var Point = require('point-geometry'); var EXTENT = require('../data/bucket').EXTENT; +var Grid = require('../util/grid'); module.exports = CollisionTile; @@ -18,8 +18,11 @@ module.exports = CollisionTile; * @private */ function CollisionTile(angle, pitch) { - this.tree = rbush(); - this.ignoredTree = rbush(); + this.grid = new Grid(12, EXTENT, 6); + this.gridFeatures = []; + this.ignoredGrid = new Grid(12, EXTENT, 0); + this.ignoredGridFeatures = []; + this.angle = angle; var sin = Math.sin(angle), @@ -78,10 +81,10 @@ CollisionTile.prototype.placeCollisionFeature = function(collisionFeature, allow box[2] = x + box.x2; box[3] = y + box.y2 * yStretch; - var blockingBoxes = this.tree.search(box); + var blockingBoxes = this.grid.query(box); for (var i = 0; i < blockingBoxes.length; i++) { - var blocking = blockingBoxes[i]; + var blocking = this.gridFeatures[blockingBoxes[i]]; var blockingAnchorPoint = blocking.anchorPoint.matMult(rotationMatrix); minPlacementScale = this.getPlacementScale(minPlacementScale, anchorPoint, box, blockingAnchorPoint, blocking); @@ -131,7 +134,15 @@ CollisionTile.prototype.getFeaturesAt = function(queryBox, scale) { anchorPoint.y + queryBox.y2 / scale * this.yStretch ]; - var blockingBoxes = this.tree.search(searchBox).concat(this.ignoredTree.search(searchBox)); + var blockingBoxes = []; + var blockingBoxKeys = this.grid.query(searchBox); + for (var j = 0; j < blockingBoxKeys.length; j++) { + blockingBoxes.push(this.gridFeatures[blockingBoxKeys[j]]); + } + blockingBoxKeys = this.ignoredGrid.query(searchBox); + for (var k = 0; k < blockingBoxKeys.length; k++) { + blockingBoxes.push(this.ignoredGridFeatures[blockingBoxKeys[k]]); + } for (var i = 0; i < blockingBoxes.length; i++) { var blocking = blockingBoxes[i]; @@ -207,9 +218,25 @@ CollisionTile.prototype.insertCollisionFeature = function(collisionFeature, minP if (minPlacementScale < this.maxScale) { if (ignorePlacement) { - this.ignoredTree.load(boxes); + this.insertIgnoredGrid(boxes); } else { - this.tree.load(boxes); + this.insertGrid(boxes); } } }; + +CollisionTile.prototype.insertGrid = function(boxes) { + for (var i = 0; i < boxes.length; i++) { + var box = boxes[i]; + this.grid.insert(box, this.gridFeatures.length); + this.gridFeatures.push(box); + } +}; + +CollisionTile.prototype.insertIgnoredGrid = function(boxes) { + for (var i = 0; i < boxes.length; i++) { + var box = boxes[i]; + this.ignoredGrid.insert(box, this.ignoredGridFeatures.length); + this.ignoredGridFeatures.push(box); + } +}; diff --git a/js/util/grid.js b/js/util/grid.js new file mode 100644 index 00000000000..cbd1761b6b9 --- /dev/null +++ b/js/util/grid.js @@ -0,0 +1,130 @@ +'use strict'; + +module.exports = Grid; + +var NUM_PARAMS = 3; + +function Grid(n, extent, padding) { + var cells = this.cells = []; + + if (n instanceof ArrayBuffer) { + var array = new Int32Array(n); + n = array[0]; + extent = array[1]; + padding = array[2]; + + this.d = n + 2 * padding; + for (var k = 0; k < this.d * this.d; k++) { + cells.push(array.subarray(array[NUM_PARAMS + k], array[NUM_PARAMS + k + 1])); + } + var keysOffset = array[NUM_PARAMS + cells.length]; + var bboxesOffset = array[NUM_PARAMS + cells.length + 1]; + this.keys = array.subarray(keysOffset, bboxesOffset); + this.bboxes = array.subarray(bboxesOffset); + } else { + this.d = n + 2 * padding; + for (var i = 0; i < this.d * this.d; i++) { + cells.push([]); + } + this.keys = []; + this.bboxes = []; + } + + this.n = n; + this.extent = extent; + this.padding = padding; + this.scale = n / extent; + this.uid = 0; +} + + +Grid.prototype.insert = function(bbox, key) { + this._forEachCell(bbox, this._insertCell, this.uid++); + this.keys.push(key); + this.bboxes.push(bbox[0]); + this.bboxes.push(bbox[1]); + this.bboxes.push(bbox[2]); + this.bboxes.push(bbox[3]); +}; + +Grid.prototype._insertCell = function(bbox, cellIndex, uid) { + this.cells[cellIndex].push(uid); +}; + +Grid.prototype.query = function(bbox) { + var result = []; + var seenUids = {}; + this._forEachCell(bbox, this._queryCell, result, seenUids); + return result; +}; + +Grid.prototype._queryCell = function(bbox, cellIndex, result, seenUids) { + var cell = this.cells[cellIndex]; + var keys = this.keys; + var bboxes = this.bboxes; + for (var u = 0; u < cell.length; u++) { + var uid = cell[u]; + if (seenUids[uid] === undefined) { + var offset = uid * 4; + if ((bbox[0] <= bboxes[offset + 2]) && + (bbox[1] <= bboxes[offset + 3]) && + (bbox[2] >= bboxes[offset + 0]) && + (bbox[3] >= bboxes[offset + 1])) { + seenUids[uid] = true; + result.push(keys[uid]); + } else { + seenUids[uid] = false; + } + } + } +}; + +Grid.prototype._forEachCell = function(bbox, fn, arg1, arg2) { + var x1 = this._convertToCellCoord(bbox[0]); + var y1 = this._convertToCellCoord(bbox[1]); + var x2 = this._convertToCellCoord(bbox[2]); + var y2 = this._convertToCellCoord(bbox[3]); + for (var x = x1; x <= x2; x++) { + for (var y = y1; y <= y2; y++) { + var cellIndex = this.d * y + x; + if (fn.call(this, bbox, cellIndex, arg1, arg2)) return; + } + } +}; + +Grid.prototype._convertToCellCoord = function(x) { + return Math.max(0, Math.min(this.d - 1, Math.floor(x * this.scale) + this.padding)); +}; + +Grid.prototype.toArrayBuffer = function() { + var cells = this.cells; + + var metadataLength = NUM_PARAMS + this.cells.length + 1 + 1; + var totalCellLength = 0; + for (var i = 0; i < this.cells.length; i++) { + totalCellLength += this.cells[i].length; + } + + var array = new Int32Array(metadataLength + totalCellLength + this.keys.length + this.bboxes.length); + array[0] = this.n; + array[1] = this.extent; + array[2] = this.padding; + + var offset = metadataLength; + for (var k = 0; k < cells.length; k++) { + var cell = cells[k]; + array[NUM_PARAMS + k] = offset; + array.set(cell, offset); + offset += cell.length; + } + + array[NUM_PARAMS + cells.length] = offset; + array.set(this.keys, offset); + offset += this.keys.length; + + array[NUM_PARAMS + cells.length + 1] = offset; + array.set(this.bboxes, offset); + offset += this.bboxes.length; + + return array.buffer; +}; From 11c8bb562b9a8a9b987033b315aca88d5e5f7376 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Mon, 29 Feb 2016 19:01:55 -0800 Subject: [PATCH 03/48] store CollisionBoxes in an ArrayBuffer This reduces the long-term memory usage of a tile by ~500KB. Saving memory is significant because it lets us cache more parsed tiles. This also speeds up CollisionFeature creation by about 60%. It's hard to measure, but this probably saves us a bit of gc time. Using an ArrayBuffer to store the data makes it potentially transferrable, which moves us closer to eliminating worker state. --- This also adds StructArray, a pretty fast and convenient way of adding conceptual objects to ArrayBuffers. var BoxArray = createStructArrayType([ { type: 'Int16', name: 'x1' }, { type: 'Int16', name: 'y1' }, { type: 'Int16', name: 'x2' }, { type: 'Int16', name: 'y2' }, { type: 'Float32', name: 'scale' }, ]); var boxes = new BoxArray(); boxes.emplaceBack(-2, -1, 2, 1, 1.23); boxes.emplaceBack(-3, -1, 3, 1, 1.23); var box = boxes.at(0); assert(box.x2 - box.x1 === 4); box._setIndex(1); // avoid creating a new box object assert(box.x2 - box.x1 === 6); How fast is it compared to regular objects? Adding features with emplaceBack is faster. Accessing and setting individual components is a tiny bit slower. --- js/data/bucket/symbol_bucket.js | 21 +++-- js/source/worker_tile.js | 10 ++- js/symbol/collision_box.js | 54 ++++++----- js/symbol/collision_feature.js | 23 ++--- js/symbol/collision_tile.js | 147 +++++++++++++++++------------- js/util/grid.js | 44 ++++----- js/util/struct_array.js | 155 ++++++++++++++++++++++++++++++++ 7 files changed, 328 insertions(+), 126 deletions(-) create mode 100644 js/util/struct_array.js diff --git a/js/data/bucket/symbol_bucket.js b/js/data/bucket/symbol_bucket.js index afa8a715a1e..4d16579b90f 100644 --- a/js/data/bucket/symbol_bucket.js +++ b/js/data/bucket/symbol_bucket.js @@ -28,6 +28,7 @@ function SymbolBucket(options) { Bucket.apply(this, arguments); this.showCollisionBoxes = options.showCollisionBoxes; this.overscaling = options.overscaling; + this.collisionBoxArray = options.collisionBoxArray; } SymbolBucket.prototype = util.inherit(Bucket, {}); @@ -223,7 +224,8 @@ SymbolBucket.prototype.populateBuffers = function(collisionTile, stacks, icons) } if (shapedText || shapedIcon) { - this.addFeature(geometries[k], shapedText, shapedIcon, features[k]); + // TODO WRONG + this.addFeature(geometries[k], shapedText, shapedIcon, k); } } @@ -232,7 +234,7 @@ SymbolBucket.prototype.populateBuffers = function(collisionTile, stacks, icons) this.trimBuffers(); }; -SymbolBucket.prototype.addFeature = function(lines, shapedText, shapedIcon, feature) { +SymbolBucket.prototype.addFeature = function(lines, shapedText, shapedIcon, featureIndex) { var layout = this.layer.layout; var glyphSize = 24; @@ -305,7 +307,7 @@ SymbolBucket.prototype.addFeature = function(lines, shapedText, shapedIcon, feat var addToBuffers = inside || mayOverlap; this.symbolInstances.push(new SymbolInstance(anchor, line, shapedText, shapedIcon, layout, - addToBuffers, this.symbolInstances.length, feature, this.layerIDs, + addToBuffers, this.symbolInstances.length, this.collisionBoxArray, featureIndex, 0, 0, textBoxScale, textPadding, textAlongLine, iconBoxScale, iconPadding, iconAlongLine)); } @@ -509,10 +511,9 @@ SymbolBucket.prototype.addToDebugBuffers = function(collisionTile) { for (var i = 0; i < 2; i++) { var feature = this.symbolInstances[j][i === 0 ? 'textCollisionFeature' : 'iconCollisionFeature']; if (!feature) continue; - var boxes = feature.boxes; - for (var b = 0; b < boxes.length; b++) { - var box = boxes[b]; + for (var b = feature.boxStartIndex; b < feature.boxEndIndex; b++) { + var box = this.collisionBoxArray.at(b); var anchorPoint = box.anchorPoint; var tl = new Point(box.x1, box.y1 * yStretch)._rotate(angle); @@ -537,7 +538,7 @@ SymbolBucket.prototype.addToDebugBuffers = function(collisionTile) { } }; -function SymbolInstance(anchor, line, shapedText, shapedIcon, layout, addToBuffers, index, feature, layerIDs, +function SymbolInstance(anchor, line, shapedText, shapedIcon, layout, addToBuffers, index, collisionBoxArray, featureIndex, sourceLayerIndex, bucketIndex, textBoxScale, textPadding, textAlongLine, iconBoxScale, iconPadding, iconAlongLine) { @@ -549,11 +550,13 @@ function SymbolInstance(anchor, line, shapedText, shapedIcon, layout, addToBuffe if (this.hasText) { this.glyphQuads = addToBuffers ? getGlyphQuads(anchor, shapedText, textBoxScale, line, layout, textAlongLine) : []; - this.textCollisionFeature = new CollisionFeature(line, anchor, feature, layerIDs, shapedText, textBoxScale, textPadding, textAlongLine, false); + this.textCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, + shapedText, textBoxScale, textPadding, textAlongLine, false); } if (this.hasIcon) { this.iconQuads = addToBuffers ? getIconQuads(anchor, shapedIcon, iconBoxScale, line, layout, iconAlongLine) : []; - this.iconCollisionFeature = new CollisionFeature(line, anchor, feature, layerIDs, shapedIcon, iconBoxScale, iconPadding, iconAlongLine, true); + this.iconCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, + shapedIcon, iconBoxScale, iconPadding, iconAlongLine, true); } } diff --git a/js/source/worker_tile.js b/js/source/worker_tile.js index 90c285da915..d36d5f464d2 100644 --- a/js/source/worker_tile.js +++ b/js/source/worker_tile.js @@ -4,6 +4,7 @@ var FeatureTree = require('../data/feature_tree'); var CollisionTile = require('../symbol/collision_tile'); var Bucket = require('../data/bucket'); var featureFilter = require('feature-filter'); +var CollisionBoxArray = require('../symbol/collision_box'); module.exports = WorkerTile; @@ -24,7 +25,8 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback) { this.status = 'parsing'; this.data = data; - var collisionTile = new CollisionTile(this.angle, this.pitch); + this.collisionBoxArray = new CollisionBoxArray(); + var collisionTile = new CollisionTile(this.angle, this.pitch, this.collisionBoxArray); this.featureTree = new FeatureTree(this.coord, this.overscaling, collisionTile); var stats = { _total: 0 }; @@ -51,7 +53,8 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback) { layer: layer, zoom: this.zoom, overscaling: this.overscaling, - showCollisionBoxes: this.showCollisionBoxes + showCollisionBoxes: this.showCollisionBoxes, + collisionBoxArray: this.collisionBoxArray }); bucket.createFilter(); @@ -203,7 +206,8 @@ WorkerTile.prototype.redoPlacement = function(angle, pitch, showCollisionBoxes) return {}; } - var collisionTile = new CollisionTile(angle, pitch); + var collisionTile = new CollisionTile(angle, pitch, this.collisionBoxArray); + this.featureTree.setCollisionTile(collisionTile); var buckets = this.symbolBuckets; diff --git a/js/symbol/collision_box.js b/js/symbol/collision_box.js index 24cbf654ee3..db5846ebd63 100644 --- a/js/symbol/collision_box.js +++ b/js/symbol/collision_box.js @@ -1,6 +1,8 @@ 'use strict'; -module.exports = CollisionBox; +var createStructArrayType = require('../util/struct_array'); +var Point = require('point-geometry'); + /** * A collision box represents an area of the map that that is covered by a @@ -45,29 +47,39 @@ module.exports = CollisionBox; * @param {Array} layerIDs The IDs of the layers that this CollisionBox is a part of. * @private */ -function CollisionBox(anchorPoint, x1, y1, x2, y2, maxScale, feature, layerIDs) { - // the box is centered around the anchor point - this.anchorPoint = anchorPoint; - // distances to the edges from the anchor - this.x1 = x1; - this.y1 = y1; - this.x2 = x2; - this.y2 = y2; +module.exports = createStructArrayType([ + // the box is centered around the anchor point + // TODO: this should be an Int16 but it's causing some render tests to fail. + { type: 'Float32', name: 'anchorPointX' }, + { type: 'Float32', name: 'anchorPointY' }, - // the box is only valid for scales < maxScale. - // The box does not block other boxes at scales >= maxScale; - this.maxScale = maxScale; + // distances to the edges from the anchor + { type: 'Int16', name: 'x1' }, + { type: 'Int16', name: 'y1' }, + { type: 'Int16', name: 'x2' }, + { type: 'Int16', name: 'y2' }, - // the index of the feature in the original vectortile - this.feature = feature; + // the box is only valid for scales < maxScale. + // The box does not block other boxes at scales >= maxScale; + { type: 'Float32', name: 'maxScale' }, - // the IDs of the layers this feature collision box appears in - this.layerIDs = layerIDs; + // the index of the feature in the original vectortile + { type: 'Uint32', name: 'featureIndex' }, + // the source layer the feature appears in + { type: 'Uint16', name: 'sourceLayerIndex' }, + // the bucket the feature appears in + { type: 'Uint16', name: 'bucketIndex' }, - // the scale at which the label can first be shown - this.placementScale = 0; + // rotated and scaled bbox used for indexing + { type: 'Int16', name: 'bbox0' }, + { type: 'Int16', name: 'bbox1' }, + { type: 'Int16', name: 'bbox2' }, + { type: 'Int16', name: 'bbox3' }, - // rotated and scaled bbox used for indexing - this[0] = this[1] = this[2] = this[3] = 0; -} + { type: 'Float32', name: 'placementScale' } +], { + get anchorPoint() { + return new Point(this.anchorPointX, this.anchorPointY); + } +}); diff --git a/js/symbol/collision_feature.js b/js/symbol/collision_feature.js index 87e159505c5..e447c5ab4f7 100644 --- a/js/symbol/collision_feature.js +++ b/js/symbol/collision_feature.js @@ -1,8 +1,5 @@ 'use strict'; -var CollisionBox = require('./collision_box'); -var Point = require('point-geometry'); - module.exports = CollisionFeature; /** @@ -23,14 +20,14 @@ module.exports = CollisionFeature; * * @private */ -function CollisionFeature(line, anchor, feature, IDs, shaped, boxScale, padding, alignLine, straight) { +function CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shaped, boxScale, padding, alignLine, straight) { var y1 = shaped.top * boxScale - padding; var y2 = shaped.bottom * boxScale + padding; var x1 = shaped.left * boxScale - padding; var x2 = shaped.right * boxScale + padding; - this.boxes = []; + this.boxStartIndex = collisionBoxArray.length; if (alignLine) { @@ -46,15 +43,18 @@ function CollisionFeature(line, anchor, feature, IDs, shaped, boxScale, padding, // used for icon labels that are aligned with the line, but don't curve along it var vector = line[anchor.segment + 1].sub(line[anchor.segment])._unit()._mult(length); var straightLine = [anchor.sub(vector), anchor.add(vector)]; - this._addLineCollisionBoxes(straightLine, anchor, 0, length, height, feature, IDs); + this._addLineCollisionBoxes(collisionBoxArray, straightLine, anchor, 0, length, height, featureIndex, sourceLayerIndex, bucketIndex); } else { // used for text labels that curve along a line - this._addLineCollisionBoxes(line, anchor, anchor.segment, length, height, feature, IDs); + this._addLineCollisionBoxes(collisionBoxArray, line, anchor, anchor.segment, length, height, featureIndex, sourceLayerIndex, bucketIndex); } } else { - this.boxes.push(new CollisionBox(new Point(anchor.x, anchor.y), x1, y1, x2, y2, Infinity, feature, IDs)); + collisionBoxArray.emplaceBack(anchor.x, anchor.y, x1, y1, x2, y2, Infinity, featureIndex, sourceLayerIndex, bucketIndex, + 0, 0, 0, 0, 0); } + + this.boxEndIndex = collisionBoxArray.length; } /** @@ -69,7 +69,7 @@ function CollisionFeature(line, anchor, feature, IDs, shaped, boxScale, padding, * * @private */ -CollisionFeature.prototype._addLineCollisionBoxes = function(line, anchor, segment, labelLength, boxSize, feature, IDs) { +CollisionFeature.prototype._addLineCollisionBoxes = function(collisionBoxArray, line, anchor, segment, labelLength, boxSize, featureIndex, sourceLayerIndex, bucketIndex) { var step = boxSize / 2; var nBoxes = Math.floor(labelLength / step); @@ -122,7 +122,10 @@ CollisionFeature.prototype._addLineCollisionBoxes = function(line, anchor, segme var distanceToInnerEdge = Math.max(Math.abs(boxDistanceToAnchor - firstBoxOffset) - step / 2, 0); var maxScale = labelLength / 2 / distanceToInnerEdge; - bboxes.push(new CollisionBox(boxAnchorPoint, -boxSize / 2, -boxSize / 2, boxSize / 2, boxSize / 2, maxScale, feature, IDs)); + collisionBoxArray.emplaceBack(boxAnchorPoint.x, boxAnchorPoint.y, + -boxSize / 2, -boxSize / 2, boxSize / 2, boxSize / 2, maxScale, + featureIndex, sourceLayerIndex, bucketIndex, + 0, 0, 0, 0, 0); } return bboxes; diff --git a/js/symbol/collision_tile.js b/js/symbol/collision_tile.js index 7761abd5c0b..c5748e84b55 100644 --- a/js/symbol/collision_tile.js +++ b/js/symbol/collision_tile.js @@ -1,6 +1,5 @@ 'use strict'; -var CollisionBox = require('./collision_box'); var Point = require('point-geometry'); var EXTENT = require('../data/bucket').EXTENT; var Grid = require('../util/grid'); @@ -17,7 +16,7 @@ module.exports = CollisionTile; * @param {number} pitch * @private */ -function CollisionTile(angle, pitch) { +function CollisionTile(angle, pitch, collisionBoxArray) { this.grid = new Grid(12, EXTENT, 6); this.gridFeatures = []; this.ignoredGrid = new Grid(12, EXTENT, 0); @@ -37,15 +36,38 @@ function CollisionTile(angle, pitch) { // Sort of account for this by making all boxes a bit bigger. this.yStretch = Math.pow(this.yStretch, 1.3); - this.edges = [ + this.collisionBoxArray = collisionBoxArray; + if (collisionBoxArray.length === 0) { + // the first collisionBoxArray is passed to a CollisionTile + + // tempCollisionBox + collisionBoxArray.emplaceBack(); + + var maxInt16 = 32767; //left - new CollisionBox(new Point(0, 0), 0, -Infinity, 0, Infinity, Infinity), + collisionBoxArray.emplaceBack(0, 0, 0, -maxInt16, 0, maxInt16, maxInt16, + 0, 0, 0, 0, 0, 0, 0, 0, + 0); // right - new CollisionBox(new Point(EXTENT, 0), 0, -Infinity, 0, Infinity, Infinity), + collisionBoxArray.emplaceBack(EXTENT, 0, 0, -maxInt16, 0, maxInt16, maxInt16, + 0, 0, 0, 0, 0, 0, 0, 0, + 0); // top - new CollisionBox(new Point(0, 0), -Infinity, 0, Infinity, 0, Infinity), + collisionBoxArray.emplaceBack(0, 0, -maxInt16, 0, maxInt16, 0, maxInt16, + 0, 0, 0, 0, 0, 0, 0, 0, + 0); // bottom - new CollisionBox(new Point(0, EXTENT), -Infinity, 0, Infinity, 0, Infinity) + collisionBoxArray.emplaceBack(0, EXTENT, -maxInt16, 0, maxInt16, 0, maxInt16, + 0, 0, 0, 0, 0, 0, 0, 0, + 0); + } + + this.tempCollisionBox = collisionBoxArray.at(0); + this.edges = [ + collisionBoxArray.at(1), + collisionBoxArray.at(2), + collisionBoxArray.at(3), + collisionBoxArray.at(4) ]; } @@ -66,26 +88,34 @@ CollisionTile.prototype.placeCollisionFeature = function(collisionFeature, allow var minPlacementScale = this.minScale; var rotationMatrix = this.rotationMatrix; var yStretch = this.yStretch; + var boxArray = this.collisionBoxArray; + var box = boxArray._struct; + var blocking = boxArray.at(0); - for (var b = 0; b < collisionFeature.boxes.length; b++) { + for (var b = collisionFeature.boxStartIndex; b < collisionFeature.boxEndIndex; b++) { - var box = collisionFeature.boxes[b]; + box._setIndex(b); if (!allowOverlap) { - var anchorPoint = box.anchorPoint.matMult(rotationMatrix); + var anchorPoint = box.anchorPoint._matMult(rotationMatrix); var x = anchorPoint.x; var y = anchorPoint.y; - box[0] = x + box.x1; - box[1] = y + box.y1 * yStretch; - box[2] = x + box.x2; - box[3] = y + box.y2 * yStretch; + var x1 = x + box.x1; + var y1 = y + box.y1 * yStretch; + var x2 = x + box.x2; + var y2 = y + box.y2 * yStretch; - var blockingBoxes = this.grid.query(box); + box.bbox0 = x1; + box.bbox1 = y1; + box.bbox2 = x2; + box.bbox3 = y2; + + var blockingBoxes = this.grid.query(x1, y1, x2, y2); for (var i = 0; i < blockingBoxes.length; i++) { - var blocking = this.gridFeatures[blockingBoxes[i]]; - var blockingAnchorPoint = blocking.anchorPoint.matMult(rotationMatrix); + blocking._setIndex(blockingBoxes[i]); + var blockingAnchorPoint = blocking.anchorPoint._matMult(rotationMatrix); minPlacementScale = this.getPlacementScale(minPlacementScale, anchorPoint, box, blockingAnchorPoint, blocking); if (minPlacementScale >= this.maxScale) { @@ -95,17 +125,26 @@ CollisionTile.prototype.placeCollisionFeature = function(collisionFeature, allow } if (avoidEdges) { - var reverseRotationMatrix = this.reverseRotationMatrix; - var tl = new Point(box.x1, box.y1).matMult(reverseRotationMatrix); - var tr = new Point(box.x2, box.y1).matMult(reverseRotationMatrix); - var bl = new Point(box.x1, box.y2).matMult(reverseRotationMatrix); - var br = new Point(box.x2, box.y2).matMult(reverseRotationMatrix); - var rotatedCollisionBox = new CollisionBox(box.anchorPoint, - Math.min(tl.x, tr.x, bl.x, br.x), - Math.min(tl.y, tr.x, bl.x, br.x), - Math.max(tl.x, tr.x, bl.x, br.x), - Math.max(tl.y, tr.x, bl.x, br.x), - box.maxScale); + var rotatedCollisionBox; + + if (this.angle) { + var reverseRotationMatrix = this.reverseRotationMatrix; + var tl = new Point(box.x1, box.y1).matMult(reverseRotationMatrix); + var tr = new Point(box.x2, box.y1).matMult(reverseRotationMatrix); + var bl = new Point(box.x1, box.y2).matMult(reverseRotationMatrix); + var br = new Point(box.x2, box.y2).matMult(reverseRotationMatrix); + + rotatedCollisionBox = this.tempCollisionBox; + rotatedCollisionBox.anchorPointX = box.anchorPoint.x; + rotatedCollisionBox.anchorPointY = box.anchorPoint.y; + rotatedCollisionBox.x1 = Math.min(tl.x, tr.x, bl.x, br.x); + rotatedCollisionBox.y1 = Math.min(tl.y, tr.x, bl.x, br.x); + rotatedCollisionBox.x2 = Math.max(tl.x, tr.x, bl.x, br.x); + rotatedCollisionBox.y2 = Math.max(tl.y, tr.x, bl.x, br.x); + rotatedCollisionBox.maxScale = box.maxScale; + } else { + rotatedCollisionBox = box; + } for (var k = 0; k < this.edges.length; k++) { var edgeBox = this.edges[k]; @@ -166,26 +205,30 @@ CollisionTile.prototype.getPlacementScale = function(minPlacementScale, anchorPo // Find the lowest scale at which the two boxes can fit side by side without overlapping. // Original algorithm: - var s1 = (blocking.x1 - box.x2) / (anchorPoint.x - blockingAnchorPoint.x); // scale at which new box is to the left of old box - var s2 = (blocking.x2 - box.x1) / (anchorPoint.x - blockingAnchorPoint.x); // scale at which new box is to the right of old box - var s3 = (blocking.y1 - box.y2) * this.yStretch / (anchorPoint.y - blockingAnchorPoint.y); // scale at which new box is to the top of old box - var s4 = (blocking.y2 - box.y1) * this.yStretch / (anchorPoint.y - blockingAnchorPoint.y); // scale at which new box is to the bottom of old box + var anchorDiffX = anchorPoint.x - blockingAnchorPoint.x; + var anchorDiffY = anchorPoint.y - blockingAnchorPoint.y; + var s1 = (blocking.x1 - box.x2) / anchorDiffX; // scale at which new box is to the left of old box + var s2 = (blocking.x2 - box.x1) / anchorDiffX; // scale at which new box is to the right of old box + var s3 = (blocking.y1 - box.y2) * this.yStretch / anchorDiffY; // scale at which new box is to the top of old box + var s4 = (blocking.y2 - box.y1) * this.yStretch / anchorDiffY; // scale at which new box is to the bottom of old box if (isNaN(s1) || isNaN(s2)) s1 = s2 = 1; if (isNaN(s3) || isNaN(s4)) s3 = s4 = 1; var collisionFreeScale = Math.min(Math.max(s1, s2), Math.max(s3, s4)); + var blockingMaxScale = blocking.maxScale; + var boxMaxScale = box.maxScale; - if (collisionFreeScale > blocking.maxScale) { + if (collisionFreeScale > blockingMaxScale) { // After a box's maxScale the label has shrunk enough that the box is no longer needed to cover it, // so unblock the new box at the scale that the old box disappears. - collisionFreeScale = blocking.maxScale; + collisionFreeScale = blockingMaxScale; } - if (collisionFreeScale > box.maxScale) { + if (collisionFreeScale > boxMaxScale) { // If the box can only be shown after it is visible, then the box can never be shown. // But the label can be shown after this box is not visible. - collisionFreeScale = box.maxScale; + collisionFreeScale = boxMaxScale; } if (collisionFreeScale > minPlacementScale && @@ -211,32 +254,14 @@ CollisionTile.prototype.getPlacementScale = function(minPlacementScale, anchorPo */ CollisionTile.prototype.insertCollisionFeature = function(collisionFeature, minPlacementScale, ignorePlacement) { - var boxes = collisionFeature.boxes; - for (var k = 0; k < boxes.length; k++) { - boxes[k].placementScale = minPlacementScale; - } + var grid = ignorePlacement ? this.ignoredGrid : this.grid; - if (minPlacementScale < this.maxScale) { - if (ignorePlacement) { - this.insertIgnoredGrid(boxes); - } else { - this.insertGrid(boxes); + var box = this.collisionBoxArray._struct; + for (var k = collisionFeature.boxStartIndex; k < collisionFeature.boxEndIndex; k++) { + box._setIndex(k); + box.placementScale = minPlacementScale; + if (minPlacementScale < this.maxScale) { + grid.insert(k, box.bbox0, box.bbox1, box.bbox2, box.bbox3); } } }; - -CollisionTile.prototype.insertGrid = function(boxes) { - for (var i = 0; i < boxes.length; i++) { - var box = boxes[i]; - this.grid.insert(box, this.gridFeatures.length); - this.gridFeatures.push(box); - } -}; - -CollisionTile.prototype.insertIgnoredGrid = function(boxes) { - for (var i = 0; i < boxes.length; i++) { - var box = boxes[i]; - this.ignoredGrid.insert(box, this.ignoredGridFeatures.length); - this.ignoredGridFeatures.push(box); - } -}; diff --git a/js/util/grid.js b/js/util/grid.js index cbd1761b6b9..b64b01e2ffd 100644 --- a/js/util/grid.js +++ b/js/util/grid.js @@ -38,27 +38,27 @@ function Grid(n, extent, padding) { } -Grid.prototype.insert = function(bbox, key) { - this._forEachCell(bbox, this._insertCell, this.uid++); +Grid.prototype.insert = function(key, x1, y1, x2, y2) { + this._forEachCell(x1, y1, x2, y2, this._insertCell, this.uid++); this.keys.push(key); - this.bboxes.push(bbox[0]); - this.bboxes.push(bbox[1]); - this.bboxes.push(bbox[2]); - this.bboxes.push(bbox[3]); + this.bboxes.push(x1); + this.bboxes.push(y1); + this.bboxes.push(x2); + this.bboxes.push(y2); }; -Grid.prototype._insertCell = function(bbox, cellIndex, uid) { +Grid.prototype._insertCell = function(x1, y1, x2, y2, cellIndex, uid) { this.cells[cellIndex].push(uid); }; -Grid.prototype.query = function(bbox) { +Grid.prototype.query = function(x1, y1, x2, y2) { var result = []; var seenUids = {}; - this._forEachCell(bbox, this._queryCell, result, seenUids); + this._forEachCell(x1, y1, x2, y2, this._queryCell, result, seenUids); return result; }; -Grid.prototype._queryCell = function(bbox, cellIndex, result, seenUids) { +Grid.prototype._queryCell = function(x1, y1, x2, y2, cellIndex, result, seenUids) { var cell = this.cells[cellIndex]; var keys = this.keys; var bboxes = this.bboxes; @@ -66,10 +66,10 @@ Grid.prototype._queryCell = function(bbox, cellIndex, result, seenUids) { var uid = cell[u]; if (seenUids[uid] === undefined) { var offset = uid * 4; - if ((bbox[0] <= bboxes[offset + 2]) && - (bbox[1] <= bboxes[offset + 3]) && - (bbox[2] >= bboxes[offset + 0]) && - (bbox[3] >= bboxes[offset + 1])) { + if ((x1 <= bboxes[offset + 2]) && + (y1 <= bboxes[offset + 3]) && + (x2 >= bboxes[offset + 0]) && + (y2 >= bboxes[offset + 1])) { seenUids[uid] = true; result.push(keys[uid]); } else { @@ -79,15 +79,15 @@ Grid.prototype._queryCell = function(bbox, cellIndex, result, seenUids) { } }; -Grid.prototype._forEachCell = function(bbox, fn, arg1, arg2) { - var x1 = this._convertToCellCoord(bbox[0]); - var y1 = this._convertToCellCoord(bbox[1]); - var x2 = this._convertToCellCoord(bbox[2]); - var y2 = this._convertToCellCoord(bbox[3]); - for (var x = x1; x <= x2; x++) { - for (var y = y1; y <= y2; y++) { +Grid.prototype._forEachCell = function(x1, y1, x2, y2, fn, arg1, arg2) { + var cx1 = this._convertToCellCoord(x1); + var cy1 = this._convertToCellCoord(y1); + var cx2 = this._convertToCellCoord(x2); + var cy2 = this._convertToCellCoord(y2); + for (var x = cx1; x <= cx2; x++) { + for (var y = cy1; y <= cy2; y++) { var cellIndex = this.d * y + x; - if (fn.call(this, bbox, cellIndex, arg1, arg2)) return; + if (fn.call(this, x1, y1, x2, y2, cellIndex, arg1, arg2)) return; } } }; diff --git a/js/util/struct_array.js b/js/util/struct_array.js new file mode 100644 index 00000000000..c5e8f9c953b --- /dev/null +++ b/js/util/struct_array.js @@ -0,0 +1,155 @@ +'use strict'; + +var inherit = require('./util').inherit; + +module.exports = createStructArrayType; + +var viewTypes = { + 'Int8': Int8Array, + 'Uint8': Uint8Array, + 'Uint8Clamped': Uint8ClampedArray, + 'Int16': Int16Array, + 'Uint16': Uint16Array, + 'Int32': Int32Array, + 'Uint32': Uint32Array, + 'Float32': Float32Array, + 'Float64': Float64Array +}; + +function createStructArrayType(members, methods) { + if (methods === undefined) methods = {}; + + function StructType() { + Struct.apply(this, arguments); + } + + StructType.prototype = inherit(Struct, methods); + + var offset = 0; + var maxSize = 0; + + for (var m = 0; m < members.length; m++) { + var member = members[m]; + + if (!viewTypes[member.type]) { + throw new Error(JSON.stringify(member.type) + ' is not a valid type'); + } + + var size = sizeOf(member.type); + maxSize = Math.max(maxSize, size); + offset = member.offset = align(offset, size); + + Object.defineProperty(StructType.prototype, member.name, { + get: createGetter(member.type, offset), + set: createSetter(member.type, offset) + }); + + offset += size; + } + + StructType.prototype.BYTE_SIZE = align(offset, maxSize); + + function StructArrayType() { + StructArray.apply(this, arguments); + } + + StructArrayType.prototype = Object.create(StructArray.prototype); + StructArrayType.prototype.StructType = StructType; + StructArrayType.prototype.BYTES_PER_ELEMENT = StructType.prototype.BYTE_SIZE; + StructArrayType.prototype.emplaceBack = createEmplaceBack(members, StructType.prototype.BYTE_SIZE); + + return StructArrayType; +} + +function align(offset, size) { + return Math.ceil(offset / size) * size; +} + +function sizeOf(type) { + return viewTypes[type].BYTES_PER_ELEMENT; +} + +function getArrayViewName(type) { + return type.toLowerCase() + 'Array'; +} + + +function createEmplaceBack(members, BYTES_PER_ELEMENT) { + var argNames = []; + var body = '' + + 'var pos1 = this.length * ' + BYTES_PER_ELEMENT.toFixed(0) + ';\n' + + 'var pos2 = pos1 / 2;\n' + + 'var pos4 = pos1 / 4;\n' + + 'this.length++;\n' + + 'if (this.length > this.allocatedLength) this.resize(this.length);\n'; + for (var m = 0; m < members.length; m++) { + var member = members[m]; + var argName = 'arg_' + m; + var index = 'pos' + sizeOf(member.type).toFixed(0) + ' + ' + (member.offset / sizeOf(member.type)).toFixed(0); + body += 'this.' + getArrayViewName(member.type) + '[' + index + '] = ' + argName + ';\n'; + argNames.push(argName); + } + return new Function(argNames, body); +} + +function createGetter(type, offset) { + var index = 'this._pos' + sizeOf(type).toFixed(0) + ' + ' + (offset / sizeOf(type)).toFixed(0); + return new Function([], 'return this._structArray.' + getArrayViewName(type) + '[' + index + '];'); +} + +function createSetter(type, offset) { + var index = 'this._pos' + sizeOf(type).toFixed(0) + ' + ' + (offset / sizeOf(type)).toFixed(0); + return new Function(['x'], 'this._structArray.' + getArrayViewName(type) + '[' + index + '] = x;'); +} + + +function Struct(structArray, index) { + this._structArray = structArray; + this._setIndex(index); +} + +Struct.prototype._setIndex = function(index) { + this._pos1 = index * this.BYTE_SIZE; + this._pos2 = this._pos1 / 2; + this._pos4 = this._pos1 / 4; +}; + + +function StructArray(initialAllocatedLength) { + if (initialAllocatedLength instanceof ArrayBuffer) { + this.arrayBuffer = initialAllocatedLength; + this._refreshViews(); + this.length = this.uint8Array.length / this.BYTES_PER_ELEMENT; + this.size = this.uint8Array.length; + } else { + if (initialAllocatedLength === undefined) { + initialAllocatedLength = this.DEFAULT_ALLOCATED_LENGTH; + } + this.resize(initialAllocatedLength); + } + this._struct = new this.StructType(this, 0); +} + +StructArray.prototype.DEFAULT_ALLOCATED_LENGTH = 100; +StructArray.prototype.RESIZE_FACTOR = 1.5; +StructArray.prototype.allocatedLength = 0; +StructArray.prototype.length = 0; + +StructArray.prototype.resize = function(n) { + this.allocatedLength = Math.max(n, Math.floor(this.allocatedLength * this.RESIZE_FACTOR)); + this.arrayBuffer = new ArrayBuffer(Math.floor(this.allocatedLength * this.BYTES_PER_ELEMENT / 8) * 8); + + var oldUint8Array = this.uint8Array; + this._refreshViews(); + if (oldUint8Array) this.uint8Array.set(oldUint8Array); +}; + +StructArray.prototype._refreshViews = function() { + for (var t in viewTypes) { + this[getArrayViewName(t)] = new viewTypes[t](this.arrayBuffer); + } +}; + +StructArray.prototype.at = function(index) { + return new this.StructType(this, index); +}; From 9b4f1b3f5f00d3cb7a7e620fe28d0b503dad272f Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 1 Mar 2016 02:05:09 -0800 Subject: [PATCH 04/48] store the featureTree array in a StructArray --- js/data/bucket.js | 3 +- js/data/bucket/symbol_bucket.js | 5 +- js/data/feature_tree.js | 115 ++++++++++++++++++++----------- js/source/worker_tile.js | 16 ++++- js/symbol/collision_tile.js | 49 +++++++------ js/util/string_number_mapping.js | 13 ++++ 6 files changed, 132 insertions(+), 69 deletions(-) create mode 100644 js/util/string_number_mapping.js diff --git a/js/data/bucket.js b/js/data/bucket.js index f51ead6e9b3..8c49842c64e 100644 --- a/js/data/bucket.js +++ b/js/data/bucket.js @@ -71,7 +71,8 @@ function Bucket(options) { this.type = this.layer.type; this.features = []; this.id = this.layer.id; - this['source-layer'] = this.layer['source-layer']; + this.sourceLayer = this.layer.sourceLayer; + this.sourceLayerIndex = options.sourceLayerIndex; this.minZoom = this.layer.minzoom; this.maxZoom = this.layer.maxzoom; diff --git a/js/data/bucket/symbol_bucket.js b/js/data/bucket/symbol_bucket.js index 4d16579b90f..7294b549711 100644 --- a/js/data/bucket/symbol_bucket.js +++ b/js/data/bucket/symbol_bucket.js @@ -224,8 +224,7 @@ SymbolBucket.prototype.populateBuffers = function(collisionTile, stacks, icons) } if (shapedText || shapedIcon) { - // TODO WRONG - this.addFeature(geometries[k], shapedText, shapedIcon, k); + this.addFeature(geometries[k], shapedText, shapedIcon, features[k].index); } } @@ -307,7 +306,7 @@ SymbolBucket.prototype.addFeature = function(lines, shapedText, shapedIcon, feat var addToBuffers = inside || mayOverlap; this.symbolInstances.push(new SymbolInstance(anchor, line, shapedText, shapedIcon, layout, - addToBuffers, this.symbolInstances.length, this.collisionBoxArray, featureIndex, 0, 0, + addToBuffers, this.symbolInstances.length, this.collisionBoxArray, featureIndex, this.sourceLayerIndex, this.index, textBoxScale, textPadding, textAlongLine, iconBoxScale, iconPadding, iconAlongLine)); } diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index 5d6733cda16..7ce8e63171a 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -4,35 +4,54 @@ var rbush = require('rbush'); var Point = require('point-geometry'); var util = require('../util/util'); var loadGeometry = require('./load_geometry'); -var CollisionBox = require('../symbol/collision_box'); var EXTENT = require('./bucket').EXTENT; var featureFilter = require('feature-filter'); +var createStructArrayType = require('../util/struct_array'); +var Grid = require('../util/grid'); +var StringNumberMapping = require('../util/string_number_mapping'); + +var FeatureIndexArray = createStructArrayType([ + // the index of the feature in the original vectortile + { type: 'Uint32', name: 'featureIndex' }, + // the source layer the feature appears in + { type: 'Uint16', name: 'sourceLayerIndex' }, + // the bucket the feature appears in + { type: 'Uint16', name: 'bucketIndex' } +]); module.exports = FeatureTree; -function FeatureTree(coord, overscaling, collisionTile) { +function FeatureTree(coord, overscaling, collisionTile, vtLayers) { this.x = coord.x; this.y = coord.y; this.z = coord.z - Math.log(overscaling) / Math.LN2; this.rtree = rbush(9); this.toBeInserted = []; + this.grid = new Grid(16, EXTENT, 0); + this.featureIndexArray = new FeatureIndexArray(); this.setCollisionTile(collisionTile); + this.vtLayers = vtLayers; + this.sourceLayerNumberMapping = new StringNumberMapping(vtLayers ? Object.keys(vtLayers).sort() : []); } -FeatureTree.prototype.insert = function(bbox, layerIDs, feature) { - var scale = EXTENT / feature.extent; +FeatureTree.prototype.insert = function(bbox, extent, featureIndex, sourceLayerIndex, bucketIndex) { + var scale = EXTENT / extent; bbox[0] *= scale; bbox[1] *= scale; bbox[2] *= scale; bbox[3] *= scale; - bbox.layerIDs = layerIDs; - bbox.feature = feature; + bbox.key = this.featureIndexArray.length; + this.featureIndexArray.emplaceBack(featureIndex, sourceLayerIndex, bucketIndex); this.toBeInserted.push(bbox); }; // bulk insert into tree FeatureTree.prototype._load = function() { this.rtree.load(this.toBeInserted); + for (var i = 0; i < this.toBeInserted.length; i++) { + var bbox = this.toBeInserted[i]; + this.grid.insert(i, bbox[0], bbox[1], bbox[2], bbox[3]); + } this.toBeInserted = []; }; @@ -46,6 +65,7 @@ function translateDistance(translate) { // Finds features in this tile at a particular position. FeatureTree.prototype.query = function(args, styleLayersByID) { + if (!this.vtLayers) return []; if (this.toBeInserted.length) this._load(); var params = args.params || {}, @@ -89,55 +109,68 @@ FeatureTree.prototype.query = function(args, styleLayersByID) { } var bounds = [minX - additionalRadius, minY - additionalRadius, maxX + additionalRadius, maxY + additionalRadius]; - var symbolQueryBox = new CollisionBox(new Point(minX, minY), 0, 0, maxX - minX, maxY - minY, args.scale, null); + var treeMatching = this.rtree.search(bounds); - var matching = this.rtree.search(bounds).concat(this.collisionTile.getFeaturesAt(symbolQueryBox, args.scale)); + var matching = this.grid.query(minX - additionalRadius, minY - additionalRadius, maxX + additionalRadius, maxY + additionalRadius); + var match = this.featureIndexArray.at(0); + filterMatching.call(this, matching, match); - for (var k = 0; k < matching.length; k++) { - var feature = matching[k].feature, - layerIDs = matching[k].layerIDs; + if (matching.length !== treeMatching.length) throw Error("asdf"); - if (!filter(feature)) continue; + var matchingSymbols = this.collisionTile.queryRenderedSymbols(minX, minY, maxX, maxY, args.scale); + var match2 = this.collisionTile.collisionBoxArray.at(0); + filterMatching.call(this, matchingSymbols, match2); - var geoJSON = feature.toGeoJSON(this.x, this.y, this.z); + function filterMatching(matching, match) { + for (var k = 0; k < matching.length; k++) { + match._setIndex(matching[k]); + var sourceLayerName = this.sourceLayerNumberMapping.numberToString[match.sourceLayerIndex]; + var sourceLayer = this.vtLayers[sourceLayerName]; + var feature = sourceLayer.feature(match.featureIndex); + var layerIDs = this.numberToLayerIDs[match.bucketIndex]; - if (!params.includeGeometry) { - geoJSON.geometry = null; - } + if (!filter(feature)) continue; - for (var l = 0; l < layerIDs.length; l++) { - var layerID = layerIDs[l]; + var geoJSON = feature.toGeoJSON(this.x, this.y, this.z); - if (params.layerIds && params.layerIds.indexOf(layerID) < 0) { - continue; + if (!params.includeGeometry) { + geoJSON.geometry = null; } - styleLayer = styleLayersByID[layerID]; - var geometry = loadGeometry(feature); - - var translatedPolygon; - if (styleLayer.type === 'symbol') { - // all symbols already match the style + for (var l = 0; l < layerIDs.length; l++) { + var layerID = layerIDs[l]; - } else if (styleLayer.type === 'line') { - translatedPolygon = 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 (params.layerIds && params.layerIds.indexOf(layerID) < 0) { + continue; } - if (!polygonIntersectsBufferedMultiLine(translatedPolygon, geometry, halfWidth)) continue; - } else if (styleLayer.type === 'fill') { - translatedPolygon = translate(styleLayer.paint['fill-translate'], styleLayer.paint['fill-translate-anchor']); - if (!polygonIntersectsMultiPolygon(translatedPolygon, geometry)) continue; + styleLayer = styleLayersByID[layerID]; + var geometry = loadGeometry(feature); + + var translatedPolygon; + if (styleLayer.type === 'symbol') { + // all symbols already match the style + + } else if (styleLayer.type === 'line') { + translatedPolygon = 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 (!polygonIntersectsBufferedMultiLine(translatedPolygon, geometry, halfWidth)) continue; + + } else if (styleLayer.type === 'fill') { + translatedPolygon = translate(styleLayer.paint['fill-translate'], styleLayer.paint['fill-translate-anchor']); + if (!polygonIntersectsMultiPolygon(translatedPolygon, geometry)) continue; + + } else if (styleLayer.type === 'circle') { + translatedPolygon = translate(styleLayer.paint['circle-translate'], styleLayer.paint['circle-translate-anchor']); + var circleRadius = styleLayer.paint['circle-radius'] * pixelsToTileUnits; + if (!polygonIntersectsBufferedMultiPoint(translatedPolygon, geometry, circleRadius)) continue; + } - } else if (styleLayer.type === 'circle') { - translatedPolygon = translate(styleLayer.paint['circle-translate'], styleLayer.paint['circle-translate-anchor']); - var circleRadius = styleLayer.paint['circle-radius'] * pixelsToTileUnits; - if (!polygonIntersectsBufferedMultiPoint(translatedPolygon, geometry, circleRadius)) continue; + result.push(util.extend({layer: layerID}, geoJSON)); } - - result.push(util.extend({layer: layerID}, geoJSON)); } } diff --git a/js/source/worker_tile.js b/js/source/worker_tile.js index d36d5f464d2..6478292e681 100644 --- a/js/source/worker_tile.js +++ b/js/source/worker_tile.js @@ -5,6 +5,7 @@ var CollisionTile = require('../symbol/collision_tile'); var Bucket = require('../data/bucket'); var featureFilter = require('feature-filter'); var CollisionBoxArray = require('../symbol/collision_box'); +var StringNumberMapping = require('../util/string_number_mapping'); module.exports = WorkerTile; @@ -27,7 +28,8 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback) { this.collisionBoxArray = new CollisionBoxArray(); var collisionTile = new CollisionTile(this.angle, this.pitch, this.collisionBoxArray); - this.featureTree = new FeatureTree(this.coord, this.overscaling, collisionTile); + this.featureTree = new FeatureTree(this.coord, this.overscaling, collisionTile, data.layers); + var sourceLayerNumberMapping = new StringNumberMapping(data.layers ? Object.keys(data.layers).sort() : []); var stats = { _total: 0 }; @@ -54,7 +56,8 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback) { zoom: this.zoom, overscaling: this.overscaling, showCollisionBoxes: this.showCollisionBoxes, - collisionBoxArray: this.collisionBoxArray + collisionBoxArray: this.collisionBoxArray, + sourceLayerIndex: sourceLayerNumberMapping.stringToNumber[layer['source-layer']] }); bucket.createFilter(); @@ -90,6 +93,7 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback) { function sortLayerIntoBuckets(layer, buckets) { for (var i = 0; i < layer.length; i++) { var feature = layer.feature(i); + feature.index = i; for (var id in buckets) { if (buckets[id].filter(feature)) buckets[id].features.push(feature); @@ -101,10 +105,15 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback) { symbolBuckets = this.symbolBuckets = [], otherBuckets = []; + this.featureTree.numberToLayerIDs = []; + for (var id in bucketsById) { bucket = bucketsById[id]; if (bucket.features.length === 0) continue; + bucket.index = this.featureTree.numberToLayerIDs.length; + this.featureTree.numberToLayerIDs.push(bucket.layerIDs); + buckets.push(bucket); if (bucket.type === 'symbol') @@ -171,10 +180,11 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback) { bucket.populateBuffers(collisionTile, stacks, icons); var time = Date.now() - now; + if (bucket.type !== 'symbol') { for (var i = 0; i < bucket.features.length; i++) { var feature = bucket.features[i]; - tile.featureTree.insert(feature.bbox(), bucket.layerIDs, feature); + tile.featureTree.insert(feature.bbox(), feature.extent, feature.index, bucket.sourceLayerIndex, bucket.index); } } diff --git a/js/symbol/collision_tile.js b/js/symbol/collision_tile.js index c5748e84b55..127c276f062 100644 --- a/js/symbol/collision_tile.js +++ b/js/symbol/collision_tile.js @@ -18,9 +18,7 @@ module.exports = CollisionTile; */ function CollisionTile(angle, pitch, collisionBoxArray) { this.grid = new Grid(12, EXTENT, 6); - this.gridFeatures = []; this.ignoredGrid = new Grid(12, EXTENT, 0); - this.ignoredGridFeatures = []; this.angle = angle; @@ -159,12 +157,21 @@ CollisionTile.prototype.placeCollisionFeature = function(collisionFeature, allow return minPlacementScale; }; -CollisionTile.prototype.getFeaturesAt = function(queryBox, scale) { - var features = []; +CollisionTile.prototype.queryRenderedSymbols = function(minX, minY, maxX, maxY, scale) { + var sourceLayerFeatures = {}; var result = []; var rotationMatrix = this.rotationMatrix; - var anchorPoint = queryBox.anchorPoint.matMult(rotationMatrix); + var anchorPoint = new Point(minX, minY)._matMult(rotationMatrix); + + var queryBox = this.tempCollisionBox; + queryBox.anchorX = anchorPoint.x; + queryBox.anchorY = anchorPoint.y; + queryBox.x1 = 0; + queryBox.y1 = 0; + queryBox.x2 = maxX - minX; + queryBox.y2 = maxY - minY; + queryBox.maxScale = scale; var searchBox = [ anchorPoint.x + queryBox.x1 / scale, @@ -173,27 +180,27 @@ CollisionTile.prototype.getFeaturesAt = function(queryBox, scale) { anchorPoint.y + queryBox.y2 / scale * this.yStretch ]; - var blockingBoxes = []; - var blockingBoxKeys = this.grid.query(searchBox); - for (var j = 0; j < blockingBoxKeys.length; j++) { - blockingBoxes.push(this.gridFeatures[blockingBoxKeys[j]]); - } - blockingBoxKeys = this.ignoredGrid.query(searchBox); - for (var k = 0; k < blockingBoxKeys.length; k++) { - blockingBoxes.push(this.ignoredGridFeatures[blockingBoxKeys[k]]); + var blockingBoxKeys = this.grid.query(searchBox[0], searchBox[1], searchBox[2], searchBox[3]); + var blockingBoxKeys2 = this.ignoredGrid.query(searchBox[0], searchBox[1], searchBox[2], searchBox[3]); + for (var k = 0; k < blockingBoxKeys2.length; k++) { + blockingBoxKeys.push(blockingBoxKeys2[k]); } - for (var i = 0; i < blockingBoxes.length; i++) { - var blocking = blockingBoxes[i]; + var blocking = this.collisionBoxArray._struct; + for (var i = 0; i < blockingBoxKeys.length; i++) { + blocking._setIndex(blockingBoxKeys[i]); var blockingAnchorPoint = blocking.anchorPoint.matMult(rotationMatrix); var minPlacementScale = this.getPlacementScale(this.minScale, anchorPoint, queryBox, blockingAnchorPoint, blocking); if (minPlacementScale >= scale) { - if (features.indexOf(blocking.feature) < 0) { - features.push(blocking.feature); - result.push({ - feature: blocking.feature, - layerIDs: blocking.layerIDs - }); + + var sourceLayer = blocking.sourceLayerIndex; + if (sourceLayerFeatures[sourceLayer] === undefined) { + sourceLayerFeatures[sourceLayer] = {}; + } + + if (!sourceLayerFeatures[sourceLayer][blocking.featureIndex]) { + sourceLayerFeatures[sourceLayer][blocking.featureIndex] = true; + result.push(blockingBoxKeys[i]); } } } diff --git a/js/util/string_number_mapping.js b/js/util/string_number_mapping.js new file mode 100644 index 00000000000..59388409d0c --- /dev/null +++ b/js/util/string_number_mapping.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = StringNumberMapping; + +function StringNumberMapping(strings) { + this.stringToNumber = {}; + this.numberToString = []; + for (var i = 0; i < strings.length; i++) { + var string = strings[i]; + this.stringToNumber[string] = i; + this.numberToString[i] = string; + } +} From 3e45db39038fdd059034be0480e26eb1f14e602f Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 1 Mar 2016 17:21:28 -0800 Subject: [PATCH 05/48] optimize queryRenderedFeatures --- js/data/feature_tree.js | 25 +++++++++++++++++++------ js/symbol/collision_tile.js | 19 ++++++++++--------- js/util/grid.js | 19 +++++++++++++++---- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index 7ce8e63171a..1c79447ed7a 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -124,18 +124,17 @@ FeatureTree.prototype.query = function(args, styleLayersByID) { function filterMatching(matching, match) { for (var k = 0; k < matching.length; k++) { match._setIndex(matching[k]); + + var layerIDs = this.numberToLayerIDs[match.bucketIndex]; + if (params.layerIds && !matchLayers(params.layerIds, layerIDs)) continue; + var sourceLayerName = this.sourceLayerNumberMapping.numberToString[match.sourceLayerIndex]; var sourceLayer = this.vtLayers[sourceLayerName]; var feature = sourceLayer.feature(match.featureIndex); - var layerIDs = this.numberToLayerIDs[match.bucketIndex]; if (!filter(feature)) continue; - var geoJSON = feature.toGeoJSON(this.x, this.y, this.z); - - if (!params.includeGeometry) { - geoJSON.geometry = null; - } + var geoJSON = null; for (var l = 0; l < layerIDs.length; l++) { var layerID = layerIDs[l]; @@ -169,6 +168,13 @@ FeatureTree.prototype.query = function(args, styleLayersByID) { if (!polygonIntersectsBufferedMultiPoint(translatedPolygon, geometry, circleRadius)) continue; } + if (!geoJSON) { + geoJSON = feature.toGeoJSON(this.x, this.y, this.z); + if (!params.includeGeometry) { + geoJSON.geometry = null; + } + } + result.push(util.extend({layer: layerID}, geoJSON)); } } @@ -195,6 +201,13 @@ FeatureTree.prototype.query = function(args, styleLayersByID) { return result; }; +function matchLayers(filterLayerIDs, featureLayerIDs) { + for (var l = 0; l < featureLayerIDs.length; l++) { + if (filterLayerIDs.indexOf(featureLayerIDs[l]) >= 0) return true; + } + return false; +} + function offsetLine(rings, offset) { var newRings = []; var zero = new Point(0, 0); diff --git a/js/symbol/collision_tile.js b/js/symbol/collision_tile.js index 127c276f062..8dd549c1838 100644 --- a/js/symbol/collision_tile.js +++ b/js/symbol/collision_tile.js @@ -189,17 +189,18 @@ CollisionTile.prototype.queryRenderedSymbols = function(minX, minY, maxX, maxY, var blocking = this.collisionBoxArray._struct; for (var i = 0; i < blockingBoxKeys.length; i++) { blocking._setIndex(blockingBoxKeys[i]); - var blockingAnchorPoint = blocking.anchorPoint.matMult(rotationMatrix); - var minPlacementScale = this.getPlacementScale(this.minScale, anchorPoint, queryBox, blockingAnchorPoint, blocking); - if (minPlacementScale >= scale) { - var sourceLayer = blocking.sourceLayerIndex; - if (sourceLayerFeatures[sourceLayer] === undefined) { - sourceLayerFeatures[sourceLayer] = {}; - } + var sourceLayer = blocking.sourceLayerIndex; + var featureIndex = blocking.featureIndex; + if (sourceLayerFeatures[sourceLayer] === undefined) { + sourceLayerFeatures[sourceLayer] = {}; + } - if (!sourceLayerFeatures[sourceLayer][blocking.featureIndex]) { - sourceLayerFeatures[sourceLayer][blocking.featureIndex] = true; + if (!sourceLayerFeatures[sourceLayer][featureIndex]) { + var blockingAnchorPoint = blocking.anchorPoint.matMult(rotationMatrix); + var minPlacementScale = this.getPlacementScale(this.minScale, anchorPoint, queryBox, blockingAnchorPoint, blocking); + if (minPlacementScale >= scale) { + sourceLayerFeatures[sourceLayer][featureIndex] = true; result.push(blockingBoxKeys[i]); } } diff --git a/js/util/grid.js b/js/util/grid.js index b64b01e2ffd..e66ffa35087 100644 --- a/js/util/grid.js +++ b/js/util/grid.js @@ -35,6 +35,10 @@ function Grid(n, extent, padding) { this.padding = padding; this.scale = n / extent; this.uid = 0; + + var p = (padding / n) * extent; + this.min = -p; + this.max = extent + p; } @@ -52,10 +56,17 @@ Grid.prototype._insertCell = function(x1, y1, x2, y2, cellIndex, uid) { }; Grid.prototype.query = function(x1, y1, x2, y2) { - var result = []; - var seenUids = {}; - this._forEachCell(x1, y1, x2, y2, this._queryCell, result, seenUids); - return result; + var min = this.min; + var max = this.max; + if (x1 <= min && y1 <= min && max <= x2 && max <= y2) { + return this.keys.slice(); + + } else { + var result = []; + var seenUids = {}; + this._forEachCell(x1, y1, x2, y2, this._queryCell, result, seenUids); + return result; + } }; Grid.prototype._queryCell = function(x1, y1, x2, y2, cellIndex, result, seenUids) { From 16655f707c3d4a49fde01b5f7712e6bf7e10025b Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 1 Mar 2016 17:49:50 -0800 Subject: [PATCH 06/48] use grid index instead of rtree for querying features Why? Both indexes have comparable querying performance and Grid is transferrable. Insertion is faster with Grid than rbush but that's not the main benefit. Point queries are really fast for both. Each is sometimes faster than the other. Therre is no clear winner. Large, full-screen bbox queries are 10% slower with Grid than rbush but difference is not significant (0.2% of FeatureTree.query). comparisons were done with mapbox-streets-v6 --- js/data/feature_tree.js | 21 +-------------------- package.json | 1 - 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index 1c79447ed7a..67fca6fb11a 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -1,6 +1,5 @@ 'use strict'; -var rbush = require('rbush'); var Point = require('point-geometry'); var util = require('../util/util'); var loadGeometry = require('./load_geometry'); @@ -25,7 +24,6 @@ function FeatureTree(coord, overscaling, collisionTile, vtLayers) { this.x = coord.x; this.y = coord.y; this.z = coord.z - Math.log(overscaling) / Math.LN2; - this.rtree = rbush(9); this.toBeInserted = []; this.grid = new Grid(16, EXTENT, 0); this.featureIndexArray = new FeatureIndexArray(); @@ -40,19 +38,8 @@ FeatureTree.prototype.insert = function(bbox, extent, featureIndex, sourceLayerI bbox[1] *= scale; bbox[2] *= scale; bbox[3] *= scale; - bbox.key = this.featureIndexArray.length; + this.grid.insert(this.featureIndexArray.length, bbox[0], bbox[1], bbox[2], bbox[3]); this.featureIndexArray.emplaceBack(featureIndex, sourceLayerIndex, bucketIndex); - this.toBeInserted.push(bbox); -}; - -// bulk insert into tree -FeatureTree.prototype._load = function() { - this.rtree.load(this.toBeInserted); - for (var i = 0; i < this.toBeInserted.length; i++) { - var bbox = this.toBeInserted[i]; - this.grid.insert(i, bbox[0], bbox[1], bbox[2], bbox[3]); - } - this.toBeInserted = []; }; FeatureTree.prototype.setCollisionTile = function(collisionTile) { @@ -66,7 +53,6 @@ function translateDistance(translate) { // Finds features in this tile at a particular position. FeatureTree.prototype.query = function(args, styleLayersByID) { if (!this.vtLayers) return []; - if (this.toBeInserted.length) this._load(); var params = args.params || {}, pixelsToTileUnits = EXTENT / args.tileSize / args.scale, @@ -108,15 +94,10 @@ FeatureTree.prototype.query = function(args, styleLayersByID) { maxY = Math.max(maxY, p.y); } - var bounds = [minX - additionalRadius, minY - additionalRadius, maxX + additionalRadius, maxY + additionalRadius]; - var treeMatching = this.rtree.search(bounds); - var matching = this.grid.query(minX - additionalRadius, minY - additionalRadius, maxX + additionalRadius, maxY + additionalRadius); var match = this.featureIndexArray.at(0); filterMatching.call(this, matching, match); - if (matching.length !== treeMatching.length) throw Error("asdf"); - var matchingSymbols = this.collisionTile.queryRenderedSymbols(minX, minY, maxX, maxY, args.scale); var match2 = this.collisionTile.collisionBoxArray.at(0); filterMatching.call(this, matchingSymbols, match2); diff --git a/package.json b/package.json index 0df8485835b..66958c3a371 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "pbf": "^1.3.2", "pngjs": "^2.2.0", "point-geometry": "^0.0.0", - "rbush": "^1.4.0", "request": "^2.39.0", "resolve-url": "^0.2.1", "shelf-pack": "^0.0.1", From 9bd9b0a683f38a00dfa63cb99b391cf02944ad0c Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 1 Mar 2016 18:55:23 -0800 Subject: [PATCH 07/48] move queryRenderedFeatures to the main thread - it's fast - this helps avoid worker postMessage clone costs - this prevents busy workers from delaying the results - this is a step towards lazily constructing GeoJSON and eliminating `includeGeometry` --- js/data/feature_tree.js | 58 ++++++++++++++++++++++++++++++------- js/source/source.js | 33 +++++++++++---------- js/source/tile.js | 2 ++ js/source/worker.js | 2 +- js/source/worker_tile.js | 10 +++++-- js/symbol/collision_tile.js | 25 ++++++++++++++-- 6 files changed, 99 insertions(+), 31 deletions(-) diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index 67fca6fb11a..76fabaffa2f 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -8,6 +8,9 @@ var featureFilter = require('feature-filter'); var createStructArrayType = require('../util/struct_array'); var Grid = require('../util/grid'); var StringNumberMapping = require('../util/string_number_mapping'); +var CollisionTile = require('../symbol/collision_tile'); +var vt = require('vector-tile'); +var Protobuf = require('pbf'); var FeatureIndexArray = createStructArrayType([ // the index of the feature in the original vectortile @@ -20,16 +23,26 @@ var FeatureIndexArray = createStructArrayType([ module.exports = FeatureTree; -function FeatureTree(coord, overscaling, collisionTile, vtLayers) { +function FeatureTree(coord, overscaling, collisionTile) { + if (coord.grid) { + var serialized = coord; + coord = serialized.coord; + overscaling = serialized.overscaling; + collisionTile = new CollisionTile(serialized.collisionTile); + this.grid = new Grid(serialized.grid); + this.featureIndexArray = new FeatureIndexArray(serialized.featureIndexArray); + this.rawTileData = serialized.rawTileData; + this.numberToLayerIDs = serialized.numberToLayerIDs; + } else { + this.grid = new Grid(16, EXTENT, 0); + this.featureIndexArray = new FeatureIndexArray(); + } + this.coord = coord; + this.overscaling = overscaling; this.x = coord.x; this.y = coord.y; this.z = coord.z - Math.log(overscaling) / Math.LN2; - this.toBeInserted = []; - this.grid = new Grid(16, EXTENT, 0); - this.featureIndexArray = new FeatureIndexArray(); this.setCollisionTile(collisionTile); - this.vtLayers = vtLayers; - this.sourceLayerNumberMapping = new StringNumberMapping(vtLayers ? Object.keys(vtLayers).sort() : []); } FeatureTree.prototype.insert = function(bbox, extent, featureIndex, sourceLayerIndex, bucketIndex) { @@ -46,18 +59,43 @@ FeatureTree.prototype.setCollisionTile = function(collisionTile) { this.collisionTile = collisionTile; }; +FeatureTree.prototype.serialize = function() { + var collisionTile = this.collisionTile.serialize(); + var data = { + coord: this.coord, + overscaling: this.overscaling, + collisionTile: collisionTile, + grid: this.grid.toArrayBuffer(), + featureIndexArray: this.featureIndexArray.arrayBuffer, + numberToLayerIDs: this.numberToLayerIDs + }; + return { + data: data, + transferables: [ + collisionTile.collisionBoxArray, + collisionTile.grid, + collisionTile.ignoredGrid, + data.grid, + data.featureIndexArray + ] + }; +}; + 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, styleLayersByID) { - if (!this.vtLayers) return []; +FeatureTree.prototype.query = function(result, args, styleLayersByID) { + if (!this.vtLayers) { + if (!this.rawTileData) return []; + this.vtLayers = new vt.VectorTile(new Protobuf(new Uint8Array(this.rawTileData))).layers; + this.sourceLayerNumberMapping = new StringNumberMapping(this.vtLayers ? Object.keys(this.vtLayers).sort() : []); + } var params = args.params || {}, pixelsToTileUnits = EXTENT / args.tileSize / args.scale, - filter = featureFilter(params.filter), - result = []; + filter = featureFilter(params.filter); // 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 diff --git a/js/source/source.js b/js/source/source.js index c3bdeddb780..84e590e8de1 100644 --- a/js/source/source.js +++ b/js/source/source.js @@ -71,25 +71,28 @@ exports._queryRenderedVectorFeatures = function(queryGeometry, params, classes, if (!this._pyramid) return callback(null, []); - var results = this._pyramid.tilesIn(queryGeometry); - if (!results) + var tilesIn = this._pyramid.tilesIn(queryGeometry); + if (!tilesIn) return callback(null, []); - util.asyncAll(results, function queryTile(result, cb) { - this.dispatcher.send('query rendered features', { - uid: result.tile.uid, - source: this.id, - queryGeometry: result.queryGeometry, - scale: result.scale, - tileSize: result.tile.tileSize, - classes: classes, - zoom: zoom, + var styleLayers = this.map.style._layers; + var features = []; + + for (var r = 0; r < tilesIn.length; r++) { + var tileIn = tilesIn[r]; + if (!tileIn.tile.loaded || !tileIn.tile.featureTree) continue; + tileIn.tile.featureTree.query(features, { + queryGeometry: tileIn.queryGeometry, + scale: tileIn.scale, + tileSize: tileIn.tile.tileSize, bearing: bearing, params: params - }, cb, result.tile.workerID); - }.bind(this), function done(err, features) { - callback(err, Array.prototype.concat.apply([], features)); - }); + }, styleLayers); + } + + setTimeout(function() { + callback(null, features); + }, 0); }; exports._querySourceFeatures = function(params, callback) { diff --git a/js/source/tile.js b/js/source/tile.js index 2bbb40bd98d..c0a4cb14df3 100644 --- a/js/source/tile.js +++ b/js/source/tile.js @@ -2,6 +2,7 @@ var util = require('../util/util'); var Bucket = require('../data/bucket'); +var FeatureTree = require('../data/feature_tree'); module.exports = Tile; @@ -56,6 +57,7 @@ Tile.prototype = { // empty GeoJSON tile if (!data) return; + this.featureTree = new FeatureTree(data.featureTree); this.buckets = unserializeBuckets(data.buckets); }, diff --git a/js/source/worker.js b/js/source/worker.js index 1584f4e63b5..8dbd616aa9b 100644 --- a/js/source/worker.js +++ b/js/source/worker.js @@ -80,7 +80,7 @@ util.extend(Worker.prototype, { if (err) return callback(err); tile.data = new vt.VectorTile(new Protobuf(new Uint8Array(data))); - tile.parse(tile.data, this.layers, this.actor, callback); + tile.parse(tile.data, this.layers, this.actor, callback, data); this.loaded[source] = this.loaded[source] || {}; this.loaded[source][uid] = tile; diff --git a/js/source/worker_tile.js b/js/source/worker_tile.js index 6478292e681..b891964ba61 100644 --- a/js/source/worker_tile.js +++ b/js/source/worker_tile.js @@ -21,7 +21,7 @@ function WorkerTile(params) { this.showCollisionBoxes = params.showCollisionBoxes; } -WorkerTile.prototype.parse = function(data, layers, actor, callback) { +WorkerTile.prototype.parse = function(data, layers, actor, callback, rawTileData) { this.status = 'parsing'; this.data = data; @@ -202,10 +202,14 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback) { tile.redoPlacementAfterDone = false; } + var featureTree = tile.featureTree.serialize(); + featureTree.data.rawTileData = rawTileData; + callback(null, { buckets: buckets.filter(isBucketEmpty).map(serializeBucket), - bucketStats: stats // TODO put this in a separate message? - }, getTransferables(buckets)); + bucketStats: stats, // TODO put this in a separate message? + featureTree: featureTree.data + }, getTransferables(buckets).concat(featureTree.transferables.concat(rawTileData))); } }; diff --git a/js/symbol/collision_tile.js b/js/symbol/collision_tile.js index 8dd549c1838..005ce8a2dd8 100644 --- a/js/symbol/collision_tile.js +++ b/js/symbol/collision_tile.js @@ -3,6 +3,7 @@ var Point = require('point-geometry'); var EXTENT = require('../data/bucket').EXTENT; var Grid = require('../util/grid'); +var CollisionBox = require('../symbol/collision_box'); module.exports = CollisionTile; @@ -17,10 +18,20 @@ module.exports = CollisionTile; * @private */ function CollisionTile(angle, pitch, collisionBoxArray) { - this.grid = new Grid(12, EXTENT, 6); - this.ignoredGrid = new Grid(12, EXTENT, 0); + if (typeof angle === 'object') { + var serialized = angle; + angle = serialized.angle; + pitch = serialized.pitch; + collisionBoxArray = new CollisionBox(serialized.collisionBoxArray); + this.grid = new Grid(serialized.grid); + this.ignoredGrid = new Grid(serialized.ignoredGrid); + } else { + this.grid = new Grid(12, EXTENT, 6); + this.ignoredGrid = new Grid(12, EXTENT, 0); + } this.angle = angle; + this.pitch = pitch; var sin = Math.sin(angle), cos = Math.cos(angle); @@ -69,6 +80,16 @@ function CollisionTile(angle, pitch, collisionBoxArray) { ]; } +CollisionTile.prototype.serialize = function() { + return { + angle: this.angle, + pitch: this.pitch, + collisionBoxArray: this.collisionBoxArray.arrayBuffer.slice(), + grid: this.grid.toArrayBuffer(), + ignoredGrid: this.ignoredGrid.toArrayBuffer() + }; +}; + CollisionTile.prototype.minScale = 0.25; CollisionTile.prototype.maxScale = 2; From 571a8e73e1bb1059989e497b80e611ee0b4edd90 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 2 Mar 2016 00:13:02 -0800 Subject: [PATCH 08/48] index each line/ring of a geometry separately This improves performance for large multipolygons and multilines. mapbox-streets-v7 collects features with identical properties and combines into a single giant feature. This saves space and speeds up parsing but it also makes indexing by feature bbox useless. --- js/data/feature_tree.js | 35 +++++++++++++++++++++++++++-------- js/source/worker_tile.js | 2 +- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index 76fabaffa2f..4d517feff9a 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -45,14 +45,27 @@ function FeatureTree(coord, overscaling, collisionTile) { this.setCollisionTile(collisionTile); } -FeatureTree.prototype.insert = function(bbox, extent, featureIndex, sourceLayerIndex, bucketIndex) { - var scale = EXTENT / extent; - bbox[0] *= scale; - bbox[1] *= scale; - bbox[2] *= scale; - bbox[3] *= scale; - this.grid.insert(this.featureIndexArray.length, bbox[0], bbox[1], bbox[2], bbox[3]); +FeatureTree.prototype.insert = function(feature, featureIndex, sourceLayerIndex, bucketIndex) { + var key = this.featureIndexArray.length; this.featureIndexArray.emplaceBack(featureIndex, sourceLayerIndex, bucketIndex); + var geometry = loadGeometry(feature); + + for (var r = 0; r < geometry.length; r++) { + var ring = geometry[r]; + + // TODO: skip holes when we start using vector tile spec 2.0 + + var bbox = [Infinity, Infinity, -Infinity, -Infinity]; + for (var i = 0; i < ring.length; i++) { + var p = ring[i]; + bbox[0] = Math.min(bbox[0], p.x); + bbox[1] = Math.min(bbox[1], p.y); + bbox[2] = Math.max(bbox[2], p.x); + bbox[3] = Math.max(bbox[3], p.y); + } + + this.grid.insert(key, bbox[0], bbox[1], bbox[2], bbox[3]); + } }; FeatureTree.prototype.setCollisionTile = function(collisionTile) { @@ -141,8 +154,14 @@ FeatureTree.prototype.query = function(result, args, styleLayersByID) { filterMatching.call(this, matchingSymbols, match2); function filterMatching(matching, match) { + var seen = {}; for (var k = 0; k < matching.length; k++) { - match._setIndex(matching[k]); + var index = matching[k]; + + if (seen[index]) continue; + seen[index] = true; + + match._setIndex(index); var layerIDs = this.numberToLayerIDs[match.bucketIndex]; if (params.layerIds && !matchLayers(params.layerIds, layerIDs)) continue; diff --git a/js/source/worker_tile.js b/js/source/worker_tile.js index b891964ba61..5e25692962f 100644 --- a/js/source/worker_tile.js +++ b/js/source/worker_tile.js @@ -184,7 +184,7 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback, rawTileData if (bucket.type !== 'symbol') { for (var i = 0; i < bucket.features.length; i++) { var feature = bucket.features[i]; - tile.featureTree.insert(feature.bbox(), feature.extent, feature.index, bucket.sourceLayerIndex, bucket.index); + tile.featureTree.insert(feature, feature.index, bucket.sourceLayerIndex, bucket.index); } } From dd4f2a03f4e3d64df490fa389ee19df1044ba386 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 2 Mar 2016 00:31:16 -0800 Subject: [PATCH 09/48] don't load geometries for symbol properties --- js/data/feature_tree.js | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index 4d517feff9a..cdd60c2e6d2 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -182,28 +182,30 @@ FeatureTree.prototype.query = function(result, args, styleLayersByID) { } styleLayer = styleLayersByID[layerID]; - var geometry = loadGeometry(feature); var translatedPolygon; - if (styleLayer.type === 'symbol') { + if (styleLayer.type !== 'symbol') { // all symbols already match the style - } else if (styleLayer.type === 'line') { - translatedPolygon = 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 (!polygonIntersectsBufferedMultiLine(translatedPolygon, geometry, halfWidth)) continue; + var geometry = loadGeometry(feature); + + if (styleLayer.type === 'line') { + translatedPolygon = 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 (!polygonIntersectsBufferedMultiLine(translatedPolygon, geometry, halfWidth)) continue; - } else if (styleLayer.type === 'fill') { - translatedPolygon = translate(styleLayer.paint['fill-translate'], styleLayer.paint['fill-translate-anchor']); - if (!polygonIntersectsMultiPolygon(translatedPolygon, geometry)) continue; + } else if (styleLayer.type === 'fill') { + translatedPolygon = translate(styleLayer.paint['fill-translate'], styleLayer.paint['fill-translate-anchor']); + if (!polygonIntersectsMultiPolygon(translatedPolygon, geometry)) continue; - } else if (styleLayer.type === 'circle') { - translatedPolygon = translate(styleLayer.paint['circle-translate'], styleLayer.paint['circle-translate-anchor']); - var circleRadius = styleLayer.paint['circle-radius'] * pixelsToTileUnits; - if (!polygonIntersectsBufferedMultiPoint(translatedPolygon, geometry, circleRadius)) continue; + } else if (styleLayer.type === 'circle') { + translatedPolygon = translate(styleLayer.paint['circle-translate'], styleLayer.paint['circle-translate-anchor']); + var circleRadius = styleLayer.paint['circle-radius'] * pixelsToTileUnits; + if (!polygonIntersectsBufferedMultiPoint(translatedPolygon, geometry, circleRadius)) continue; + } } if (!geoJSON) { From e72a46745f2d5a6bff77c8b5b52a63750342dafd Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 2 Mar 2016 11:51:50 -0800 Subject: [PATCH 10/48] remove `includeGeometry` and lazily build geojson geometry The code for converting to geojson is copied from https://github.com/mapbox/vector-tile-js 0f73eb2c417979601be973e046e169c74147ca7 --- js/data/feature_tree.js | 15 ++----- js/ui/map.js | 1 - js/util/vectortile_to_geojson.js | 71 ++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 js/util/vectortile_to_geojson.js diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index cdd60c2e6d2..5ecd7fe0149 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -1,7 +1,6 @@ 'use strict'; var Point = require('point-geometry'); -var util = require('../util/util'); var loadGeometry = require('./load_geometry'); var EXTENT = require('./bucket').EXTENT; var featureFilter = require('feature-filter'); @@ -11,6 +10,7 @@ var StringNumberMapping = require('../util/string_number_mapping'); var CollisionTile = require('../symbol/collision_tile'); var vt = require('vector-tile'); var Protobuf = require('pbf'); +var GeoJSONFeature = require('../util/vectortile_to_geojson'); var FeatureIndexArray = createStructArrayType([ // the index of the feature in the original vectortile @@ -172,8 +172,6 @@ FeatureTree.prototype.query = function(result, args, styleLayersByID) { if (!filter(feature)) continue; - var geoJSON = null; - for (var l = 0; l < layerIDs.length; l++) { var layerID = layerIDs[l]; @@ -208,14 +206,9 @@ FeatureTree.prototype.query = function(result, args, styleLayersByID) { } } - if (!geoJSON) { - geoJSON = feature.toGeoJSON(this.x, this.y, this.z); - if (!params.includeGeometry) { - geoJSON.geometry = null; - } - } - - result.push(util.extend({layer: layerID}, geoJSON)); + var geojsonFeature = new GeoJSONFeature(feature, this.z, this.x, this.y); + geojsonFeature.layer = layerID; + result.push(geojsonFeature); } } } diff --git a/js/ui/map.js b/js/ui/map.js index 495c372b249..550eebf3291 100644 --- a/js/ui/map.js +++ b/js/ui/map.js @@ -384,7 +384,6 @@ util.extend(Map.prototype, /** @lends Map.prototype */{ * @param {Point|Array|Array|Array>} [pointOrBox] Either [x, y] pixel coordinates of a point, or [[x1, y1], [x2, y2]] pixel coordinates of opposite corners of bounding rectangle. Optional: use entire viewport if omitted. * @param {Object} params * @param {string|Array} [params.layer] Only return features from a given layer or layers - * @param {boolean} [params.includeGeometry=false] If `true`, geometry of features will be included in the results at the expense of a much slower query time. * @param {Array} [params.filter] A mapbox-gl-style-spec filter. * @param {featuresCallback} callback function that receives the results * diff --git a/js/util/vectortile_to_geojson.js b/js/util/vectortile_to_geojson.js new file mode 100644 index 00000000000..f41eb62a9f9 --- /dev/null +++ b/js/util/vectortile_to_geojson.js @@ -0,0 +1,71 @@ +'use strict'; + +var VectorTileFeature = require('vector-tile').VectorTileFeature; + +module.exports = Feature; + +function Feature(vectorTileFeature, z, x, y) { + this._vectorTileFeature = vectorTileFeature; + this._z = z; + this._x = x; + this._y = y; + + this.properties = vectorTileFeature.properties; + + if (vectorTileFeature._id) { + this.id = vectorTileFeature._id; + } +} + +Feature.prototype = { + type: "Feature", + + get geometry() { + if (!this._geometry) { + var feature = this._vectorTileFeature; + var coords = projectCoords( + feature.loadGeometry(), + feature.extent, + this._z, this._x, this._y); + + var type = VectorTileFeature.types[feature.type]; + + if (type === 'Point' && coords.length === 1) { + coords = coords[0][0]; + } else if (type === 'Point') { + coords = coords[0]; + type = 'MultiPoint'; + } else if (type === 'LineString' && coords.length === 1) { + coords = coords[0]; + } else if (type === 'LineString') { + type = 'MultiLineString'; + } + + this._geometry = { + type: type, + coordinates: coords + }; + + this._vectorTileFeature = null; + } + return this._geometry; + } +}; + +function projectCoords(coords, extent, z, x, y) { + var size = extent * Math.pow(2, z), + x0 = extent * x, + y0 = extent * y; + for (var i = 0; i < coords.length; i++) { + var line = coords[i]; + for (var j = 0; j < line.length; j++) { + var p = line[j]; + var y2 = 180 - (p.y + y0) * 360 / size; + line[j] = [ + (p.x + x0) * 360 / size - 180, + 360 / Math.PI * Math.atan(Math.exp(y2 * Math.PI / 180)) - 90 + ]; + } + } + return coords; +} From 3da7cd46265b630fc9e833fb6e2a743629baa096 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 2 Mar 2016 13:37:13 -0800 Subject: [PATCH 11/48] optimize feature query filtering --- js/data/feature_tree.js | 159 ++++++++++++++++++++++------------------ 1 file changed, 89 insertions(+), 70 deletions(-) diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index 5ecd7fe0149..5e85000fa8e 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -114,9 +114,8 @@ FeatureTree.prototype.query = function(result, args, styleLayersByID) { // 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 styleLayer = styleLayersByID[id]; var styleLayerDistance = 0; if (styleLayer.type === 'line') { @@ -147,92 +146,98 @@ FeatureTree.prototype.query = function(result, args, styleLayersByID) { var matching = this.grid.query(minX - additionalRadius, minY - additionalRadius, maxX + additionalRadius, maxY + additionalRadius); var match = this.featureIndexArray.at(0); - filterMatching.call(this, matching, match); + this.filterMatching(result, matching, match, queryGeometry, filter, params.layerIds, styleLayersByID, args.bearing, pixelsToTileUnits); var matchingSymbols = this.collisionTile.queryRenderedSymbols(minX, minY, maxX, maxY, args.scale); var match2 = this.collisionTile.collisionBoxArray.at(0); - filterMatching.call(this, matchingSymbols, match2); + this.filterMatching(result, matchingSymbols, match2, queryGeometry, filter, params.layerIds, styleLayersByID, args.bearing, pixelsToTileUnits); - function filterMatching(matching, match) { - var seen = {}; - for (var k = 0; k < matching.length; k++) { - var index = matching[k]; - - if (seen[index]) continue; - seen[index] = true; + return result; +}; - match._setIndex(index); +FeatureTree.prototype.filterMatching = function(result, matching, match, queryGeometry, filter, filterLayerIDs, styleLayersByID, bearing, pixelsToTileUnits) { + var seen = {}; + for (var k = 0; k < matching.length; k++) { + var index = matching[k]; - var layerIDs = this.numberToLayerIDs[match.bucketIndex]; - if (params.layerIds && !matchLayers(params.layerIds, layerIDs)) continue; + if (seen[index]) continue; + seen[index] = true; - var sourceLayerName = this.sourceLayerNumberMapping.numberToString[match.sourceLayerIndex]; - var sourceLayer = this.vtLayers[sourceLayerName]; - var feature = sourceLayer.feature(match.featureIndex); + match._setIndex(index); - if (!filter(feature)) continue; + var layerIDs = this.numberToLayerIDs[match.bucketIndex]; + if (filterLayerIDs && !matchLayers(filterLayerIDs, layerIDs)) continue; - for (var l = 0; l < layerIDs.length; l++) { - var layerID = layerIDs[l]; + var sourceLayerName = this.sourceLayerNumberMapping.numberToString[match.sourceLayerIndex]; + var sourceLayer = this.vtLayers[sourceLayerName]; + var feature = sourceLayer.feature(match.featureIndex); - if (params.layerIds && params.layerIds.indexOf(layerID) < 0) { - continue; - } + if (!filter(feature)) continue; - styleLayer = styleLayersByID[layerID]; + for (var l = 0; l < layerIDs.length; l++) { + var layerID = layerIDs[l]; - var translatedPolygon; - if (styleLayer.type !== 'symbol') { - // all symbols already match the style + if (filterLayerIDs && filterLayerIDs.indexOf(layerID) < 0) { + continue; + } - var geometry = loadGeometry(feature); + var styleLayer = styleLayersByID[layerID]; - if (styleLayer.type === 'line') { - translatedPolygon = 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 (!polygonIntersectsBufferedMultiLine(translatedPolygon, geometry, halfWidth)) continue; + var translatedPolygon; + if (styleLayer.type !== 'symbol') { + // all symbols already match the style - } else if (styleLayer.type === 'fill') { - translatedPolygon = translate(styleLayer.paint['fill-translate'], styleLayer.paint['fill-translate-anchor']); - if (!polygonIntersectsMultiPolygon(translatedPolygon, geometry)) continue; + var geometry = loadGeometry(feature); - } else if (styleLayer.type === 'circle') { - translatedPolygon = translate(styleLayer.paint['circle-translate'], styleLayer.paint['circle-translate-anchor']); - var circleRadius = styleLayer.paint['circle-radius'] * pixelsToTileUnits; - if (!polygonIntersectsBufferedMultiPoint(translatedPolygon, geometry, circleRadius)) continue; + if (styleLayer.type === 'line') { + translatedPolygon = translate(queryGeometry, + styleLayer.paint['line-translate'], styleLayer.paint['line-translate-anchor'], + bearing, pixelsToTileUnits); + var halfWidth = styleLayer.paint['line-width'] / 2 * pixelsToTileUnits; + if (styleLayer.paint['line-offset']) { + geometry = offsetLine(geometry, styleLayer.paint['line-offset'] * pixelsToTileUnits); } + if (!polygonIntersectsBufferedMultiLine(translatedPolygon, geometry, halfWidth)) continue; + + } else if (styleLayer.type === 'fill') { + translatedPolygon = translate(queryGeometry, + styleLayer.paint['fill-translate'], styleLayer.paint['fill-translate-anchor'], + bearing, pixelsToTileUnits); + if (!polygonIntersectsMultiPolygon(translatedPolygon, geometry)) continue; + + } else if (styleLayer.type === 'circle') { + translatedPolygon = translate(queryGeometry, + styleLayer.paint['circle-translate'], styleLayer.paint['circle-translate-anchor'], + bearing, pixelsToTileUnits); + var circleRadius = styleLayer.paint['circle-radius'] * pixelsToTileUnits; + if (!polygonIntersectsBufferedMultiPoint(translatedPolygon, geometry, circleRadius)) continue; } - - var geojsonFeature = new GeoJSONFeature(feature, this.z, this.x, this.y); - geojsonFeature.layer = layerID; - result.push(geojsonFeature); } - } - } - function translate(translate, translateAnchor) { - if (!translate[0] && !translate[1]) { - return queryGeometry; + var geojsonFeature = new GeoJSONFeature(feature, this.z, this.x, this.y); + geojsonFeature.layer = layerID; + result.push(geojsonFeature); } + } +}; - translate = Point.convert(translate); +function translate(queryGeometry, translate, translateAnchor, bearing, pixelsToTileUnits) { + if (!translate[0] && !translate[1]) { + return queryGeometry; + } - if (translateAnchor === "viewport") { - translate._rotate(-args.bearing); - } + translate = Point.convert(translate); - var translated = []; - for (var i = 0; i < queryGeometry.length; i++) { - translated.push(queryGeometry[i].sub(translate._mult(pixelsToTileUnits))); - } - return translated; + if (translateAnchor === "viewport") { + translate._rotate(-bearing); } - return result; -}; + var translated = []; + for (var i = 0; i < queryGeometry.length; i++) { + translated.push(queryGeometry[i].sub(translate._mult(pixelsToTileUnits))); + } + return translated; +} function matchLayers(filterLayerIDs, featureLayerIDs) { for (var l = 0; l < featureLayerIDs.length; l++) { @@ -266,12 +271,11 @@ function offsetLine(rings, offset) { } function polygonIntersectsBufferedMultiPoint(polygon, rings, radius) { - var multiPolygon = [polygon]; for (var i = 0; i < rings.length; i++) { var ring = rings[i]; for (var k = 0; k < ring.length; k++) { var point = ring[k]; - if (multiPolygonContainsPoint(multiPolygon, point)) return true; + if (polygonContainsPoint(polygon, point)) return true; if (pointIntersectsBufferedLine(point, polygon, radius)) return true; } } @@ -279,18 +283,22 @@ function polygonIntersectsBufferedMultiPoint(polygon, rings, radius) { } function polygonIntersectsMultiPolygon(polygon, multiPolygon) { - for (var i = 0; i < polygon.length; i++) { - if (multiPolygonContainsPoint(multiPolygon, polygon[i])) return true; + + if (polygon.length === 1) { + return multiPolygonContainsPoint(multiPolygon, polygon[0]); } - var polygon_ = [polygon]; for (var m = 0; m < multiPolygon.length; m++) { var ring = multiPolygon[m]; for (var n = 0; n < ring.length; n++) { - if (multiPolygonContainsPoint(polygon_, ring[n])) return true; + if (polygonContainsPoint(polygon, ring[n])) return true; } } + for (var i = 0; i < polygon.length; i++) { + if (multiPolygonContainsPoint(multiPolygon, polygon[i])) return true; + } + for (var k = 0; k < multiPolygon.length; k++) { if (lineIntersectsLine(polygon, multiPolygon[k])) return true; } @@ -298,12 +306,11 @@ function polygonIntersectsMultiPolygon(polygon, multiPolygon) { } function polygonIntersectsBufferedMultiLine(polygon, multiLine, radius) { - var multiPolygon = [polygon]; for (var i = 0; i < multiLine.length; i++) { var line = multiLine[i]; for (var k = 0; k < line.length; k++) { - if (multiPolygonContainsPoint(multiPolygon, line[k])) return true; + if (polygonContainsPoint(polygon, line[k])) return true; } if (lineIntersectsBufferedLine(polygon, line, radius)) return true; @@ -392,3 +399,15 @@ function multiPolygonContainsPoint(rings, p) { } return c; } + +function polygonContainsPoint(ring, p) { + var c = false; + for (var i = 0, j = ring.length - 1; i < ring.length; j = i++) { + var p1 = ring[i]; + var p2 = ring[j]; + if (((p1.y > p.y) !== (p2.y > p.y)) && (p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) { + c = !c; + } + } + return c; +} From 845e9d27cc233810a12acb334d0ae7321075ea6d Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 2 Mar 2016 15:36:02 -0800 Subject: [PATCH 12/48] move querySourceFeatures to main thread It's much faster to deserialize straight from pbf than to deserialize and reserialize as GeoJSON. This also lets us lazily load geometries to make it even faster for cases where not all geometries are used. benchmark with buildings in mapbox-streets-v7 old (with stringify added) 85ms new 1.5 ms new with all geometries loaded 15ms benchmark with buildings in mapbox-streets-v6 old 500ms old (with stringify added) 150ms new 3ms new with all geometries loaded 15ms --- js/data/feature_tree.js | 3 ++- js/source/source.js | 19 +++++-------------- js/source/tile.js | 34 ++++++++++++++++++++++++++++++++++ js/source/worker_tile.js | 4 ++-- 4 files changed, 43 insertions(+), 17 deletions(-) diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index 5e85000fa8e..0c7707ca701 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -26,12 +26,13 @@ module.exports = FeatureTree; function FeatureTree(coord, overscaling, collisionTile) { if (coord.grid) { var serialized = coord; + var rawTileData = overscaling; coord = serialized.coord; overscaling = serialized.overscaling; collisionTile = new CollisionTile(serialized.collisionTile); this.grid = new Grid(serialized.grid); this.featureIndexArray = new FeatureIndexArray(serialized.featureIndexArray); - this.rawTileData = serialized.rawTileData; + this.rawTileData = rawTileData; this.numberToLayerIDs = serialized.numberToLayerIDs; } else { this.grid = new Grid(16, EXTENT, 0); diff --git a/js/source/source.js b/js/source/source.js index 84e590e8de1..abc20c9b962 100644 --- a/js/source/source.js +++ b/js/source/source.js @@ -105,28 +105,19 @@ exports._querySourceFeatures = function(params, callback) { return pyramid.getTile(id); }); + var result = []; + var dataTiles = {}; for (var i = 0; i < tiles.length; i++) { var tile = tiles[i]; var dataID = new TileCoord(Math.min(tile.sourceMaxZoom, tile.coord.z), tile.coord.x, tile.coord.y, 0).id; if (!dataTiles[dataID]) { - dataTiles[dataID] = tile; + dataTiles[dataID] = true; + tile.querySourceFeatures(result, params); } } - util.asyncAll(Object.keys(dataTiles), function(dataID, callback) { - var tile = dataTiles[dataID]; - this.dispatcher.send('query source features', { - uid: tile.uid, - source: this.id, - params: params - }, callback, tile.workerID); - }.bind(this), function(err, results) { - callback(err, results.reduce(function(array, d) { - if (d) array = array.concat(d); - return array; - }, [])); - }); + callback(null, result); }; /* diff --git a/js/source/tile.js b/js/source/tile.js index c0a4cb14df3..1c6437c27e7 100644 --- a/js/source/tile.js +++ b/js/source/tile.js @@ -3,6 +3,10 @@ var util = require('../util/util'); var Bucket = require('../data/bucket'); var FeatureTree = require('../data/feature_tree'); +var vt = require('vector-tile'); +var Protobuf = require('pbf'); +var GeoJSONFeature = require('../util/vectortile_to_geojson'); +var featureFilter = require('feature-filter'); module.exports = Tile; @@ -58,6 +62,7 @@ Tile.prototype = { if (!data) return; this.featureTree = new FeatureTree(data.featureTree); + this.rawTileData = data.rawTileData; this.buckets = unserializeBuckets(data.buckets); }, @@ -132,8 +137,37 @@ Tile.prototype = { } }, +<<<<<<< HEAD getBucket: function(layer) { return this.buckets && this.buckets[layer.ref || layer.id]; +======= + getElementGroups: function(layer, shaderName) { + return this.elementGroups && this.elementGroups[layer.ref || layer.id] && this.elementGroups[layer.ref || layer.id][shaderName]; + }, + + querySourceFeatures: function(result, params) { + if (!this.rawTileData) return; + + if (!this.vtLayers) { + this.vtLayers = new vt.VectorTile(new Protobuf(new Uint8Array(this.rawTileData))).layers; + } + + var layer = this.vtLayers[params.sourceLayer]; + + if (!layer) return; + + var filter = featureFilter(params.filter); + var coord = { z: this.coord.z, x: this.coord.x, y: this.coord.y }; + + for (var i = 0; i < layer.length; i++) { + var feature = layer.feature(i); + if (filter(feature)) { + var geojsonFeature = new GeoJSONFeature(feature, this.coord.z, this.coord.x, this.coord.y); + geojsonFeature.tile = coord; + result.push(geojsonFeature); + } + } +>>>>>>> move querySourceFeatures to main thread } }; diff --git a/js/source/worker_tile.js b/js/source/worker_tile.js index 5e25692962f..4742d310ac1 100644 --- a/js/source/worker_tile.js +++ b/js/source/worker_tile.js @@ -203,12 +203,12 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback, rawTileData } var featureTree = tile.featureTree.serialize(); - featureTree.data.rawTileData = rawTileData; callback(null, { buckets: buckets.filter(isBucketEmpty).map(serializeBucket), bucketStats: stats, // TODO put this in a separate message? - featureTree: featureTree.data + featureTree: featureTree.data, + rawTileData: rawTileData }, getTransferables(buckets).concat(featureTree.transferables.concat(rawTileData))); } }; From 0bbd180c97dee26a4ee43eb35f8a150c28f15312 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 2 Mar 2016 18:01:22 -0800 Subject: [PATCH 13/48] remove StuctArray._struct --- js/symbol/collision_tile.js | 11 +++++++---- js/util/struct_array.js | 1 - 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/js/symbol/collision_tile.js b/js/symbol/collision_tile.js index 005ce8a2dd8..2fe55562166 100644 --- a/js/symbol/collision_tile.js +++ b/js/symbol/collision_tile.js @@ -78,6 +78,9 @@ function CollisionTile(angle, pitch, collisionBoxArray) { collisionBoxArray.at(3), collisionBoxArray.at(4) ]; + + this.box = collisionBoxArray.at(0); + this.blocking = collisionBoxArray.at(0); } CollisionTile.prototype.serialize = function() { @@ -108,8 +111,8 @@ CollisionTile.prototype.placeCollisionFeature = function(collisionFeature, allow var rotationMatrix = this.rotationMatrix; var yStretch = this.yStretch; var boxArray = this.collisionBoxArray; - var box = boxArray._struct; - var blocking = boxArray.at(0); + var box = this.box; + var blocking = this.blocking; for (var b = collisionFeature.boxStartIndex; b < collisionFeature.boxEndIndex; b++) { @@ -207,7 +210,7 @@ CollisionTile.prototype.queryRenderedSymbols = function(minX, minY, maxX, maxY, blockingBoxKeys.push(blockingBoxKeys2[k]); } - var blocking = this.collisionBoxArray._struct; + var blocking = this.blocking; for (var i = 0; i < blockingBoxKeys.length; i++) { blocking._setIndex(blockingBoxKeys[i]); @@ -285,7 +288,7 @@ CollisionTile.prototype.insertCollisionFeature = function(collisionFeature, minP var grid = ignorePlacement ? this.ignoredGrid : this.grid; - var box = this.collisionBoxArray._struct; + var box = this.box; for (var k = collisionFeature.boxStartIndex; k < collisionFeature.boxEndIndex; k++) { box._setIndex(k); box.placementScale = minPlacementScale; diff --git a/js/util/struct_array.js b/js/util/struct_array.js index c5e8f9c953b..b31ac1724c9 100644 --- a/js/util/struct_array.js +++ b/js/util/struct_array.js @@ -127,7 +127,6 @@ function StructArray(initialAllocatedLength) { } this.resize(initialAllocatedLength); } - this._struct = new this.StructType(this, 0); } StructArray.prototype.DEFAULT_ALLOCATED_LENGTH = 100; From 73a6e5788143cb116df8dd5ce37c29e4277f5fcf Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 2 Mar 2016 18:27:15 -0800 Subject: [PATCH 14/48] fix queryRenderedFeatures after rotation --- js/data/feature_tree.js | 12 +----------- js/source/tile.js | 14 ++++++++------ js/source/worker_tile.js | 29 ++++++++++++++++++----------- js/symbol/collision_tile.js | 11 ++++++----- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index 0c7707ca701..a28fa179856 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -7,7 +7,6 @@ var featureFilter = require('feature-filter'); var createStructArrayType = require('../util/struct_array'); var Grid = require('../util/grid'); var StringNumberMapping = require('../util/string_number_mapping'); -var CollisionTile = require('../symbol/collision_tile'); var vt = require('vector-tile'); var Protobuf = require('pbf'); var GeoJSONFeature = require('../util/vectortile_to_geojson'); @@ -29,7 +28,6 @@ function FeatureTree(coord, overscaling, collisionTile) { var rawTileData = overscaling; coord = serialized.coord; overscaling = serialized.overscaling; - collisionTile = new CollisionTile(serialized.collisionTile); this.grid = new Grid(serialized.grid); this.featureIndexArray = new FeatureIndexArray(serialized.featureIndexArray); this.rawTileData = rawTileData; @@ -74,24 +72,16 @@ FeatureTree.prototype.setCollisionTile = function(collisionTile) { }; FeatureTree.prototype.serialize = function() { - var collisionTile = this.collisionTile.serialize(); var data = { coord: this.coord, overscaling: this.overscaling, - collisionTile: collisionTile, grid: this.grid.toArrayBuffer(), featureIndexArray: this.featureIndexArray.arrayBuffer, numberToLayerIDs: this.numberToLayerIDs }; return { data: data, - transferables: [ - collisionTile.collisionBoxArray, - collisionTile.grid, - collisionTile.ignoredGrid, - data.grid, - data.featureIndexArray - ] + transferables: [data.grid, data.featureIndexArray] }; }; diff --git a/js/source/tile.js b/js/source/tile.js index 1c6437c27e7..44908b7e6e9 100644 --- a/js/source/tile.js +++ b/js/source/tile.js @@ -7,6 +7,8 @@ var vt = require('vector-tile'); var Protobuf = require('pbf'); var GeoJSONFeature = require('../util/vectortile_to_geojson'); var featureFilter = require('feature-filter'); +var CollisionTile = require('../symbol/collision_tile'); +var CollisionBoxArray = require('../symbol/collision_box'); module.exports = Tile; @@ -61,7 +63,9 @@ Tile.prototype = { // empty GeoJSON tile if (!data) return; - this.featureTree = new FeatureTree(data.featureTree); + this.collisionBoxArray = new CollisionBoxArray(data.collisionBoxArray); + this.collisionTile = new CollisionTile(data.collisionTile, this.collisionBoxArray); + this.featureTree = new FeatureTree(data.featureTree, data.rawTileData, this.collisionTile); this.rawTileData = data.rawTileData; this.buckets = unserializeBuckets(data.buckets); }, @@ -77,6 +81,9 @@ Tile.prototype = { reloadSymbolData: function(data, painter) { if (this.isUnloaded) return; + this.collisionTile = new CollisionTile(data.collisionTile, this.collisionBoxArray); + this.featureTree.setCollisionTile(this.collisionTile); + // Destroy and delete existing symbol buckets for (var id in this.buckets) { var bucket = this.buckets[id]; @@ -137,12 +144,8 @@ Tile.prototype = { } }, -<<<<<<< HEAD getBucket: function(layer) { return this.buckets && this.buckets[layer.ref || layer.id]; -======= - getElementGroups: function(layer, shaderName) { - return this.elementGroups && this.elementGroups[layer.ref || layer.id] && this.elementGroups[layer.ref || layer.id][shaderName]; }, querySourceFeatures: function(result, params) { @@ -167,7 +170,6 @@ Tile.prototype = { result.push(geojsonFeature); } } ->>>>>>> move querySourceFeatures to main thread } }; diff --git a/js/source/worker_tile.js b/js/source/worker_tile.js index 4742d310ac1..f02aafc2580 100644 --- a/js/source/worker_tile.js +++ b/js/source/worker_tile.js @@ -28,7 +28,7 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback, rawTileData this.collisionBoxArray = new CollisionBoxArray(); var collisionTile = new CollisionTile(this.angle, this.pitch, this.collisionBoxArray); - this.featureTree = new FeatureTree(this.coord, this.overscaling, collisionTile, data.layers); + var featureTree = new FeatureTree(this.coord, this.overscaling, collisionTile, data.layers); var sourceLayerNumberMapping = new StringNumberMapping(data.layers ? Object.keys(data.layers).sort() : []); var stats = { _total: 0 }; @@ -105,14 +105,14 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback, rawTileData symbolBuckets = this.symbolBuckets = [], otherBuckets = []; - this.featureTree.numberToLayerIDs = []; + featureTree.numberToLayerIDs = []; for (var id in bucketsById) { bucket = bucketsById[id]; if (bucket.features.length === 0) continue; - bucket.index = this.featureTree.numberToLayerIDs.length; - this.featureTree.numberToLayerIDs.push(bucket.layerIDs); + bucket.index = featureTree.numberToLayerIDs.length; + featureTree.numberToLayerIDs.push(bucket.layerIDs); buckets.push(bucket); @@ -184,7 +184,7 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback, rawTileData if (bucket.type !== 'symbol') { for (var i = 0; i < bucket.features.length; i++) { var feature = bucket.features[i]; - tile.featureTree.insert(feature, feature.index, bucket.sourceLayerIndex, bucket.index); + featureTree.insert(feature, feature.index, bucket.sourceLayerIndex, bucket.index); } } @@ -202,14 +202,19 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback, rawTileData tile.redoPlacementAfterDone = false; } - var featureTree = tile.featureTree.serialize(); + var featureTree_ = featureTree.serialize(); + var collisionTile_ = collisionTile.serialize(); + var collisionBoxArray = tile.collisionBoxArray.arrayBuffer.slice(); + var transferables = [rawTileData, collisionBoxArray].concat(featureTree.transferables).concat(collisionTile_.transferables); callback(null, { buckets: buckets.filter(isBucketEmpty).map(serializeBucket), bucketStats: stats, // TODO put this in a separate message? - featureTree: featureTree.data, + featureTree: featureTree_.data, + collisionTile: collisionTile_.data, + collisionBoxArray: collisionBoxArray, rawTileData: rawTileData - }, getTransferables(buckets).concat(featureTree.transferables.concat(rawTileData))); + }, getTransferables(buckets).concat(transferables)); } }; @@ -222,18 +227,20 @@ WorkerTile.prototype.redoPlacement = function(angle, pitch, showCollisionBoxes) var collisionTile = new CollisionTile(angle, pitch, this.collisionBoxArray); - this.featureTree.setCollisionTile(collisionTile); var buckets = this.symbolBuckets; for (var i = buckets.length - 1; i >= 0; i--) { buckets[i].placeFeatures(collisionTile, showCollisionBoxes); } + var collisionTile_ = collisionTile.serialize(); + return { result: { - buckets: buckets.filter(isBucketEmpty).map(serializeBucket) + buckets: buckets.filter(isBucketEmpty).map(serializeBucket), + collisionTile: collisionTile_.data }, - transferables: getTransferables(buckets) + transferables: getTransferables(buckets).concat(collisionTile_.transferables) }; }; diff --git a/js/symbol/collision_tile.js b/js/symbol/collision_tile.js index 2fe55562166..ae7cc7fc3ec 100644 --- a/js/symbol/collision_tile.js +++ b/js/symbol/collision_tile.js @@ -3,7 +3,6 @@ var Point = require('point-geometry'); var EXTENT = require('../data/bucket').EXTENT; var Grid = require('../util/grid'); -var CollisionBox = require('../symbol/collision_box'); module.exports = CollisionTile; @@ -20,9 +19,9 @@ module.exports = CollisionTile; function CollisionTile(angle, pitch, collisionBoxArray) { if (typeof angle === 'object') { var serialized = angle; + collisionBoxArray = pitch; angle = serialized.angle; pitch = serialized.pitch; - collisionBoxArray = new CollisionBox(serialized.collisionBoxArray); this.grid = new Grid(serialized.grid); this.ignoredGrid = new Grid(serialized.ignoredGrid); } else { @@ -84,13 +83,16 @@ function CollisionTile(angle, pitch, collisionBoxArray) { } CollisionTile.prototype.serialize = function() { - return { + var data = { angle: this.angle, pitch: this.pitch, - collisionBoxArray: this.collisionBoxArray.arrayBuffer.slice(), grid: this.grid.toArrayBuffer(), ignoredGrid: this.ignoredGrid.toArrayBuffer() }; + return { + data: data, + transferables: [data.grid, data.ignoredGrid] + }; }; CollisionTile.prototype.minScale = 0.25; @@ -110,7 +112,6 @@ CollisionTile.prototype.placeCollisionFeature = function(collisionFeature, allow var minPlacementScale = this.minScale; var rotationMatrix = this.rotationMatrix; var yStretch = this.yStretch; - var boxArray = this.collisionBoxArray; var box = this.box; var blocking = this.blocking; From abc7b93803be82b50cf44a0cbdc4503d85456c17 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Thu, 3 Mar 2016 15:08:07 -0800 Subject: [PATCH 15/48] add async version of queryRenderedFeatures Despite being potentially parallelized, this often takes longer than the synchronous verion because the features have to be parsed from the pbf twice instead of once. The advantage of the async version is that it spends less time in the main thread which can keep the everything more responsive. It still takes some time to parse all the features from the vector tile into geojson (~15ms for a full screen query in a dense area). Parsing the pbf on the main thread is faster than parsing json. Parsing the pbf on the main thread also lets us lazily load the geometry and avoid implementing the `includeGeometry` option. --- js/data/feature_tree.js | 70 ++++++++++++++++++++++++++++---- js/source/source.js | 60 ++++++++++++++++++++------- js/source/worker.js | 9 +++- js/util/struct_array.js | 11 +++-- js/util/vectortile_to_geojson.js | 23 ++++++++--- 5 files changed, 141 insertions(+), 32 deletions(-) diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index a28fa179856..9a19c12fde1 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -20,6 +20,17 @@ var FeatureIndexArray = createStructArrayType([ { type: 'Uint16', name: 'bucketIndex' } ]); +var FilteredFeatureIndexArray = createStructArrayType([ + // the index of the feature in the original vectortile + { type: 'Uint32', name: 'featureIndex' }, + // the source layer the feature appears in + { type: 'Uint16', name: 'sourceLayerIndex' }, + // the bucket the feature appears in + { type: 'Uint16', name: 'bucketIndex' }, + // the layer the feature appears in + { type: 'Uint16', name: 'layerIndex' } +]); + module.exports = FeatureTree; function FeatureTree(coord, overscaling, collisionTile) { @@ -90,13 +101,17 @@ function translateDistance(translate) { } // Finds features in this tile at a particular position. -FeatureTree.prototype.query = function(result, args, styleLayersByID) { +FeatureTree.prototype.query = function(result, args, styleLayersByID, returnGeoJSON) { if (!this.vtLayers) { if (!this.rawTileData) return []; this.vtLayers = new vt.VectorTile(new Protobuf(new Uint8Array(this.rawTileData))).layers; this.sourceLayerNumberMapping = new StringNumberMapping(this.vtLayers ? Object.keys(this.vtLayers).sort() : []); } + if (!returnGeoJSON) { + result = new FilteredFeatureIndexArray(); + } + var params = args.params || {}, pixelsToTileUnits = EXTENT / args.tileSize / args.scale, filter = featureFilter(params.filter); @@ -137,16 +152,20 @@ FeatureTree.prototype.query = function(result, args, styleLayersByID) { var matching = this.grid.query(minX - additionalRadius, minY - additionalRadius, maxX + additionalRadius, maxY + additionalRadius); var match = this.featureIndexArray.at(0); - this.filterMatching(result, matching, match, queryGeometry, filter, params.layerIds, styleLayersByID, args.bearing, pixelsToTileUnits); + this.filterMatching(result, matching, match, queryGeometry, filter, params.layerIds, styleLayersByID, args.bearing, pixelsToTileUnits, returnGeoJSON); var matchingSymbols = this.collisionTile.queryRenderedSymbols(minX, minY, maxX, maxY, args.scale); var match2 = this.collisionTile.collisionBoxArray.at(0); - this.filterMatching(result, matchingSymbols, match2, queryGeometry, filter, params.layerIds, styleLayersByID, args.bearing, pixelsToTileUnits); + this.filterMatching(result, matchingSymbols, match2, queryGeometry, filter, params.layerIds, styleLayersByID, args.bearing, pixelsToTileUnits, returnGeoJSON); + + if (!returnGeoJSON) { + result = result.arrayBuffer; + } return result; }; -FeatureTree.prototype.filterMatching = function(result, matching, match, queryGeometry, filter, filterLayerIDs, styleLayersByID, bearing, pixelsToTileUnits) { +FeatureTree.prototype.filterMatching = function(result, matching, match, queryGeometry, filter, filterLayerIDs, styleLayersByID, bearing, pixelsToTileUnits, returnGeoJSON) { var seen = {}; for (var k = 0; k < matching.length; k++) { var index = matching[k]; @@ -165,6 +184,8 @@ FeatureTree.prototype.filterMatching = function(result, matching, match, queryGe if (!filter(feature)) continue; + var geometry = null; + for (var l = 0; l < layerIDs.length; l++) { var layerID = layerIDs[l]; @@ -178,7 +199,7 @@ FeatureTree.prototype.filterMatching = function(result, matching, match, queryGe if (styleLayer.type !== 'symbol') { // all symbols already match the style - var geometry = loadGeometry(feature); + if (!geometry) geometry = loadGeometry(feature); if (styleLayer.type === 'line') { translatedPolygon = translate(queryGeometry, @@ -205,13 +226,46 @@ FeatureTree.prototype.filterMatching = function(result, matching, match, queryGe } } - var geojsonFeature = new GeoJSONFeature(feature, this.z, this.x, this.y); - geojsonFeature.layer = layerID; - result.push(geojsonFeature); + if (returnGeoJSON) { + var geojsonFeature = new GeoJSONFeature(feature, this.z, this.x, this.y); + geojsonFeature.layer = layerID; + result.push(geojsonFeature); + } else { + result.emplaceBack(match.featureIndex, match.sourceLayerIndex, match.bucketIndex, l); + } } } }; +FeatureTree.prototype.makeGeoJSON = function(result, featureIndexArray) { + if (!this.vtLayers) { + if (!this.rawTileData) return []; + this.vtLayers = new vt.VectorTile(new Protobuf(new Uint8Array(this.rawTileData))).layers; + this.sourceLayerNumberMapping = new StringNumberMapping(this.vtLayers ? Object.keys(this.vtLayers).sort() : []); + } + + featureIndexArray = new FilteredFeatureIndexArray(featureIndexArray); + var indexes = featureIndexArray.at(0); + + var cachedLayerFeatures = {}; + for (var i = 0; i < featureIndexArray.length; i++) { + indexes._setIndex(i); + var sourceLayerName = this.sourceLayerNumberMapping.numberToString[indexes.sourceLayerIndex]; + var sourceLayer = this.vtLayers[sourceLayerName]; + var featureIndex = indexes.featureIndex; + + var cachedFeatures = cachedLayerFeatures[sourceLayerName]; + if (cachedFeatures === undefined) { + cachedFeatures = cachedLayerFeatures[sourceLayerName] = {}; + } + + var feature = cachedFeatures[featureIndex] = cachedFeatures[featureIndex] || sourceLayer.feature(featureIndex); + var geojsonFeature = new GeoJSONFeature(feature, this.z, this.x, this.y); + geojsonFeature.layer = this.numberToLayerIDs[indexes.bucketIndex][indexes.layerIndex]; + result.push(geojsonFeature); + } +}; + function translate(queryGeometry, translate, translateAnchor, bearing, pixelsToTileUnits) { if (!translate[0] && !translate[1]) { return queryGeometry; diff --git a/js/source/source.js b/js/source/source.js index abc20c9b962..a1ce5d670f0 100644 --- a/js/source/source.js +++ b/js/source/source.js @@ -78,21 +78,53 @@ exports._queryRenderedVectorFeatures = function(queryGeometry, params, classes, var styleLayers = this.map.style._layers; var features = []; - for (var r = 0; r < tilesIn.length; r++) { - var tileIn = tilesIn[r]; - if (!tileIn.tile.loaded || !tileIn.tile.featureTree) continue; - tileIn.tile.featureTree.query(features, { - queryGeometry: tileIn.queryGeometry, - scale: tileIn.scale, - tileSize: tileIn.tile.tileSize, - bearing: bearing, - params: params - }, styleLayers); - } + var async = true; + if (async) { + util.asyncAll(tilesIn, function(tileIn, callback) { + + if (!tileIn.tile.loaded || !tileIn.tile.featureTree) return callback(); + + var featureTree = tileIn.tile.featureTree.serialize(); + var collisionTile = tileIn.tile.collisionTile.serialize(); + + this.dispatcher.send('query rendered features', { + uid: tileIn.tile.uid, + source: this.id, + queryGeometry: tileIn.queryGeometry, + scale: tileIn.scale, + tileSize: tileIn.tile.tileSize, + classes: classes, + zoom: zoom, + bearing: bearing, + params: params, + featureTree: featureTree.data, + collisionTile: collisionTile.data, + rawTileData: tileIn.tile.rawTileData.slice() + }, function(err_, data) { + if (data) tileIn.tile.featureTree.makeGeoJSON(features, data); + callback(); + }, tileIn.tile.workerID); + }.bind(this), function() { + callback(null, features); + }); + + } else { + for (var r = 0; r < tilesIn.length; r++) { + var tileIn = tilesIn[r]; + if (!tileIn.tile.loaded || !tileIn.tile.featureTree) continue; + tileIn.tile.featureTree.query(features, { + queryGeometry: tileIn.queryGeometry, + scale: tileIn.scale, + tileSize: tileIn.tile.tileSize, + bearing: bearing, + params: params + }, styleLayers, true); + } - setTimeout(function() { - callback(null, features); - }, 0); + setTimeout(function() { + callback(null, features); + }, 0); + } }; exports._querySourceFeatures = function(params, callback) { diff --git a/js/source/worker.js b/js/source/worker.js index 8dbd616aa9b..9266f253020 100644 --- a/js/source/worker.js +++ b/js/source/worker.js @@ -13,6 +13,9 @@ var geojsonvt = require('geojson-vt'); var rewind = require('geojson-rewind'); var GeoJSONWrapper = require('./geojson_wrapper'); +var CollisionTile = require('../symbol/collision_tile'); +var FeatureTree = require('../data/feature_tree'); + module.exports = function(self) { return new Worker(self); }; @@ -208,7 +211,11 @@ util.extend(Worker.prototype, { } } - callback(null, tile.featureTree.query(params, this.styleLayersByID)); + var collisionTile = new CollisionTile(params.collisionTile, tile.collisionBoxArray); + var featureTree = new FeatureTree(params.featureTree, params.rawTileData, collisionTile); + + var featureArrayBuffer = featureTree.query(undefined, params, this.styleLayersByID, false); + callback(null, featureArrayBuffer, [featureArrayBuffer]); } else { callback(null, []); } diff --git a/js/util/struct_array.js b/js/util/struct_array.js index b31ac1724c9..eb8f734fa32 100644 --- a/js/util/struct_array.js +++ b/js/util/struct_array.js @@ -81,6 +81,7 @@ function createEmplaceBack(members, BYTES_PER_ELEMENT) { 'var pos2 = pos1 / 2;\n' + 'var pos4 = pos1 / 4;\n' + 'this.length++;\n' + + 'this.metadataArray[0]++;\n' + 'if (this.length > this.allocatedLength) this.resize(this.length);\n'; for (var m = 0; m < members.length; m++) { var member = members[m]; @@ -119,8 +120,8 @@ function StructArray(initialAllocatedLength) { if (initialAllocatedLength instanceof ArrayBuffer) { this.arrayBuffer = initialAllocatedLength; this._refreshViews(); - this.length = this.uint8Array.length / this.BYTES_PER_ELEMENT; - this.size = this.uint8Array.length; + this.length = this.metadataArray[0]; + this.allocatedLength = this.uint8Array.length / this.BYTES_PER_ELEMENT; } else { if (initialAllocatedLength === undefined) { initialAllocatedLength = this.DEFAULT_ALLOCATED_LENGTH; @@ -133,10 +134,11 @@ StructArray.prototype.DEFAULT_ALLOCATED_LENGTH = 100; StructArray.prototype.RESIZE_FACTOR = 1.5; StructArray.prototype.allocatedLength = 0; StructArray.prototype.length = 0; +var METADATA_BYTES = align(4, 8); StructArray.prototype.resize = function(n) { this.allocatedLength = Math.max(n, Math.floor(this.allocatedLength * this.RESIZE_FACTOR)); - this.arrayBuffer = new ArrayBuffer(Math.floor(this.allocatedLength * this.BYTES_PER_ELEMENT / 8) * 8); + this.arrayBuffer = new ArrayBuffer(METADATA_BYTES + align(this.allocatedLength * this.BYTES_PER_ELEMENT, 8)); var oldUint8Array = this.uint8Array; this._refreshViews(); @@ -145,8 +147,9 @@ StructArray.prototype.resize = function(n) { StructArray.prototype._refreshViews = function() { for (var t in viewTypes) { - this[getArrayViewName(t)] = new viewTypes[t](this.arrayBuffer); + this[getArrayViewName(t)] = new viewTypes[t](this.arrayBuffer, METADATA_BYTES); } + this.metadataArray = new Uint32Array(this.arrayBuffer, 0, 1); }; StructArray.prototype.at = function(index) { diff --git a/js/util/vectortile_to_geojson.js b/js/util/vectortile_to_geojson.js index f41eb62a9f9..10caa5afcbb 100644 --- a/js/util/vectortile_to_geojson.js +++ b/js/util/vectortile_to_geojson.js @@ -6,9 +6,9 @@ module.exports = Feature; function Feature(vectorTileFeature, z, x, y) { this._vectorTileFeature = vectorTileFeature; - this._z = z; - this._x = x; - this._y = y; + vectorTileFeature._z = z; + vectorTileFeature._x = x; + vectorTileFeature._y = y; this.properties = vectorTileFeature.properties; @@ -21,12 +21,12 @@ Feature.prototype = { type: "Feature", get geometry() { - if (!this._geometry) { + if (this._geometry === undefined) { var feature = this._vectorTileFeature; var coords = projectCoords( feature.loadGeometry(), feature.extent, - this._z, this._x, this._y); + feature._z, feature._x, feature._y); var type = VectorTileFeature.types[feature.type]; @@ -49,6 +49,19 @@ Feature.prototype = { this._vectorTileFeature = null; } return this._geometry; + }, + + set geometry(g) { + this._geometry = g; + }, + + toJSON: function() { + var json = {}; + for (var i in this) { + if (i === '_geometry' || i === '_vectorTileFeature') continue; + json[i] = this[i]; + } + return json; } }; From 2b971f5a62894ddf3b7f3c6562197170831d09f1 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Thu, 3 Mar 2016 18:09:38 -0800 Subject: [PATCH 16/48] fix query geojson layers --- js/data/feature_tree.js | 4 ++-- js/source/geojson_wrapper.js | 1 + js/source/tile.js | 2 +- js/source/worker.js | 6 +++++- js/source/worker_tile.js | 4 ++-- package.json | 3 ++- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index 9a19c12fde1..59736572b3a 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -105,7 +105,7 @@ FeatureTree.prototype.query = function(result, args, styleLayersByID, returnGeoJ if (!this.vtLayers) { if (!this.rawTileData) return []; this.vtLayers = new vt.VectorTile(new Protobuf(new Uint8Array(this.rawTileData))).layers; - this.sourceLayerNumberMapping = new StringNumberMapping(this.vtLayers ? Object.keys(this.vtLayers).sort() : []); + this.sourceLayerNumberMapping = new StringNumberMapping(this.vtLayers ? Object.keys(this.vtLayers).sort() : ['_geojsonTileLayer']); } if (!returnGeoJSON) { @@ -241,7 +241,7 @@ FeatureTree.prototype.makeGeoJSON = function(result, featureIndexArray) { if (!this.vtLayers) { if (!this.rawTileData) return []; this.vtLayers = new vt.VectorTile(new Protobuf(new Uint8Array(this.rawTileData))).layers; - this.sourceLayerNumberMapping = new StringNumberMapping(this.vtLayers ? Object.keys(this.vtLayers).sort() : []); + this.sourceLayerNumberMapping = new StringNumberMapping(this.vtLayers ? Object.keys(this.vtLayers).sort() : ['_geojsonTileLayer']); } featureIndexArray = new FilteredFeatureIndexArray(featureIndexArray); diff --git a/js/source/geojson_wrapper.js b/js/source/geojson_wrapper.js index 5dd5584a475..0f2b165438b 100644 --- a/js/source/geojson_wrapper.js +++ b/js/source/geojson_wrapper.js @@ -10,6 +10,7 @@ module.exports = GeoJSONWrapper; function GeoJSONWrapper(features) { this.features = features; this.length = features.length; + this.extent = EXTENT; } GeoJSONWrapper.prototype.feature = function(i) { diff --git a/js/source/tile.js b/js/source/tile.js index 44908b7e6e9..c45a67c4f15 100644 --- a/js/source/tile.js +++ b/js/source/tile.js @@ -155,7 +155,7 @@ Tile.prototype = { this.vtLayers = new vt.VectorTile(new Protobuf(new Uint8Array(this.rawTileData))).layers; } - var layer = this.vtLayers[params.sourceLayer]; + var layer = this.vtLayers._geojsonTileLayer || this.vtLayers[params.sourceLayer]; if (!layer) return; diff --git a/js/source/worker.js b/js/source/worker.js index 9266f253020..d49ed660a94 100644 --- a/js/source/worker.js +++ b/js/source/worker.js @@ -12,6 +12,7 @@ var supercluster = require('supercluster'); var geojsonvt = require('geojson-vt'); var rewind = require('geojson-rewind'); var GeoJSONWrapper = require('./geojson_wrapper'); +var vtpbf = require('vt-pbf'); var CollisionTile = require('../symbol/collision_tile'); var FeatureTree = require('../data/feature_tree'); @@ -184,7 +185,10 @@ util.extend(Worker.prototype, { this.loaded[source][params.uid] = tile; if (geoJSONTile) { - tile.parse(new GeoJSONWrapper(geoJSONTile.features), this.layers, this.actor, callback); + var geojsonWrapper = new GeoJSONWrapper(geoJSONTile.features); + geojsonWrapper.name = '_geojsonTileLayer'; + var rawTileData = vtpbf({ layers: { '_geojsonTileLayer': geojsonWrapper }}); + tile.parse(geojsonWrapper, this.layers, this.actor, callback, rawTileData); } else { return callback(null, null); // nothing in the given tile } diff --git a/js/source/worker_tile.js b/js/source/worker_tile.js index f02aafc2580..8cb93363c1c 100644 --- a/js/source/worker_tile.js +++ b/js/source/worker_tile.js @@ -29,7 +29,7 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback, rawTileData this.collisionBoxArray = new CollisionBoxArray(); var collisionTile = new CollisionTile(this.angle, this.pitch, this.collisionBoxArray); var featureTree = new FeatureTree(this.coord, this.overscaling, collisionTile, data.layers); - var sourceLayerNumberMapping = new StringNumberMapping(data.layers ? Object.keys(data.layers).sort() : []); + var sourceLayerNumberMapping = new StringNumberMapping(data.layers ? Object.keys(data.layers).sort() : ['_geojsonTileLayer']); var stats = { _total: 0 }; @@ -57,7 +57,7 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback, rawTileData overscaling: this.overscaling, showCollisionBoxes: this.showCollisionBoxes, collisionBoxArray: this.collisionBoxArray, - sourceLayerIndex: sourceLayerNumberMapping.stringToNumber[layer['source-layer']] + sourceLayerIndex: sourceLayerNumberMapping.stringToNumber[layer['source-layer'] || '_geojsonTileLayer'] }); bucket.createFilter(); diff --git a/package.json b/package.json index 66958c3a371..c0656c72446 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "unassertify": "^2.0.0", "unitbezier": "^0.0.0", "vector-tile": "^1.2.0", - "webworkify": "^1.0.2" + "webworkify": "^1.0.2", + "vt-pbf": "^2.0.2" }, "devDependencies": { "benchmark": "~1.0.0", From 8dc1d4342dba8c18ac4e311f5b7eedf022292204 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Fri, 4 Mar 2016 12:48:42 -0800 Subject: [PATCH 17/48] optimize polygonIntersectsBufferedMultiLine for when polygon is just a point. --- js/data/feature_tree.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index 59736572b3a..8fd79f90823 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -354,8 +354,10 @@ function polygonIntersectsBufferedMultiLine(polygon, multiLine, radius) { for (var i = 0; i < multiLine.length; i++) { var line = multiLine[i]; - for (var k = 0; k < line.length; k++) { - if (polygonContainsPoint(polygon, line[k])) return true; + if (polygon.length >= 3) { + for (var k = 0; k < line.length; k++) { + if (polygonContainsPoint(polygon, line[k])) return true; + } } if (lineIntersectsBufferedLine(polygon, line, radius)) return true; @@ -365,11 +367,13 @@ function polygonIntersectsBufferedMultiLine(polygon, multiLine, radius) { function lineIntersectsBufferedLine(lineA, lineB, radius) { - if (lineIntersectsLine(lineA, lineB)) return true; + if (lineA.length > 1) { + if (lineIntersectsLine(lineA, lineB)) return true; - // Check whether any point in either line is within radius of the other line - for (var j = 0; j < lineB.length; j++) { - if (pointIntersectsBufferedLine(lineB[j], lineA, radius)) return true; + // Check whether any point in either line is within radius of the other line + for (var j = 0; j < lineB.length; j++) { + if (pointIntersectsBufferedLine(lineB[j], lineA, radius)) return true; + } } for (var k = 0; k < lineA.length; k++) { From 1227aa0a2a4b37bc2deb2d1e5bba105a7f890bda Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Fri, 4 Mar 2016 12:51:28 -0800 Subject: [PATCH 18/48] add layer property when creation geojson --- js/data/feature_tree.js | 19 ++++++++++++++----- js/source/source.js | 2 +- js/style/style.js | 16 +++------------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index 8fd79f90823..0d21d41ccf5 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -152,11 +152,11 @@ FeatureTree.prototype.query = function(result, args, styleLayersByID, returnGeoJ var matching = this.grid.query(minX - additionalRadius, minY - additionalRadius, maxX + additionalRadius, maxY + additionalRadius); var match = this.featureIndexArray.at(0); - this.filterMatching(result, matching, match, queryGeometry, filter, params.layerIds, styleLayersByID, args.bearing, pixelsToTileUnits, returnGeoJSON); + this.filterMatching(result, matching, match, queryGeometry, filter, params.layerIDs, styleLayersByID, args.bearing, pixelsToTileUnits, returnGeoJSON); var matchingSymbols = this.collisionTile.queryRenderedSymbols(minX, minY, maxX, maxY, args.scale); var match2 = this.collisionTile.collisionBoxArray.at(0); - this.filterMatching(result, matchingSymbols, match2, queryGeometry, filter, params.layerIds, styleLayersByID, args.bearing, pixelsToTileUnits, returnGeoJSON); + this.filterMatching(result, matchingSymbols, match2, queryGeometry, filter, params.layerIDs, styleLayersByID, args.bearing, pixelsToTileUnits, returnGeoJSON); if (!returnGeoJSON) { result = result.arrayBuffer; @@ -194,6 +194,7 @@ FeatureTree.prototype.filterMatching = function(result, matching, match, queryGe } var styleLayer = styleLayersByID[layerID]; + if (!styleLayer) continue; var translatedPolygon; if (styleLayer.type !== 'symbol') { @@ -228,7 +229,9 @@ FeatureTree.prototype.filterMatching = function(result, matching, match, queryGe if (returnGeoJSON) { var geojsonFeature = new GeoJSONFeature(feature, this.z, this.x, this.y); - geojsonFeature.layer = layerID; + geojsonFeature.layer = styleLayer.serialize({ + includeRefProperties: true + }); result.push(geojsonFeature); } else { result.emplaceBack(match.featureIndex, match.sourceLayerIndex, match.bucketIndex, l); @@ -237,7 +240,7 @@ FeatureTree.prototype.filterMatching = function(result, matching, match, queryGe } }; -FeatureTree.prototype.makeGeoJSON = function(result, featureIndexArray) { +FeatureTree.prototype.makeGeoJSON = function(result, featureIndexArray, styleLayers) { if (!this.vtLayers) { if (!this.rawTileData) return []; this.vtLayers = new vt.VectorTile(new Protobuf(new Uint8Array(this.rawTileData))).layers; @@ -260,8 +263,14 @@ FeatureTree.prototype.makeGeoJSON = function(result, featureIndexArray) { } var feature = cachedFeatures[featureIndex] = cachedFeatures[featureIndex] || sourceLayer.feature(featureIndex); + + var styleLayer = styleLayers[this.numberToLayerIDs[indexes.bucketIndex][indexes.layerIndex]]; + if (!styleLayer) continue; + var geojsonFeature = new GeoJSONFeature(feature, this.z, this.x, this.y); - geojsonFeature.layer = this.numberToLayerIDs[indexes.bucketIndex][indexes.layerIndex]; + geojsonFeature.layer = styleLayer.serialize({ + includeRefProperties: true + }); result.push(geojsonFeature); } }; diff --git a/js/source/source.js b/js/source/source.js index a1ce5d670f0..20b15e9c7e7 100644 --- a/js/source/source.js +++ b/js/source/source.js @@ -101,7 +101,7 @@ exports._queryRenderedVectorFeatures = function(queryGeometry, params, classes, collisionTile: collisionTile.data, rawTileData: tileIn.tile.rawTileData.slice() }, function(err_, data) { - if (data) tileIn.tile.featureTree.makeGeoJSON(features, data); + if (data) tileIn.tile.featureTree.makeGeoJSON(features, data, styleLayers); callback(); }, tileIn.tile.workerID); }.bind(this), function() { diff --git a/js/style/style.js b/js/style/style.js index 2833961809c..8e97c0755a2 100644 --- a/js/style/style.js +++ b/js/style/style.js @@ -415,7 +415,7 @@ Style.prototype = util.inherit(Evented, { var error = null; if (params.layer) { - params.layerIds = Array.isArray(params.layer) ? params.layer : [params.layer]; + params.layerIDs = Array.isArray(params.layer) ? params.layer : [params.layer]; } util.asyncAll(Object.keys(this.sources), function(id, callback) { @@ -427,18 +427,8 @@ Style.prototype = util.inherit(Evented, { }); }.bind(this), function() { if (error) return callback(error); - - callback(null, features - .filter(function(feature) { - return this._layers[feature.layer] !== undefined; - }.bind(this)) - .map(function(feature) { - feature.layer = this._layers[feature.layer].serialize({ - includeRefProperties: true - }); - return feature; - }.bind(this))); - }.bind(this)); + callback(null, features); + }); }, _remove: function() { From e89fe1c360b68006228013f6ab2e922a4560f43d Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Fri, 4 Mar 2016 13:33:53 -0800 Subject: [PATCH 19/48] add synchronous version of queryRenderedFeatures --- js/source/source.js | 18 +++++++----------- js/style/style.js | 31 ++++++++++++++++++++----------- js/ui/map.js | 24 ++++++++++++++++++++---- 3 files changed, 47 insertions(+), 26 deletions(-) diff --git a/js/source/source.js b/js/source/source.js index 20b15e9c7e7..9baf2a16d6a 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._queryRenderedVectorFeatures = function(queryGeometry, params, classes, zoom, bearing, callback) { +exports._queryRenderedVectorFeatures = function(resultFeatures, queryGeometry, params, classes, zoom, bearing, callback) { if (!this._pyramid) return callback(null, []); @@ -76,10 +76,10 @@ exports._queryRenderedVectorFeatures = function(queryGeometry, params, classes, return callback(null, []); var styleLayers = this.map.style._layers; - var features = []; - var async = true; - if (async) { + var isAsync = callback !== undefined; + + if (isAsync) { util.asyncAll(tilesIn, function(tileIn, callback) { if (!tileIn.tile.loaded || !tileIn.tile.featureTree) return callback(); @@ -101,18 +101,18 @@ exports._queryRenderedVectorFeatures = function(queryGeometry, params, classes, collisionTile: collisionTile.data, rawTileData: tileIn.tile.rawTileData.slice() }, function(err_, data) { - if (data) tileIn.tile.featureTree.makeGeoJSON(features, data, styleLayers); + if (data) tileIn.tile.featureTree.makeGeoJSON(resultFeatures, data, styleLayers); callback(); }, tileIn.tile.workerID); }.bind(this), function() { - callback(null, features); + callback(null); }); } else { for (var r = 0; r < tilesIn.length; r++) { var tileIn = tilesIn[r]; if (!tileIn.tile.loaded || !tileIn.tile.featureTree) continue; - tileIn.tile.featureTree.query(features, { + tileIn.tile.featureTree.query(resultFeatures, { queryGeometry: tileIn.queryGeometry, scale: tileIn.scale, tileSize: tileIn.tile.tileSize, @@ -120,10 +120,6 @@ exports._queryRenderedVectorFeatures = function(queryGeometry, params, classes, params: params }, styleLayers, true); } - - setTimeout(function() { - callback(null, features); - }, 0); } }; diff --git a/js/style/style.js b/js/style/style.js index 8e97c0755a2..af720af6628 100644 --- a/js/style/style.js +++ b/js/style/style.js @@ -411,24 +411,33 @@ Style.prototype = util.inherit(Evented, { }, queryRenderedFeatures: function(queryGeometry, params, classes, zoom, bearing, callback) { - var features = []; + var resultFeatures = []; var error = null; if (params.layer) { params.layerIDs = Array.isArray(params.layer) ? params.layer : [params.layer]; } - util.asyncAll(Object.keys(this.sources), function(id, callback) { - var source = this.sources[id]; - source.queryRenderedFeatures(queryGeometry, params, classes, zoom, bearing, function(err, result) { - if (result) features = features.concat(result); - if (err) error = err; - callback(); + var isAsync = callback !== undefined; + + if (isAsync) { + util.asyncAll(Object.keys(this.sources), function(id, callback) { + var source = this.sources[id]; + source.queryRenderedFeatures(resultFeatures, queryGeometry, params, classes, zoom, bearing, function(err) { + if (err) error = err; + callback(); + }); + }.bind(this), function() { + if (error) return callback(error); + callback(null, resultFeatures); }); - }.bind(this), function() { - if (error) return callback(error); - callback(null, features); - }); + } else { + for (var id in this.sources) { + this.sources[id].queryRenderedFeatures(resultFeatures, queryGeometry, params, classes, zoom, bearing); + } + + return resultFeatures; + } }, _remove: function() { diff --git a/js/ui/map.js b/js/ui/map.js index 550eebf3291..a0ea56217a9 100644 --- a/js/ui/map.js +++ b/js/ui/map.js @@ -400,9 +400,27 @@ util.extend(Map.prototype, /** @lends Map.prototype */{ * }); */ queryRenderedFeatures: function(pointOrBox, params, callback) { - if (callback === undefined) { + if (!(pointOrBox instanceof Point || Array.isArray(pointOrBox))) { callback = params; params = pointOrBox; + pointOrBox = undefined; + } + var queryGeometry = this._makeQueryGeometry(pointOrBox); + this.style.queryRenderedFeatures(queryGeometry, params, this._classes, this.transform.zoom, this.transform.angle, callback); + return this; + }, + + queryRenderedFeaturesSync: function(pointOrBox, params) { + if (!(pointOrBox instanceof Point || Array.isArray(pointOrBox))) { + params = pointOrBox; + pointOrBox = undefined; + } + var queryGeometry = this._makeQueryGeometry(pointOrBox); + return this.style.queryRenderedFeatures(queryGeometry, params, this._classes, this.transform.zoom, this.transform.angle); + }, + + _makeQueryGeometry: function(pointOrBox) { + if (pointOrBox === undefined) { // bounds was omitted: use full viewport pointOrBox = [ Point.convert([0, 0]), @@ -431,11 +449,9 @@ util.extend(Map.prototype, /** @lends Map.prototype */{ return this.transform.locationCoordinate(this.transform.pointLocation(p).wrap()); }.bind(this)); - this.style.queryRenderedFeatures(queryGeometry, params, this._classes, this.transform.zoom, this.transform.angle, callback); - return this; + return queryGeometry; }, - /** * Get data from vector tiles as an array of GeoJSON Features. * From c84642ffd2ed466d1ecb230d4f9668cc18dea0e2 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Mon, 7 Mar 2016 18:20:20 -0800 Subject: [PATCH 20/48] only create Int32Array for non-empty cells This significantly reduces memory usage for CollisionTiles that aren't used, or are barely used. --- js/util/grid.js | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/js/util/grid.js b/js/util/grid.js index e66ffa35087..dd26ac7a153 100644 --- a/js/util/grid.js +++ b/js/util/grid.js @@ -15,7 +15,11 @@ function Grid(n, extent, padding) { this.d = n + 2 * padding; for (var k = 0; k < this.d * this.d; k++) { - cells.push(array.subarray(array[NUM_PARAMS + k], array[NUM_PARAMS + k + 1])); + var start = array[NUM_PARAMS + k]; + var end = array[NUM_PARAMS + k + 1]; + cells.push(start === end ? + null : + array.subarray(start, end)); } var keysOffset = array[NUM_PARAMS + cells.length]; var bboxesOffset = array[NUM_PARAMS + cells.length + 1]; @@ -71,20 +75,22 @@ Grid.prototype.query = function(x1, y1, x2, y2) { Grid.prototype._queryCell = function(x1, y1, x2, y2, cellIndex, result, seenUids) { var cell = this.cells[cellIndex]; - var keys = this.keys; - var bboxes = this.bboxes; - for (var u = 0; u < cell.length; u++) { - var uid = cell[u]; - if (seenUids[uid] === undefined) { - var offset = uid * 4; - if ((x1 <= bboxes[offset + 2]) && - (y1 <= bboxes[offset + 3]) && - (x2 >= bboxes[offset + 0]) && - (y2 >= bboxes[offset + 1])) { - seenUids[uid] = true; - result.push(keys[uid]); - } else { - seenUids[uid] = false; + if (cell !== null) { + var keys = this.keys; + var bboxes = this.bboxes; + for (var u = 0; u < cell.length; u++) { + var uid = cell[u]; + if (seenUids[uid] === undefined) { + var offset = uid * 4; + if ((x1 <= bboxes[offset + 2]) && + (y1 <= bboxes[offset + 3]) && + (x2 >= bboxes[offset + 0]) && + (y2 >= bboxes[offset + 1])) { + seenUids[uid] = true; + result.push(keys[uid]); + } else { + seenUids[uid] = false; + } } } } From a92d4e0996ed62d8ff013f0fb052b5a48cfc3007 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 8 Mar 2016 12:02:25 -0800 Subject: [PATCH 21/48] sort queryRenderedFeatures top-down --- js/data/feature_tree.js | 40 ++++++++++++++++++++++++---------- js/source/source.js | 47 ++++++++++++++++++++++++++++++++-------- js/source/worker.js | 2 +- js/source/worker_tile.js | 2 +- js/style/style.js | 39 ++++++++++++++++++++------------- js/ui/map.js | 4 ++-- js/util/grid.js | 5 ++++- 7 files changed, 98 insertions(+), 41 deletions(-) diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index 0d21d41ccf5..519b129b90b 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -101,16 +101,16 @@ function translateDistance(translate) { } // Finds features in this tile at a particular position. -FeatureTree.prototype.query = function(result, args, styleLayersByID, returnGeoJSON) { +FeatureTree.prototype.query = function(args, styleLayersByID, returnGeoJSON) { if (!this.vtLayers) { if (!this.rawTileData) return []; this.vtLayers = new vt.VectorTile(new Protobuf(new Uint8Array(this.rawTileData))).layers; this.sourceLayerNumberMapping = new StringNumberMapping(this.vtLayers ? Object.keys(this.vtLayers).sort() : ['_geojsonTileLayer']); } - if (!returnGeoJSON) { - result = new FilteredFeatureIndexArray(); - } + var result = returnGeoJSON ? + {} : + new FilteredFeatureIndexArray(); var params = args.params || {}, pixelsToTileUnits = EXTENT / args.tileSize / args.scale, @@ -151,20 +151,22 @@ FeatureTree.prototype.query = function(result, args, styleLayersByID, returnGeoJ } var matching = this.grid.query(minX - additionalRadius, minY - additionalRadius, maxX + additionalRadius, maxY + additionalRadius); + matching.sort(topDown); var match = this.featureIndexArray.at(0); this.filterMatching(result, matching, match, queryGeometry, filter, params.layerIDs, styleLayersByID, args.bearing, pixelsToTileUnits, returnGeoJSON); var matchingSymbols = this.collisionTile.queryRenderedSymbols(minX, minY, maxX, maxY, args.scale); var match2 = this.collisionTile.collisionBoxArray.at(0); + matchingSymbols.sort(); this.filterMatching(result, matchingSymbols, match2, queryGeometry, filter, params.layerIDs, styleLayersByID, args.bearing, pixelsToTileUnits, returnGeoJSON); - if (!returnGeoJSON) { - result = result.arrayBuffer; - } - return result; }; +function topDown(a, b) { + return b - a; +} + FeatureTree.prototype.filterMatching = function(result, matching, match, queryGeometry, filter, filterLayerIDs, styleLayersByID, bearing, pixelsToTileUnits, returnGeoJSON) { var seen = {}; for (var k = 0; k < matching.length; k++) { @@ -232,7 +234,12 @@ FeatureTree.prototype.filterMatching = function(result, matching, match, queryGe geojsonFeature.layer = styleLayer.serialize({ includeRefProperties: true }); - result.push(geojsonFeature); + var layerResult = result[layerID]; + if (layerResult === undefined) { + layerResult = result[layerID] = []; + } + layerResult.push(geojsonFeature); + } else { result.emplaceBack(match.featureIndex, match.sourceLayerIndex, match.bucketIndex, l); } @@ -240,13 +247,15 @@ FeatureTree.prototype.filterMatching = function(result, matching, match, queryGe } }; -FeatureTree.prototype.makeGeoJSON = function(result, featureIndexArray, styleLayers) { +FeatureTree.prototype.makeGeoJSON = function(featureIndexArray, styleLayers) { if (!this.vtLayers) { if (!this.rawTileData) return []; this.vtLayers = new vt.VectorTile(new Protobuf(new Uint8Array(this.rawTileData))).layers; this.sourceLayerNumberMapping = new StringNumberMapping(this.vtLayers ? Object.keys(this.vtLayers).sort() : ['_geojsonTileLayer']); } + var result = {}; + featureIndexArray = new FilteredFeatureIndexArray(featureIndexArray); var indexes = featureIndexArray.at(0); @@ -264,15 +273,22 @@ FeatureTree.prototype.makeGeoJSON = function(result, featureIndexArray, styleLay var feature = cachedFeatures[featureIndex] = cachedFeatures[featureIndex] || sourceLayer.feature(featureIndex); - var styleLayer = styleLayers[this.numberToLayerIDs[indexes.bucketIndex][indexes.layerIndex]]; + var layerID = this.numberToLayerIDs[indexes.bucketIndex][indexes.layerIndex]; + var styleLayer = styleLayers[layerID]; if (!styleLayer) continue; var geojsonFeature = new GeoJSONFeature(feature, this.z, this.x, this.y); geojsonFeature.layer = styleLayer.serialize({ includeRefProperties: true }); - result.push(geojsonFeature); + var layerResult = result[layerID]; + if (layerResult === undefined) { + layerResult = result[layerID] = []; + } + layerResult.push(geojsonFeature); } + + return result; }; function translate(queryGeometry, translate, translateAnchor, bearing, pixelsToTileUnits) { diff --git a/js/source/source.js b/js/source/source.js index 9baf2a16d6a..9190bc60a2f 100644 --- a/js/source/source.js +++ b/js/source/source.js @@ -67,14 +67,42 @@ exports._getVisibleCoordinates = function() { else return this._pyramid.renderedIDs().map(TileCoord.fromID); }; -exports._queryRenderedVectorFeatures = function(resultFeatures, queryGeometry, params, classes, zoom, bearing, callback) { +function sortTilesIn(a, b) { + var coordA = a.tile.coord; + var coordB = b.tile.coord; + return (coordA.z - coordB.z) || (coordA.y - coordB.y) || (coordA.x - coordB.x); +} + +function mergeRenderedFeatureLayers(tiles) { + var result = tiles[0] || {}; + for (var i = 1; i < tiles.length; i++) { + var tile = tiles[i]; + for (var layerID in tile) { + var tileFeatures = tile[layerID]; + var resultFeatures = result[layerID]; + if (resultFeatures === undefined) { + resultFeatures = result[layerID] = tileFeatures; + } else { + for (var f = 0; f < tileFeatures.length; f++) { + resultFeatures.push(tileFeatures[f]); + } + } + } + } + return result; +} + +exports._queryRenderedVectorFeatures = function(queryGeometry, params, classes, zoom, bearing, callback) { if (!this._pyramid) return callback(null, []); var tilesIn = this._pyramid.tilesIn(queryGeometry); + if (!tilesIn) return callback(null, []); + tilesIn.sort(sortTilesIn); + var styleLayers = this.map.style._layers; var isAsync = callback !== undefined; @@ -99,27 +127,28 @@ exports._queryRenderedVectorFeatures = function(resultFeatures, queryGeometry, p params: params, featureTree: featureTree.data, collisionTile: collisionTile.data, - rawTileData: tileIn.tile.rawTileData.slice() - }, function(err_, data) { - if (data) tileIn.tile.featureTree.makeGeoJSON(resultFeatures, data, styleLayers); - callback(); + rawTileData: tileIn.tile.rawTileData.slice(0) + }, function(err, data) { + callback(err, data ? tileIn.tile.featureTree.makeGeoJSON(data, styleLayers) : {}); }, tileIn.tile.workerID); - }.bind(this), function() { - callback(null); + }.bind(this), function(err, renderedFeatureLayers) { + callback(err, mergeRenderedFeatureLayers(renderedFeatureLayers)); }); } else { + var renderedFeatureLayers = []; for (var r = 0; r < tilesIn.length; r++) { var tileIn = tilesIn[r]; if (!tileIn.tile.loaded || !tileIn.tile.featureTree) continue; - tileIn.tile.featureTree.query(resultFeatures, { + renderedFeatureLayers.push(tileIn.tile.featureTree.query({ queryGeometry: tileIn.queryGeometry, scale: tileIn.scale, tileSize: tileIn.tile.tileSize, bearing: bearing, params: params - }, styleLayers, true); + }, styleLayers, true)); } + return mergeRenderedFeatureLayers(renderedFeatureLayers); } }; diff --git a/js/source/worker.js b/js/source/worker.js index d49ed660a94..3b6710eaa7c 100644 --- a/js/source/worker.js +++ b/js/source/worker.js @@ -218,7 +218,7 @@ util.extend(Worker.prototype, { var collisionTile = new CollisionTile(params.collisionTile, tile.collisionBoxArray); var featureTree = new FeatureTree(params.featureTree, params.rawTileData, collisionTile); - var featureArrayBuffer = featureTree.query(undefined, params, this.styleLayersByID, false); + var featureArrayBuffer = featureTree.query(params, this.styleLayersByID, false).arrayBuffer; callback(null, featureArrayBuffer, [featureArrayBuffer]); } else { callback(null, []); diff --git a/js/source/worker_tile.js b/js/source/worker_tile.js index 8cb93363c1c..2dcb1fd7ab5 100644 --- a/js/source/worker_tile.js +++ b/js/source/worker_tile.js @@ -204,7 +204,7 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback, rawTileData var featureTree_ = featureTree.serialize(); var collisionTile_ = collisionTile.serialize(); - var collisionBoxArray = tile.collisionBoxArray.arrayBuffer.slice(); + var collisionBoxArray = tile.collisionBoxArray.arrayBuffer.slice(0); var transferables = [rawTileData, collisionBoxArray].concat(featureTree.transferables).concat(collisionTile_.transferables); callback(null, { diff --git a/js/style/style.js b/js/style/style.js index af720af6628..a0dc84824b6 100644 --- a/js/style/style.js +++ b/js/style/style.js @@ -410,10 +410,23 @@ Style.prototype = util.inherit(Evented, { }, function(value) { return value !== undefined; }); }, - queryRenderedFeatures: function(queryGeometry, params, classes, zoom, bearing, callback) { - var resultFeatures = []; - var error = null; + _flattenRenderedFeatures: function(sourceResults) { + var features = []; + for (var l = this._order.length - 1; l >= 0; l--) { + var layerID = this._order[l]; + for (var s = 0; s < sourceResults.length; s++) { + var layerFeatures = sourceResults[s][layerID]; + if (layerFeatures) { + for (var f = 0; f < layerFeatures.length; f++) { + features.push(layerFeatures[f]); + } + } + } + } + return features; + }, + queryRenderedFeatures: function(queryGeometry, params, classes, zoom, bearing, callback) { if (params.layer) { params.layerIDs = Array.isArray(params.layer) ? params.layer : [params.layer]; } @@ -422,21 +435,17 @@ Style.prototype = util.inherit(Evented, { if (isAsync) { util.asyncAll(Object.keys(this.sources), function(id, callback) { - var source = this.sources[id]; - source.queryRenderedFeatures(resultFeatures, queryGeometry, params, classes, zoom, bearing, function(err) { - if (err) error = err; - callback(); - }); - }.bind(this), function() { - if (error) return callback(error); - callback(null, resultFeatures); - }); + this.sources[id].queryRenderedFeatures(queryGeometry, params, classes, zoom, bearing, callback); + }.bind(this), function(err, sourceResults) { + if (err) return callback(err); + callback(null, this._flattenRenderedFeatures(sourceResults)); + }.bind(this)); } else { + var sourceResults = []; for (var id in this.sources) { - this.sources[id].queryRenderedFeatures(resultFeatures, queryGeometry, params, classes, zoom, bearing); + sourceResults.push(this.sources[id].queryRenderedFeatures(queryGeometry, params, classes, zoom, bearing)); } - - return resultFeatures; + return this._flattenRenderedFeatures(sourceResults); } }, diff --git a/js/ui/map.js b/js/ui/map.js index a0ea56217a9..9c1d2b1f86b 100644 --- a/js/ui/map.js +++ b/js/ui/map.js @@ -399,7 +399,7 @@ util.extend(Map.prototype, /** @lends Map.prototype */{ * console.log(features); * }); */ - queryRenderedFeatures: function(pointOrBox, params, callback) { + queryRenderedFeaturesAsync: function(pointOrBox, params, callback) { if (!(pointOrBox instanceof Point || Array.isArray(pointOrBox))) { callback = params; params = pointOrBox; @@ -410,7 +410,7 @@ util.extend(Map.prototype, /** @lends Map.prototype */{ return this; }, - queryRenderedFeaturesSync: function(pointOrBox, params) { + queryRenderedFeatures: function(pointOrBox, params) { if (!(pointOrBox instanceof Point || Array.isArray(pointOrBox))) { params = pointOrBox; pointOrBox = undefined; diff --git a/js/util/grid.js b/js/util/grid.js index dd26ac7a153..ecbfb8843f8 100644 --- a/js/util/grid.js +++ b/js/util/grid.js @@ -8,7 +8,8 @@ function Grid(n, extent, padding) { var cells = this.cells = []; if (n instanceof ArrayBuffer) { - var array = new Int32Array(n); + this.arrayBuffer = n; + var array = new Int32Array(this.arrayBuffer); n = array[0]; extent = array[1]; padding = array[2]; @@ -114,6 +115,8 @@ Grid.prototype._convertToCellCoord = function(x) { }; Grid.prototype.toArrayBuffer = function() { + if (this.arrayBuffer) return this.arrayBuffer; + var cells = this.cells; var metadataLength = NUM_PARAMS + this.cells.length + 1 + 1; From a31b379e09d477d7a32dbf75d825bc354b0eb140 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 8 Mar 2016 14:56:54 -0800 Subject: [PATCH 22/48] fix query tests --- package.json | 2 +- test/suite_implementation.js | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index c0656c72446..c6ae1a9d769 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "express": "^4.13.4", "gl": "^2.1.5", "istanbul": "^0.4.2", - "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#7ef3f507d3b48cea9f3524f01a8f2489848821a9", + "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#a71dd3a5e58ff28ae192b8f5fccbd89a46d05493", "nyc": "^6.1.1", "sinon": "^1.15.4", "st": "^1.0.0", diff --git a/test/suite_implementation.js b/test/suite_implementation.js index d19ae00593d..9eae5be025b 100644 --- a/test/suite_implementation.js +++ b/test/suite_implementation.js @@ -47,10 +47,8 @@ module.exports = function(style, options, callback) { tmp.copy(data, end); } - if (options.at) { - map.queryRenderedFeatures(options.at, options, done); - } else if (options.in) { - map.queryRenderedFeatures(options.in, options, done); + if (options.queryGeometry) { + done(null, map.queryRenderedFeatures(options.queryGeometry, options)); } else { done(null, []); } @@ -63,7 +61,8 @@ module.exports = function(style, options, callback) { results = results.map(function (r) { delete r.layer; - return r; + r.geometry = null; + return JSON.parse(JSON.stringify(r)); }); callback(null, data, results); From 14df0f36c20e9a6702e48dab1d0f04b77451a97e Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 8 Mar 2016 15:11:05 -0800 Subject: [PATCH 23/48] split up sync and async query functions --- js/source/geojson_source.js | 1 + js/source/source.js | 101 +++++++++++++++++--------------- js/source/vector_tile_source.js | 1 + js/style/style.js | 32 +++++----- js/ui/map.js | 2 +- 5 files changed, 73 insertions(+), 64 deletions(-) diff --git a/js/source/geojson_source.js b/js/source/geojson_source.js index 76dd8e7ff90..0ea32bbdc78 100644 --- a/js/source/geojson_source.js +++ b/js/source/geojson_source.js @@ -138,6 +138,7 @@ GeoJSONSource.prototype = util.inherit(Evented, /** @lends GeoJSONSource.prototy getTile: Source._getTile, queryRenderedFeatures: Source._queryRenderedVectorFeatures, + queryRenderedFeaturesAsync: Source._queryRenderedVectorFeaturesAsync, querySourceFeatures: Source._querySourceFeatures, _updateData: function() { diff --git a/js/source/source.js b/js/source/source.js index 9190bc60a2f..f70ed175d76 100644 --- a/js/source/source.js +++ b/js/source/source.js @@ -92,64 +92,69 @@ function mergeRenderedFeatureLayers(tiles) { return result; } -exports._queryRenderedVectorFeatures = function(queryGeometry, params, classes, zoom, bearing, callback) { +exports._queryRenderedVectorFeaturesAsync = function(queryGeometry, params, classes, zoom, bearing, callback) { if (!this._pyramid) - return callback(null, []); + return callback(null, {}); var tilesIn = this._pyramid.tilesIn(queryGeometry); - if (!tilesIn) - return callback(null, []); - tilesIn.sort(sortTilesIn); var styleLayers = this.map.style._layers; - var isAsync = callback !== undefined; - - if (isAsync) { - util.asyncAll(tilesIn, function(tileIn, callback) { - - if (!tileIn.tile.loaded || !tileIn.tile.featureTree) return callback(); - - var featureTree = tileIn.tile.featureTree.serialize(); - var collisionTile = tileIn.tile.collisionTile.serialize(); - - this.dispatcher.send('query rendered features', { - uid: tileIn.tile.uid, - source: this.id, - queryGeometry: tileIn.queryGeometry, - scale: tileIn.scale, - tileSize: tileIn.tile.tileSize, - classes: classes, - zoom: zoom, - bearing: bearing, - params: params, - featureTree: featureTree.data, - collisionTile: collisionTile.data, - rawTileData: tileIn.tile.rawTileData.slice(0) - }, function(err, data) { - callback(err, data ? tileIn.tile.featureTree.makeGeoJSON(data, styleLayers) : {}); - }, tileIn.tile.workerID); - }.bind(this), function(err, renderedFeatureLayers) { - callback(err, mergeRenderedFeatureLayers(renderedFeatureLayers)); - }); + util.asyncAll(tilesIn, function(tileIn, callback) { + + if (!tileIn.tile.loaded) return callback(); + + var featureTree = tileIn.tile.featureTree.serialize(); + var collisionTile = tileIn.tile.collisionTile.serialize(); + + this.dispatcher.send('query rendered features', { + uid: tileIn.tile.uid, + source: this.id, + queryGeometry: tileIn.queryGeometry, + scale: tileIn.scale, + tileSize: tileIn.tile.tileSize, + classes: classes, + zoom: zoom, + bearing: bearing, + params: params, + featureTree: featureTree.data, + collisionTile: collisionTile.data, + rawTileData: tileIn.tile.rawTileData.slice(0) + }, function(err, data) { + callback(err, data ? tileIn.tile.featureTree.makeGeoJSON(data, styleLayers) : {}); + }, tileIn.tile.workerID); + }.bind(this), function(err, renderedFeatureLayers) { + callback(err, mergeRenderedFeatureLayers(renderedFeatureLayers)); + }); +}; - } else { - var renderedFeatureLayers = []; - for (var r = 0; r < tilesIn.length; r++) { - var tileIn = tilesIn[r]; - if (!tileIn.tile.loaded || !tileIn.tile.featureTree) continue; - renderedFeatureLayers.push(tileIn.tile.featureTree.query({ - queryGeometry: tileIn.queryGeometry, - scale: tileIn.scale, - tileSize: tileIn.tile.tileSize, - bearing: bearing, - params: params - }, styleLayers, true)); - } - return mergeRenderedFeatureLayers(renderedFeatureLayers); + +exports._queryRenderedVectorFeatures = function(queryGeometry, params, classes, zoom, bearing) { + if (!this._pyramid) + return {}; + + var tilesIn = this._pyramid.tilesIn(queryGeometry); + + tilesIn.sort(sortTilesIn); + + var styleLayers = this.map.style._layers; + + var renderedFeatureLayers = []; + for (var r = 0; r < tilesIn.length; r++) { + var tileIn = tilesIn[r]; + if (!tileIn.tile.loaded) continue; + + renderedFeatureLayers.push(tileIn.tile.featureTree.query({ + queryGeometry: tileIn.queryGeometry, + scale: tileIn.scale, + tileSize: tileIn.tile.tileSize, + bearing: bearing, + params: params + }, styleLayers, true)); } + return mergeRenderedFeatureLayers(renderedFeatureLayers); }; exports._querySourceFeatures = function(params, callback) { diff --git a/js/source/vector_tile_source.js b/js/source/vector_tile_source.js index 9c39499b065..cd1f5544260 100644 --- a/js/source/vector_tile_source.js +++ b/js/source/vector_tile_source.js @@ -54,6 +54,7 @@ VectorTileSource.prototype = util.inherit(Evented, { getTile: Source._getTile, queryRenderedFeatures: Source._queryRenderedVectorFeatures, + queryRenderedFeaturesAsync: Source._queryRenderedVectorFeaturesAsync, querySourceFeatures: Source._querySourceFeatures, _loadTile: function(tile) { diff --git a/js/style/style.js b/js/style/style.js index a0dc84824b6..5cad123ed51 100644 --- a/js/style/style.js +++ b/js/style/style.js @@ -426,27 +426,29 @@ Style.prototype = util.inherit(Evented, { return features; }, - queryRenderedFeatures: function(queryGeometry, params, classes, zoom, bearing, callback) { + queryRenderedFeaturesAsync: function(queryGeometry, params, classes, zoom, bearing, callback) { if (params.layer) { params.layerIDs = Array.isArray(params.layer) ? params.layer : [params.layer]; } - var isAsync = callback !== undefined; + util.asyncAll(Object.keys(this.sources), function(id, callback) { + this.sources[id].queryRenderedFeaturesAsync(queryGeometry, params, classes, zoom, bearing, callback); + }.bind(this), function(err, sourceResults) { + if (err) return callback(err); + callback(null, this._flattenRenderedFeatures(sourceResults)); + }.bind(this)); + }, - if (isAsync) { - util.asyncAll(Object.keys(this.sources), function(id, callback) { - this.sources[id].queryRenderedFeatures(queryGeometry, params, classes, zoom, bearing, callback); - }.bind(this), function(err, sourceResults) { - if (err) return callback(err); - callback(null, this._flattenRenderedFeatures(sourceResults)); - }.bind(this)); - } else { - var sourceResults = []; - for (var id in this.sources) { - sourceResults.push(this.sources[id].queryRenderedFeatures(queryGeometry, params, classes, zoom, bearing)); - } - return this._flattenRenderedFeatures(sourceResults); + queryRenderedFeatures: function(queryGeometry, params, classes, zoom, bearing) { + if (params.layer) { + params.layerIDs = Array.isArray(params.layer) ? params.layer : [params.layer]; + } + + var sourceResults = []; + for (var id in this.sources) { + sourceResults.push(this.sources[id].queryRenderedFeatures(queryGeometry, params, classes, zoom, bearing)); } + return this._flattenRenderedFeatures(sourceResults); }, _remove: function() { diff --git a/js/ui/map.js b/js/ui/map.js index 9c1d2b1f86b..6646b0165e3 100644 --- a/js/ui/map.js +++ b/js/ui/map.js @@ -406,7 +406,7 @@ util.extend(Map.prototype, /** @lends Map.prototype */{ pointOrBox = undefined; } var queryGeometry = this._makeQueryGeometry(pointOrBox); - this.style.queryRenderedFeatures(queryGeometry, params, this._classes, this.transform.zoom, this.transform.angle, callback); + this.style.queryRenderedFeaturesAsync(queryGeometry, params, this._classes, this.transform.zoom, this.transform.angle, callback); return this; }, From cfdda7b7972079ea84de64397157f2923c48bc91 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 8 Mar 2016 15:28:10 -0800 Subject: [PATCH 24/48] fix queryRenderedFeatures for line-gap-width --- js/data/feature_tree.js | 33 +++++++++++++++++++++------------ package.json | 2 +- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index 519b129b90b..dd74c53e9f3 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -103,7 +103,6 @@ function translateDistance(translate) { // Finds features in this tile at a particular position. FeatureTree.prototype.query = function(args, styleLayersByID, returnGeoJSON) { if (!this.vtLayers) { - if (!this.rawTileData) return []; this.vtLayers = new vt.VectorTile(new Protobuf(new Uint8Array(this.rawTileData))).layers; this.sourceLayerNumberMapping = new StringNumberMapping(this.vtLayers ? Object.keys(this.vtLayers).sort() : ['_geojsonTileLayer']); } @@ -122,14 +121,15 @@ FeatureTree.prototype.query = function(args, styleLayersByID, returnGeoJSON) { var additionalRadius = 0; for (var id in styleLayersByID) { var styleLayer = styleLayersByID[id]; + var paint = styleLayer.paint; var styleLayerDistance = 0; if (styleLayer.type === 'line') { - styleLayerDistance = styleLayer.paint['line-width'] / 2 + Math.abs(styleLayer.paint['line-offset']) + translateDistance(styleLayer.paint['line-translate']); + styleLayerDistance = getLineWidth(paint) / 2 + Math.abs(paint['line-offset']) + translateDistance(paint['line-translate']); } else if (styleLayer.type === 'fill') { - styleLayerDistance = translateDistance(styleLayer.paint['fill-translate']); + styleLayerDistance = translateDistance(paint['fill-translate']); } else if (styleLayer.type === 'circle') { - styleLayerDistance = styleLayer.paint['circle-radius'] + translateDistance(styleLayer.paint['circle-translate']); + styleLayerDistance = paint['circle-radius'] + translateDistance(paint['circle-translate']); } additionalRadius = Math.max(additionalRadius, styleLayerDistance * pixelsToTileUnits); } @@ -167,6 +167,14 @@ function topDown(a, b) { return b - a; } +function getLineWidth(paint) { + if (paint['line-gap-width'] > 0) { + return paint['line-gap-width'] + 2 * paint['line-width']; + } else { + return paint['line-width']; + } +} + FeatureTree.prototype.filterMatching = function(result, matching, match, queryGeometry, filter, filterLayerIDs, styleLayersByID, bearing, pixelsToTileUnits, returnGeoJSON) { var seen = {}; for (var k = 0; k < matching.length; k++) { @@ -204,27 +212,29 @@ FeatureTree.prototype.filterMatching = function(result, matching, match, queryGe if (!geometry) geometry = loadGeometry(feature); + var paint = styleLayer.paint; + if (styleLayer.type === 'line') { translatedPolygon = translate(queryGeometry, - styleLayer.paint['line-translate'], styleLayer.paint['line-translate-anchor'], + paint['line-translate'], paint['line-translate-anchor'], bearing, pixelsToTileUnits); - var halfWidth = styleLayer.paint['line-width'] / 2 * pixelsToTileUnits; - if (styleLayer.paint['line-offset']) { - geometry = offsetLine(geometry, styleLayer.paint['line-offset'] * pixelsToTileUnits); + var halfWidth = getLineWidth(paint) / 2 * pixelsToTileUnits; + if (paint['line-offset']) { + geometry = offsetLine(geometry, paint['line-offset'] * pixelsToTileUnits); } if (!polygonIntersectsBufferedMultiLine(translatedPolygon, geometry, halfWidth)) continue; } else if (styleLayer.type === 'fill') { translatedPolygon = translate(queryGeometry, - styleLayer.paint['fill-translate'], styleLayer.paint['fill-translate-anchor'], + paint['fill-translate'], paint['fill-translate-anchor'], bearing, pixelsToTileUnits); if (!polygonIntersectsMultiPolygon(translatedPolygon, geometry)) continue; } else if (styleLayer.type === 'circle') { translatedPolygon = translate(queryGeometry, - styleLayer.paint['circle-translate'], styleLayer.paint['circle-translate-anchor'], + paint['circle-translate'], paint['circle-translate-anchor'], bearing, pixelsToTileUnits); - var circleRadius = styleLayer.paint['circle-radius'] * pixelsToTileUnits; + var circleRadius = paint['circle-radius'] * pixelsToTileUnits; if (!polygonIntersectsBufferedMultiPoint(translatedPolygon, geometry, circleRadius)) continue; } } @@ -249,7 +259,6 @@ FeatureTree.prototype.filterMatching = function(result, matching, match, queryGe FeatureTree.prototype.makeGeoJSON = function(featureIndexArray, styleLayers) { if (!this.vtLayers) { - if (!this.rawTileData) return []; this.vtLayers = new vt.VectorTile(new Protobuf(new Uint8Array(this.rawTileData))).layers; this.sourceLayerNumberMapping = new StringNumberMapping(this.vtLayers ? Object.keys(this.vtLayers).sort() : ['_geojsonTileLayer']); } diff --git a/package.json b/package.json index c6ae1a9d769..9d8127ae84d 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "express": "^4.13.4", "gl": "^2.1.5", "istanbul": "^0.4.2", - "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#a71dd3a5e58ff28ae192b8f5fccbd89a46d05493", + "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#c6223ed9aeb49b7fb4a880b1a1921e20a4255911", "nyc": "^6.1.1", "sinon": "^1.15.4", "st": "^1.0.0", From 5f60ecc2f9d99589bff3a1549af118ccd678b896 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 8 Mar 2016 15:49:19 -0800 Subject: [PATCH 25/48] fix queryRenderedFeature no-ops for raster sources --- js/source/image_source.js | 17 ----------------- js/source/raster_tile_source.js | 8 -------- js/source/video_source.js | 8 -------- js/style/style.js | 12 ++++++++++-- 4 files changed, 10 insertions(+), 35 deletions(-) diff --git a/js/source/image_source.js b/js/source/image_source.js index b5c10f47dce..1bf34aee491 100644 --- a/js/source/image_source.js +++ b/js/source/image_source.js @@ -151,23 +151,6 @@ ImageSource.prototype = util.inherit(Evented, { return this.tile; }, - /** - * An ImageSource doesn't have any vector features that could - * be selectable, so always return an empty array. - * @private - */ - queryRenderedFeatures: function(point, params, callback) { - return callback(null, []); - }, - - /** - * An ImageSource doesn't have any tiled data. - * @private - */ - querySourceFeatures: function(params, callback) { - return callback(null, []); - }, - serialize: function() { return { type: 'image', diff --git a/js/source/raster_tile_source.js b/js/source/raster_tile_source.js index 8417c31230f..3d7afef034a 100644 --- a/js/source/raster_tile_source.js +++ b/js/source/raster_tile_source.js @@ -114,13 +114,5 @@ RasterTileSource.prototype = util.inherit(Evented, { _unloadTile: function(tile) { if (tile.texture) this.map.painter.saveTexture(tile.texture); - }, - - queryRenderedFeatures: function(point, params, callback) { - callback(null, []); - }, - - querySourceFeatures: function(params, callback) { - return callback(null, []); } }); diff --git a/js/source/video_source.js b/js/source/video_source.js index 0db7fbfdee1..f58f4040ec9 100644 --- a/js/source/video_source.js +++ b/js/source/video_source.js @@ -179,14 +179,6 @@ VideoSource.prototype = util.inherit(Evented, /** @lends VideoSource.prototype * return this.tile; }, - queryRenderedFeatures: function(point, params, callback) { - return callback(null, []); - }, - - getSourceFeatures: function(params, callback) { - return callback(null, []); - }, - serialize: function() { return { type: 'video', diff --git a/js/style/style.js b/js/style/style.js index 5cad123ed51..09895fa2b7d 100644 --- a/js/style/style.js +++ b/js/style/style.js @@ -432,7 +432,12 @@ Style.prototype = util.inherit(Evented, { } util.asyncAll(Object.keys(this.sources), function(id, callback) { - this.sources[id].queryRenderedFeaturesAsync(queryGeometry, params, classes, zoom, bearing, callback); + var source = this.sources[id]; + if (source.queryRenderedFeaturesAsync) { + source.queryRenderedFeaturesAsync(queryGeometry, params, classes, zoom, bearing, callback); + } else { + callback(null, {}); + } }.bind(this), function(err, sourceResults) { if (err) return callback(err); callback(null, this._flattenRenderedFeatures(sourceResults)); @@ -446,7 +451,10 @@ Style.prototype = util.inherit(Evented, { var sourceResults = []; for (var id in this.sources) { - sourceResults.push(this.sources[id].queryRenderedFeatures(queryGeometry, params, classes, zoom, bearing)); + var source = this.sources[id]; + if (source.queryRenderedFeatures) { + sourceResults.push(source.queryRenderedFeatures(queryGeometry, params, classes, zoom, bearing)); + } } return this._flattenRenderedFeatures(sourceResults); }, From 066462eeb06a2b0d0b3128989b5e9a616a5f422a Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 8 Mar 2016 15:57:34 -0800 Subject: [PATCH 26/48] remove callback from querySourceFeatures --- js/source/source.js | 6 +++--- js/ui/map.js | 18 ++++++------------ 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/js/source/source.js b/js/source/source.js index f70ed175d76..5a8cf412e3e 100644 --- a/js/source/source.js +++ b/js/source/source.js @@ -157,9 +157,9 @@ exports._queryRenderedVectorFeatures = function(queryGeometry, params, classes, return mergeRenderedFeatureLayers(renderedFeatureLayers); }; -exports._querySourceFeatures = function(params, callback) { +exports._querySourceFeatures = function(params) { if (!this._pyramid) { - return callback(null, []); + return []; } var pyramid = this._pyramid; @@ -179,7 +179,7 @@ exports._querySourceFeatures = function(params, callback) { } } - callback(null, result); + return result; }; /* diff --git a/js/ui/map.js b/js/ui/map.js index 6646b0165e3..0c5f2a677fd 100644 --- a/js/ui/map.js +++ b/js/ui/map.js @@ -459,20 +459,14 @@ util.extend(Map.prototype, /** @lends Map.prototype */{ * @param {Object} params * @param {string} [params.sourceLayer] The name of the vector tile layer to get features from. * @param {Array} [params.filter] A mapbox-gl-style-spec filter. - * @param {callback} callback function that receives the results * - * @returns {Map} `this` + * @returns {Array} features - An array of [GeoJSON](http://geojson.org/) features matching the query parameters. The GeoJSON properties of each feature are taken from the original source. Each feature object also contains a top-level `layer` property whose value is an object representing the style layer to which the feature belongs. Layout and paint properties in this object contain values which are fully evaluated for the given zoom level and feature. */ - querySourceFeatures: function(sourceID, params, callback) { + querySourceFeatures: function(sourceID, params) { var source = this.getSource(sourceID); - - if (!source) { - return callback("No source with id '" + sourceID + "'.", []); - } - - source.querySourceFeatures(params, callback); - - return this; + return source && source.querySourceFeatures ? + source.querySourceFeatures(params) : + []; }, /** @@ -1005,7 +999,7 @@ util.extend(Map.prototype, /** @lends Map.prototype */{ /** - * Callback to receive results from `Map#queryRenderFeatures` and `Map#querySourceFeatures`. + * Callback to receive results from `Map#queryRenderFeaturesAsync` * * Note: because features come from vector tiles or GeoJSON data that is converted to vector tiles internally, the returned features will be: * From 099ec01c7108c671c387cf5ba818dba6f69aba13 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 8 Mar 2016 16:27:41 -0800 Subject: [PATCH 27/48] fix symbol queryRenderedFeatures bug --- js/symbol/collision_tile.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/symbol/collision_tile.js b/js/symbol/collision_tile.js index ae7cc7fc3ec..da5d7737990 100644 --- a/js/symbol/collision_tile.js +++ b/js/symbol/collision_tile.js @@ -198,6 +198,9 @@ CollisionTile.prototype.queryRenderedSymbols = function(minX, minY, maxX, maxY, queryBox.y2 = maxY - minY; queryBox.maxScale = scale; + // maxScale is stored using a Float32. Convert `scale` to the stored Float32 value. + scale = queryBox.maxScale; + var searchBox = [ anchorPoint.x + queryBox.x1 / scale, anchorPoint.y + queryBox.y1 / scale * this.yStretch, From 1625f6d86e18ae2f7bfa0e2ff28856f412a0fe2d Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 8 Mar 2016 17:15:47 -0800 Subject: [PATCH 28/48] fix tests for queryRenderedFeatures remove all FeatureTree tests. FeatureTree is covered by the query render tests. It's really hard to construct and verify test cases without visual help. Whenever the FeatureTree tests break its hard to tell if its a broken test or a broken implementation. It's way easier to create good query tests. --- js/symbol/collision_feature.js | 28 ++-- test/js/data/feature_tree.test.js | 227 ---------------------------- test/js/data/symbol_bucket.test.js | 15 +- test/js/style/style.test.js | 83 +++++----- test/js/symbol/collision_feature.js | 68 +++++---- test/js/ui/map.test.js | 10 +- 6 files changed, 108 insertions(+), 323 deletions(-) delete mode 100644 test/js/data/feature_tree.test.js diff --git a/js/symbol/collision_feature.js b/js/symbol/collision_feature.js index e447c5ab4f7..fae27f982c0 100644 --- a/js/symbol/collision_feature.js +++ b/js/symbol/collision_feature.js @@ -34,19 +34,19 @@ function CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceL var height = y2 - y1; var length = x2 - x1; - if (height <= 0) return; - - // set minimum box height to avoid very many small labels - height = Math.max(10 * boxScale, height); - - if (straight) { - // used for icon labels that are aligned with the line, but don't curve along it - var vector = line[anchor.segment + 1].sub(line[anchor.segment])._unit()._mult(length); - var straightLine = [anchor.sub(vector), anchor.add(vector)]; - this._addLineCollisionBoxes(collisionBoxArray, straightLine, anchor, 0, length, height, featureIndex, sourceLayerIndex, bucketIndex); - } else { - // used for text labels that curve along a line - this._addLineCollisionBoxes(collisionBoxArray, line, anchor, anchor.segment, length, height, featureIndex, sourceLayerIndex, bucketIndex); + if (height > 0) { + // set minimum box height to avoid very many small labels + height = Math.max(10 * boxScale, height); + + if (straight) { + // used for icon labels that are aligned with the line, but don't curve along it + var vector = line[anchor.segment + 1].sub(line[anchor.segment])._unit()._mult(length); + var straightLine = [anchor.sub(vector), anchor.add(vector)]; + this._addLineCollisionBoxes(collisionBoxArray, straightLine, anchor, 0, length, height, featureIndex, sourceLayerIndex, bucketIndex); + } else { + // used for text labels that curve along a line + this._addLineCollisionBoxes(collisionBoxArray, line, anchor, anchor.segment, length, height, featureIndex, sourceLayerIndex, bucketIndex); + } } } else { @@ -117,7 +117,7 @@ CollisionFeature.prototype._addLineCollisionBoxes = function(collisionBoxArray, var p0 = line[index]; var p1 = line[index + 1]; - var boxAnchorPoint = p1.sub(p0)._unit()._mult(segmentBoxDistance)._add(p0); + var boxAnchorPoint = p1.sub(p0)._unit()._mult(segmentBoxDistance)._add(p0)._round(); var distanceToInnerEdge = Math.max(Math.abs(boxDistanceToAnchor - firstBoxOffset) - step / 2, 0); var maxScale = labelLength / 2 / distanceToInnerEdge; diff --git a/test/js/data/feature_tree.test.js b/test/js/data/feature_tree.test.js deleted file mode 100644 index 0934e4cbc6b..00000000000 --- a/test/js/data/feature_tree.test.js +++ /dev/null @@ -1,227 +0,0 @@ -'use strict'; - -var test = require('tap').test; -var vt = require('vector-tile'); -var fs = require('fs'); -var Point = require('point-geometry'); -var Protobuf = require('pbf'); -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) { - return vt.VectorTileFeature.types[feature.type]; - } - function getGeometry(feature) { - return feature.loadGeometry(); - } - var ft = new FeatureTree(getGeometry, getType, new CollisionTile(0, 0)); - var feature = tile.layers.road.feature(0); - t.ok(feature); - t.ok(ft, 'can be created'); - ft.insert(feature.bbox(), ['road'], feature); - t.deepEqual(ft.query({ - scale: 1, - tileSize: 512, - params: { }, - queryGeometry: [new Point(0, 0)] - }, styleLayers), []); - t.end(); -}); - -test('featuretree with args', 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) { - return vt.VectorTileFeature.types[feature.type]; - } - function getGeometry(feature) { - return feature.loadGeometry(); - } - var ft = new FeatureTree(getGeometry, getType, new CollisionTile(0, 0)); - var feature = tile.layers.road.feature(0); - t.ok(feature); - t.ok(ft, 'can be created'); - ft.insert(feature.bbox(), ['road'], feature); - t.deepEqual(ft.query({ - params: {}, - scale: 1, - tileSize: 512, - queryGeometry: [new Point(0, 0)] - }, styleLayers), []); - t.end(); -}); - -test('featuretree point query', function(t) { - var tile = new vt.VectorTile(new Protobuf(new Uint8Array(fs.readFileSync(path.join(__dirname, '/../../fixtures/mbsv5-6-18-23.vector.pbf'))))); - var ft = new FeatureTree({ x: 18, y: 23, z: 6 }, 1, new CollisionTile(0, 0)); - - for (var i = 0; i < tile.layers.water._features.length; i++) { - var feature = tile.layers.water.feature(i); - ft.insert(feature.bbox(), ['water'], feature); - } - var features = ft.query({ - source: "mapbox.mapbox-streets-v5", - scale: 1.4142135624, - tileSize: 512, - params: { - includeGeometry: true - }, - queryGeometry: [new Point(-180, 1780)] - }, 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) { - var tile = new vt.VectorTile(new Protobuf(new Uint8Array(fs.readFileSync(path.join(__dirname, '/../../fixtures/mbsv5-6-18-23.vector.pbf'))))); - var ft = new FeatureTree({ x: 18, y: 23, z: 6 }, 1, new CollisionTile(0, 0)); - - for (var i = 0; i < tile.layers.water._features.length; i++) { - var feature = tile.layers.water.feature(i); - ft.insert(feature.bbox(), ['water'], feature); - } - - var features = ft.query({ - source: "mapbox.mapbox-streets-v5", - scale: 1.4142135624, - tileSize: 512, - params: { - includeGeometry: true - }, - queryGeometry: [ - new Point(0, 3072), - new Point(2048, 3072), - new Point(2048, 4096), - new Point(0, 4096), - new Point(0, 3072) - ] - }, 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) { - var tile = new vt.VectorTile(new Protobuf(new Uint8Array(fs.readFileSync(path.join(__dirname, '/../../fixtures/mbsv5-6-18-23.vector.pbf'))))); - function getType(feature) { - return vt.VectorTileFeature.types[feature.type]; - } - function getGeometry(feature) { - return feature.loadGeometry(); - } - var ft = new FeatureTree(getGeometry, getType, new CollisionTile(0, 0)); - - for (var i = 0; i < tile.layers.water._features.length; i++) { - var feature = tile.layers.water.feature(i); - ft.insert(feature.bbox(), ['water'], feature); - } - - var features = ft.query({ - source: "mapbox.mapbox-streets-v5", - scale: 1.4142135624, - tileSize: 512, - params: { - layerIds: ['water'] - }, - queryGeometry: [new Point(-180, 1780)] - }, styleLayers); - - t.equal(features.length, 1); - - var features2 = ft.query({ - source: "mapbox.mapbox-streets-v5", - scale: 1.4142135624, - tileSize: 512, - params: { - layerIds: ['none'] - }, - queryGeometry: [new Point(1842, 2014)] - }, styleLayers); - - t.equal(features2.length, 0); - t.end(); -}); - - -test('featuretree query with filter', 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) { - return vt.VectorTileFeature.types[feature.type]; - } - function getGeometry(feature) { - return feature.loadGeometry(); - } - var ft = new FeatureTree(getGeometry, getType, new CollisionTile(0, 0)); - - for (var i = 0; i < tile.layers.water._features.length; i++) { - var feature = tile.layers.water.feature(i); - ft.insert(feature.bbox(), ['water'], feature); - } - - var features = ft.query({ - source: "mapbox.mapbox-streets-v5", - scale: 1.4142135624, - tileSize: 512, - params: { - filter: ['==', '$type', 'Polygon'] - }, - queryGeometry: [new Point(-180, 1780)] - }, styleLayers); - - t.equal(features.length, 1); - - var features2 = ft.query({ - source: "mapbox.mapbox-streets-v5", - scale: 1.4142135624, - tileSize: 512, - params: { - filter: ['!=', '$type', 'Polygon'] - }, - queryGeometry: [new Point(1842, 2014)] - }, styleLayers); - - t.equal(features2.length, 0); - t.end(); -}); diff --git a/test/js/data/symbol_bucket.test.js b/test/js/data/symbol_bucket.test.js index 750f9c641e1..bf94968f142 100644 --- a/test/js/data/symbol_bucket.test.js +++ b/test/js/data/symbol_bucket.test.js @@ -7,6 +7,7 @@ var Protobuf = require('pbf'); var VectorTile = require('vector-tile').VectorTile; var SymbolBucket = require('../../../js/data/bucket/symbol_bucket'); var Collision = require('../../../js/symbol/collision_tile'); +var CollisionBoxArray = require('../../../js/symbol/collision_box'); var GlyphAtlas = require('../../../js/symbol/glyph_atlas'); // Load a point feature from fixture tile. @@ -17,7 +18,8 @@ var glyphs = JSON.parse(fs.readFileSync(path.join(__dirname, '/../../fixtures/fo test('SymbolBucket', function(t) { /*eslint new-cap: 0*/ var buffers = {}; - var collision = new Collision(0, 0); + var collisionBoxArray = new CollisionBoxArray(); + var collision = new Collision(0, 0, collisionBoxArray); var atlas = new GlyphAtlas(1024, 1024); for (var id in glyphs) { glyphs[id].bitmap = true; @@ -31,6 +33,7 @@ test('SymbolBucket', function(t) { buffers: buffers, overscaling: 1, zoom: 0, + collisionBoxArray: collisionBoxArray, layer: { id: 'test', type: 'symbol', layout: {'text-font': ['Test'] }}, tileExtent: 4096 }); @@ -44,16 +47,16 @@ test('SymbolBucket', function(t) { var bucketB = bucketSetup(); // add feature from bucket A - var a = JSON.stringify(collision); + var a = collision.grid.keys.length; t.equal(bucketA.populateBuffers(collision, stacks), undefined); - var b = JSON.stringify(collision); + var b = collision.grid.keys.length; t.notEqual(a, b, 'places feature'); // add same feature from bucket B - a = JSON.stringify(collision); + var a2 = collision.grid.keys.length; t.equal(bucketB.populateBuffers(collision, stacks), undefined); - b = JSON.stringify(collision); - t.equal(a, b, 'detects collision and does not place feature'); + var b2 = collision.grid.keys.length; + t.equal(a2, b2, 'detects collision and does not place feature'); t.end(); }); diff --git a/test/js/style/style.test.js b/test/js/style/style.test.js index 830b264657e..793be7d4064 100644 --- a/test/js/style/style.test.js +++ b/test/js/style/style.test.js @@ -871,7 +871,7 @@ test('Style#setLayerZoomRange', function(t) { t.end(); }); -test('Style#queryRenderedFeatures - race condition', function(t) { +test('Style#queryRenderedFeaturesAsync - race condition', function(t) { var style = new Style({ "version": 8, "sources": { @@ -899,7 +899,7 @@ test('Style#queryRenderedFeatures - race condition', function(t) { style._cascade([]); style._recalculate(0); - style.sources.mapbox.queryRenderedFeatures = function(position, params, classes, zoom, bearing, callback) { + style.sources.mapbox.queryRenderedFeaturesAsync = function(position, params, classes, zoom, bearing, callback) { var features = [{ type: 'Feature', layer: 'land', @@ -911,8 +911,8 @@ test('Style#queryRenderedFeatures - race condition', function(t) { }, 10); }; - t.test('queryRenderedFeatures race condition', function(t) { - style.queryRenderedFeatures([256, 256], {}, {}, 0, 0, function(err, results) { + t.test('queryRenderedFeaturesAsync race condition', function(t) { + style.queryRenderedFeaturesAsync([256, 256], {}, {}, 0, 0, function(err, results) { t.error(err); t.equal(results.length, 0); t.end(); @@ -923,7 +923,7 @@ test('Style#queryRenderedFeatures - race condition', function(t) { }); }); -test('Style#queryRenderedFeatures', function(t) { +test('Style#queryRenderedFeaturesAsync', function(t) { var style = new Style({ "version": 8, "sources": { @@ -959,31 +959,36 @@ test('Style#queryRenderedFeatures', function(t) { style._cascade([]); style._recalculate(0); - style.sources.mapbox.queryRenderedFeatures = function(position, params, classes, zoom, bearing, callback) { - var features = [{ - type: 'Feature', - layer: 'land', - geometry: { - type: 'Polygon' - } - }, { - type: 'Feature', - layer: 'land', - geometry: { - type: 'Point' - } - }, { - type: 'Feature', - layer: 'landref', - geometry: { - type: 'Point' - } - }]; + style.sources.mapbox.queryRenderedFeaturesAsync = function(position, params, classes, zoom, bearing, callback) { + var features = { + 'land': [{ + type: 'Feature', + layer: style._layers.land, + geometry: { + type: 'Polygon' + } + }, { + type: 'Feature', + layer: style._layers.land, + geometry: { + type: 'Point' + } + }], + 'landref': [{ + type: 'Feature', + layer: style._layers.landref, + geometry: { + type: 'Line' + } + }] + }; if (params.layer) { - features = features.filter(function(f) { - return params.layerIds.indexOf(f.layer) > -1; - }); + for (var l in features) { + if (params.layerIDs.indexOf(l) < 0) { + delete features[l]; + } + } } setTimeout(function() { @@ -992,15 +997,15 @@ test('Style#queryRenderedFeatures', function(t) { }; t.test('returns feature type', function(t) { - style.queryRenderedFeatures([{column: 1, row: 1, zoom: 1}], {}, {}, 0, 0, function(err, results) { + style.queryRenderedFeaturesAsync([{column: 1, row: 1, zoom: 1}], {}, {}, 0, 0, function(err, results) { t.error(err); - t.equal(results[0].geometry.type, 'Polygon'); + t.equal(results[0].geometry.type, 'Line'); t.end(); }); }); t.test('filters by `layer` option', function(t) { - style.queryRenderedFeatures([{column: 1, row: 1, zoom: 1}], {layer: 'land'}, {}, 0, 0, function(err, results) { + style.queryRenderedFeaturesAsync([{column: 1, row: 1, zoom: 1}], {layer: 'land'}, {}, 0, 0, function(err, results) { t.error(err); t.equal(results.length, 2); t.end(); @@ -1008,7 +1013,7 @@ test('Style#queryRenderedFeatures', function(t) { }); t.test('includes layout properties', function(t) { - style.queryRenderedFeatures([{column: 1, row: 1, zoom: 1}], {}, {}, 0, 0, function(err, results) { + style.queryRenderedFeaturesAsync([{column: 1, row: 1, zoom: 1}], {}, {}, 0, 0, function(err, results) { t.error(err); var layout = results[0].layer.layout; t.deepEqual(layout['line-cap'], 'round'); @@ -1017,19 +1022,19 @@ test('Style#queryRenderedFeatures', function(t) { }); t.test('includes paint properties', function(t) { - style.queryRenderedFeatures([{column: 1, row: 1, zoom: 1}], {}, {}, 0, 0, function(err, results) { + style.queryRenderedFeaturesAsync([{column: 1, row: 1, zoom: 1}], {}, {}, 0, 0, function(err, results) { t.error(err); - t.deepEqual(results[0].layer.paint['line-color'], 'red'); + t.deepEqual(results[2].layer.paint['line-color'], [1, 0, 0, 1]); t.end(); }); }); t.test('ref layer inherits properties', function(t) { - style.queryRenderedFeatures([{column: 1, row: 1, zoom: 1}], {}, {}, 0, 0, function(err, results) { + style.queryRenderedFeaturesAsync([{column: 1, row: 1, zoom: 1}], {}, {}, 0, 0, function(err, results) { t.error(err); var layer = results[1].layer; - var refLayer = results[2].layer; + var refLayer = results[0].layer; t.deepEqual(layer.layout, refLayer.layout); t.deepEqual(layer.type, refLayer.type); t.deepEqual(layer.id, refLayer.ref); @@ -1040,10 +1045,10 @@ test('Style#queryRenderedFeatures', function(t) { }); t.test('includes metadata', function(t) { - style.queryRenderedFeatures([{column: 1, row: 1, zoom: 1}], {}, {}, 0, 0, function(err, results) { + style.queryRenderedFeaturesAsync([{column: 1, row: 1, zoom: 1}], {}, {}, 0, 0, function(err, results) { t.error(err); - var layer = results[0].layer; + var layer = results[1].layer; t.equal(layer.metadata.something, 'else'); t.end(); @@ -1051,7 +1056,7 @@ test('Style#queryRenderedFeatures', function(t) { }); t.test('include multiple layers', function(t) { - style.queryRenderedFeatures([{column: 1, row: 1, zoom: 1}], {layer: ['land', 'landref']}, {}, 0, 0, function(err, results) { + style.queryRenderedFeaturesAsync([{column: 1, row: 1, zoom: 1}], {layer: ['land', 'landref']}, {}, 0, 0, function(err, results) { t.error(err); t.equals(results.length, 3); t.end(); diff --git a/test/js/symbol/collision_feature.js b/test/js/symbol/collision_feature.js index f41324800c4..3da2ca447e6 100644 --- a/test/js/symbol/collision_feature.js +++ b/test/js/symbol/collision_feature.js @@ -4,9 +4,12 @@ var test = require('tap').test; var CollisionFeature = require('../../../js/symbol/collision_feature'); var Anchor = require('../../../js/symbol/anchor'); var Point = require('point-geometry'); +var CollisionBoxArray = require('../../../js/symbol/collision_box'); test('CollisionFeature', function(t) { + var collisionBoxArray = new CollisionBoxArray(); + var shapedText = { left: -50, top: -10, @@ -14,17 +17,14 @@ test('CollisionFeature', function(t) { bottom: 10 }; - var feature = {}; - var layerIDs = []; - test('point label', function(t) { var point = new Point(500, 0); var anchor = new Anchor(point.x, point.y, 0, undefined); - var cf = new CollisionFeature([point], anchor, feature, layerIDs, shapedText, 1, 0, false); - t.equal(cf.boxes.length, 1); + var cf = new CollisionFeature(collisionBoxArray, [point], anchor, 0, 0, 0, shapedText, 1, 0, false); + t.equal(cf.boxEndIndex - cf.boxStartIndex, 1); - var box = cf.boxes[0]; + var box = collisionBoxArray.at(cf.boxStartIndex); t.equal(box.x1, -50); t.equal(box.x2, 50); t.equal(box.y1, -10); @@ -35,27 +35,27 @@ test('CollisionFeature', function(t) { test('line label', function(t) { var line = [new Point(0, 0), new Point(500, 100), new Point(510, 90), new Point(700, 0)]; var anchor = new Anchor(505, 95, 0, 1); - var cf = new CollisionFeature(line, anchor, feature, layerIDs, shapedText, 1, 0, true); - var boxPoints = cf.boxes.map(pluckAnchorPoint); + var cf = new CollisionFeature(collisionBoxArray, line, anchor, 0, 0, 0, shapedText, 1, 0, true); + var boxPoints = pluckAnchorPoints(cf); t.deepEqual(boxPoints, [ - { x: 467.71052542517856, y: 93.54210508503571 }, - { x: 477.51633218208775, y: 95.50326643641756 }, - { x: 487.32213893899694, y: 97.4644277877994 }, - { x: 497.12794569590613, y: 99.42558913918124 }, - { x: 505, y: 95 }, - { x: 512.6469868459704, y: 88.74616412559295 }, - { x: 521.6843652349058, y: 84.46530067820251 }, - { x: 530.7217436238412, y: 80.18443723081207 }, - { x: 539.7591220127766, y: 75.90357378342162 }, - { x: 548.7965004017119, y: 71.62271033603116 } ]); + { x: 468, y: 94}, + { x: 478, y: 96}, + { x: 487, y: 97}, + { x: 497, y: 99}, + { x: 505, y: 95}, + { x: 513, y: 89}, + { x: 522, y: 84}, + { x: 531, y: 80}, + { x: 540, y: 76}, + { x: 549, y: 72} ]); t.end(); }); test('vertical line label', function(t) { var line = [new Point(0, 0), new Point(0, 100), new Point(0, 111), new Point(0, 112), new Point(0, 200)]; var anchor = new Anchor(0, 110, 0, 1); - var cf = new CollisionFeature(line, anchor, feature, layerIDs, shapedText, 1, 0, true); - var boxPoints = cf.boxes.map(pluckAnchorPoint); + var cf = new CollisionFeature(collisionBoxArray, line, anchor, 0, 0, 0, shapedText, 1, 0, true); + var boxPoints = pluckAnchorPoints(cf); t.deepEqual(boxPoints, [ { x: 0, y: 70 }, { x: 0, y: 80 }, @@ -80,8 +80,8 @@ test('CollisionFeature', function(t) { var line = [new Point(0, 0), new Point(500, 100), new Point(510, 90), new Point(700, 0)]; var anchor = new Anchor(505, 95, 0, 1); - var cf = new CollisionFeature(line, anchor, feature, layerIDs, shapedText, 1, 0, true); - t.equal(cf.boxes.length, 0); + var cf = new CollisionFeature(collisionBoxArray, line, anchor, 0, 0, 0, shapedText, 1, 0, true); + t.equal(cf.boxEndIndex - cf.boxStartIndex, 0); t.end(); }); @@ -95,8 +95,8 @@ test('CollisionFeature', function(t) { var line = [new Point(0, 0), new Point(500, 100), new Point(510, 90), new Point(700, 0)]; var anchor = new Anchor(505, 95, 0, 1); - var cf = new CollisionFeature(line, anchor, feature, layerIDs, shapedText, 1, 0, true); - t.equal(cf.boxes.length, 0); + var cf = new CollisionFeature(collisionBoxArray, line, anchor, 0, 0, 0, shapedText, 1, 0, true); + t.equal(cf.boxEndIndex - cf.boxStartIndex, 0); t.end(); }); @@ -110,8 +110,8 @@ test('CollisionFeature', function(t) { var line = [new Point(0, 0), new Point(500, 100), new Point(510, 90), new Point(700, 0)]; var anchor = new Anchor(505, 95, 0, 1); - var cf = new CollisionFeature(line, anchor, feature, layerIDs, shapedText, 1, 0, true); - t.ok(cf.boxes.length < 30); + var cf = new CollisionFeature(collisionBoxArray, line, anchor, 0, 0, 0, shapedText, 1, 0, true); + t.ok(cf.boxEndIndex - cf.boxStartIndex < 30); t.end(); }); @@ -119,14 +119,18 @@ test('CollisionFeature', function(t) { var line = [new Point(3103, 4068), new Point(3225.6206896551726, 4096)]; var anchor = new Anchor(3144.5959947505007, 4077.498298013894, 0.22449735614507618, 0); var shaping = { right: 256, left: 0, bottom: 256, top: 0 }; - var cf = new CollisionFeature(line, anchor, feature, layerIDs, shaping, 1, 0, true); - t.equal(cf.boxes.length, 1); + var cf = new CollisionFeature(collisionBoxArray, line, anchor, 0, 0, 0, shaping, 1, 0, true); + t.equal(cf.boxEndIndex - cf.boxStartIndex, 1); t.end(); }); t.end(); -}); -function pluckAnchorPoint(b) { - return { x: b.anchorPoint.x, y: b.anchorPoint.y }; -} + function pluckAnchorPoints(cf) { + var result = []; + for (var i = cf.boxStartIndex; i < cf.boxEndIndex; i++) { + result.push(collisionBoxArray.at(i).anchorPoint); + } + return result; + } +}); diff --git a/test/js/ui/map.test.js b/test/js/ui/map.test.js index 0687e021e2b..25958d77fa2 100644 --- a/test/js/ui/map.test.js +++ b/test/js/ui/map.test.js @@ -484,7 +484,7 @@ test('Map', function(t) { }); - t.test('#queryRenderedFeatures', function(t) { + t.test('#queryRenderedFeaturesAsync', function(t) { var map = createMap(); map.setStyle({ "version": 8, @@ -497,7 +497,7 @@ test('Map', function(t) { var opts = {}; t.test('normal coords', function(t) { - map.style.queryRenderedFeatures = function (coords, o, classes, zoom, bearing, cb) { + map.style.queryRenderedFeaturesAsync = 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); @@ -507,11 +507,11 @@ test('Map', function(t) { t.end(); }; - map.queryRenderedFeatures(map.project(new LngLat(0, 0)), opts, callback); + map.queryRenderedFeaturesAsync(map.project(new LngLat(0, 0)), opts, callback); }); t.test('wraps coords', function(t) { - map.style.queryRenderedFeatures = function (coords, o, classes, zoom, bearing, cb) { + map.style.queryRenderedFeaturesAsync = function (coords, o, classes, zoom, bearing, cb) { // avoid floating point issues t.equal(parseFloat(coords[0].column.toFixed(4)), 0.5); t.equal(coords[0].row, 0.5); @@ -526,7 +526,7 @@ test('Map', function(t) { t.end(); }; - map.queryRenderedFeatures(map.project(new LngLat(360, 0)), opts, callback); + map.queryRenderedFeaturesAsync(map.project(new LngLat(360, 0)), opts, callback); }); t.end(); From d76b23ff9a2fe1e8ac8cbc62ffc7739526c51d89 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 8 Mar 2016 17:28:59 -0800 Subject: [PATCH 29/48] fix render tests for queryRenderedFeatures --- js/symbol/collision_box.js | 16 +++------------- package.json | 2 +- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/js/symbol/collision_box.js b/js/symbol/collision_box.js index db5846ebd63..66d84139070 100644 --- a/js/symbol/collision_box.js +++ b/js/symbol/collision_box.js @@ -3,7 +3,6 @@ var createStructArrayType = require('../util/struct_array'); var Point = require('point-geometry'); - /** * A collision box represents an area of the map that that is covered by a * label. CollisionFeature uses one or more of these collision boxes to @@ -36,23 +35,14 @@ var Point = require('point-geometry'); * label anymore. Their maxScale is smaller than the current scale. * * - * @class CollisionBox - * @param {Point} anchorPoint The anchor point the box is centered around. - * @param {number} x1 The distance from the anchor to the left edge. - * @param {number} y1 The distance from the anchor to the top edge. - * @param {number} x2 The distance from the anchor to the right edge. - * @param {number} y2 The distance from the anchor to the bottom edge. - * @param {number} maxScale The maximum scale this box can block other boxes at. - * @param {VectorTileFeature} feature The VectorTileFeature that this CollisionBox was created for. - * @param {Array} layerIDs The IDs of the layers that this CollisionBox is a part of. + * @class CollisionBoxArray * @private */ module.exports = createStructArrayType([ // the box is centered around the anchor point - // TODO: this should be an Int16 but it's causing some render tests to fail. - { type: 'Float32', name: 'anchorPointX' }, - { type: 'Float32', name: 'anchorPointY' }, + { type: 'Int16', name: 'anchorPointX' }, + { type: 'Int16', name: 'anchorPointY' }, // distances to the edges from the anchor { type: 'Int16', name: 'x1' }, diff --git a/package.json b/package.json index 9d8127ae84d..9deaf546e7c 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "express": "^4.13.4", "gl": "^2.1.5", "istanbul": "^0.4.2", - "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#c6223ed9aeb49b7fb4a880b1a1921e20a4255911", + "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#13bfab2d5937b241259586cbf545713b251305e5", "nyc": "^6.1.1", "sinon": "^1.15.4", "st": "^1.0.0", From 0fdd86848794f62d4117f3abc0b5de1e56106b93 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 8 Mar 2016 17:34:27 -0800 Subject: [PATCH 30/48] fix queryRenderedFeatures documentation --- js/ui/map.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/js/ui/map.js b/js/ui/map.js index 0c5f2a677fd..596c8f3f077 100644 --- a/js/ui/map.js +++ b/js/ui/map.js @@ -390,12 +390,12 @@ util.extend(Map.prototype, /** @lends Map.prototype */{ * @returns {Map} `this` * * @example - * map.queryRenderedFeatures([20, 35], { layer: 'my-layer-name' }, function(err, features) { + * map.queryRenderedFeaturesAsync([20, 35], { layer: 'my-layer-name' }, function(err, features) { * console.log(features); * }); * * @example - * map.queryRenderedFeatures([[10, 20], [30, 50]], { layer: 'my-layer-name' }, function(err, features) { + * map.queryRenderedFeaturesAsync([[10, 20], [30, 50]], { layer: 'my-layer-name' }, function(err, features) { * console.log(features); * }); */ @@ -410,6 +410,22 @@ util.extend(Map.prototype, /** @lends Map.prototype */{ return this; }, + /** + * Query rendered features within a point or rectangle. + * + * @param {Point|Array|Array|Array>} [pointOrBox] Either [x, y] pixel coordinates of a point, or [[x1, y1], [x2, y2]] pixel coordinates of opposite corners of bounding rectangle. Optional: use entire viewport if omitted. + * @param {Object} params + * @param {string|Array} [params.layer] Only return features from a given layer or layers + * @param {Array} [params.filter] A mapbox-gl-style-spec filter. + * + * @returns {Array} features - An array of [GeoJSON](http://geojson.org/) features matching the query parameters. The GeoJSON properties of each feature are taken from the original source. Each feature object also contains a top-level `layer` property whose value is an object representing the style layer to which the feature belongs. Layout and paint properties in this object contain values which are fully evaluated for the given zoom level and feature. + * + * @example + * var features = map.queryRenderedFeatures([20, 35], { layer: 'my-layer-name' }); + * + * @example + * var features = map.queryRenderedFeaturesAsync([[10, 20], [30, 50]], { layer: 'my-layer-name' }); + */ queryRenderedFeatures: function(pointOrBox, params) { if (!(pointOrBox instanceof Point || Array.isArray(pointOrBox))) { params = pointOrBox; From f8aeecfccf0c06ae716207febef13f01c8ab108e Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 9 Mar 2016 15:31:41 -0800 Subject: [PATCH 31/48] combine duplicate parts of Buffer and StructArray --- js/data/bucket.js | 83 ++++---- js/data/bucket/circle_bucket.js | 2 +- js/data/bucket/fill_bucket.js | 2 +- js/data/bucket/line_bucket.js | 4 +- js/data/bucket/symbol_bucket.js | 20 +- js/data/buffer.js | 256 +++-------------------- js/data/feature_tree.js | 22 +- js/source/worker.js | 6 +- js/source/worker_tile.js | 24 ++- js/symbol/collision_box.js | 10 +- js/symbol/collision_tile.js | 14 +- js/util/struct_array.js | 301 ++++++++++++++++++++++------ test/js/data/bucket.test.js | 59 +++--- test/js/data/buffer.test.js | 105 +++------- test/js/data/fill_bucket.test.js | 2 +- test/js/data/line_bucket.test.js | 2 +- test/js/data/symbol_bucket.test.js | 2 +- test/js/symbol/collision_feature.js | 4 +- test/js/util/struct_array.test.js | 98 +++++++++ 19 files changed, 540 insertions(+), 476 deletions(-) create mode 100644 test/js/util/struct_array.test.js diff --git a/js/data/bucket.js b/js/data/bucket.js index 8c49842c64e..01eddeb7232 100644 --- a/js/data/bucket.js +++ b/js/data/bucket.js @@ -4,6 +4,7 @@ var featureFilter = require('feature-filter'); var Buffer = require('./buffer'); var StyleLayer = require('../style/style_layer'); var util = require('../util/util'); +var StructArrayType = require('../util/struct_array'); module.exports = Bucket; @@ -23,8 +24,6 @@ Bucket.create = function(options) { return new Classes[options.layer.type](options); }; -Bucket.AttributeType = Buffer.AttributeType; - /** * The maximum extent of a feature that can be safely stored in the buffer. @@ -78,8 +77,10 @@ function Bucket(options) { if (options.elementGroups) { this.elementGroups = options.elementGroups; - this.buffers = util.mapObject(options.buffers, function(options) { - return new Buffer(options); + this.buffers = util.mapObject(options.structArrays, function(structArray, bufferName) { + var structArrayType = options.structArrayTypes[bufferName]; + var type = (structArrayType.members[0].name === 'vertices' ? Buffer.BufferType.ELEMENT : Buffer.BufferType.VERTEX); + return new Buffer(structArray, structArrayType, type); }); } } @@ -90,13 +91,13 @@ function Bucket(options) { */ Bucket.prototype.populateBuffers = function() { this.createStyleLayer(); - this.createBuffers(); + this.createStructArrays(); for (var i = 0; i < this.features.length; i++) { this.addFeature(this.features[i]); } - this.trimBuffers(); + this.trimArrays(); }; /** @@ -112,14 +113,14 @@ Bucket.prototype.makeRoomFor = function(shaderName, numVertices) { var currentGroup = groups.length && groups[groups.length - 1]; if (!currentGroup || currentGroup.vertexLength + numVertices > 65535) { - var vertexBuffer = this.buffers[this.getBufferName(shaderName, 'vertex')]; - var elementBuffer = this.buffers[this.getBufferName(shaderName, 'element')]; - var secondElementBuffer = this.buffers[this.getBufferName(shaderName, 'secondElement')]; + var vertexArray = this.structArrays[this.getBufferName(shaderName, 'vertex')]; + var elementArray = this.structArrays[this.getBufferName(shaderName, 'element')]; + var secondElementArray = this.structArrays[this.getBufferName(shaderName, 'secondElement')]; currentGroup = new ElementGroup( - vertexBuffer.length, - elementBuffer && elementBuffer.length, - secondElementBuffer && secondElementBuffer.length + vertexArray.length, + elementArray && elementArray.length, + secondElementArray && secondElementArray.length ); groups.push(currentGroup); } @@ -132,9 +133,10 @@ Bucket.prototype.makeRoomFor = function(shaderName, numVertices) { * as necessary. * @private */ -Bucket.prototype.createBuffers = function() { +Bucket.prototype.createStructArrays = function() { var elementGroups = this.elementGroups = {}; - var buffers = this.buffers = {}; + var structArrays = this.structArrays = {}; + var structArrayTypes = this.structArrayTypes = {}; for (var shaderName in this.shaderInterfaces) { var shaderInterface = this.shaderInterfaces[shaderName]; @@ -143,11 +145,14 @@ Bucket.prototype.createBuffers = function() { var vertexBufferName = this.getBufferName(shaderName, 'vertex'); var vertexAddMethodName = this.getAddMethodName(shaderName, 'vertex'); - buffers[vertexBufferName] = new Buffer({ - type: Buffer.BufferType.VERTEX, - attributes: shaderInterface.attributes + var VertexArrayType = new StructArrayType({ + members: shaderInterface.attributes, + alignment: Buffer.VERTEX_ATTRIBUTE_ALIGNMENT }); + structArrays[vertexBufferName] = new VertexArrayType(); + structArrayTypes[vertexBufferName] = VertexArrayType.serialize(); + this[vertexAddMethodName] = this[vertexAddMethodName] || createVertexAddMethod( shaderName, shaderInterface, @@ -155,16 +160,21 @@ Bucket.prototype.createBuffers = function() { ); } + if (shaderInterface.elementBuffer) { var elementBufferName = this.getBufferName(shaderName, 'element'); - buffers[elementBufferName] = createElementBuffer(shaderInterface.elementBufferComponents); - this[this.getAddMethodName(shaderName, 'element')] = createElementAddMethod(this.buffers[elementBufferName]); + var ElementArrayType = createElementBufferType(shaderInterface.elementBufferComponents); + structArrays[elementBufferName] = new ElementArrayType(); + structArrayTypes[elementBufferName] = ElementArrayType.serialize(); + this[this.getAddMethodName(shaderName, 'element')] = createElementAddMethod(this.structArrays[elementBufferName]); } if (shaderInterface.secondElementBuffer) { var secondElementBufferName = this.getBufferName(shaderName, 'secondElement'); - buffers[secondElementBufferName] = createElementBuffer(shaderInterface.secondElementBufferComponents); - this[this.getAddMethodName(shaderName, 'secondElement')] = createElementAddMethod(this.buffers[secondElementBufferName]); + var SecondElementArrayType = createElementBufferType(shaderInterface.secondElementBufferComponents); + structArrays[secondElementBufferName] = new SecondElementArrayType(); + structArrayTypes[secondElementBufferName] = SecondElementArrayType.serialize(); + this[this.getAddMethodName(shaderName, 'secondElement')] = createElementAddMethod(this.structArrays[secondElementBufferName]); } elementGroups[shaderName] = []; @@ -177,9 +187,9 @@ Bucket.prototype.destroy = function(gl) { } }; -Bucket.prototype.trimBuffers = function() { - for (var bufferName in this.buffers) { - this.buffers[bufferName].trim(); +Bucket.prototype.trimArrays = function() { + for (var bufferName in this.structArrays) { + this.structArrays[bufferName].trim(); } }; @@ -211,9 +221,10 @@ Bucket.prototype.serialize = function() { }, zoom: this.zoom, elementGroups: this.elementGroups, - buffers: util.mapObject(this.buffers, function(buffer) { - return buffer.serialize(); - }) + structArrays: util.mapObject(this.structArrays, function(structArray) { + return structArray.serialize(); + }), + structArrayTypes: this.structArrayTypes }; }; @@ -238,7 +249,7 @@ function createVertexAddMethod(shaderName, shaderInterface, bufferName) { pushArgs = pushArgs.concat(shaderInterface.attributes[i].value); } - var body = 'return this.buffers.' + bufferName + '.push(' + pushArgs.join(', ') + ');'; + var body = 'return this.structArrays.' + bufferName + '.emplaceBack(' + pushArgs.join(', ') + ');'; if (!createVertexAddMethodCache[body]) { createVertexAddMethodCache[body] = new Function(shaderInterface.attributeArgs, body); @@ -249,19 +260,17 @@ function createVertexAddMethod(shaderName, shaderInterface, bufferName) { function createElementAddMethod(buffer) { return function(one, two, three) { - return buffer.push(one, two, three); + return buffer.emplaceBack(one, two, three); }; } -function createElementBuffer(components) { - return new Buffer({ - type: Buffer.BufferType.ELEMENT, - attributes: [{ +function createElementBufferType(components) { + return new StructArrayType({ + members: [{ + type: Buffer.ELEMENT_ATTRIBUTE_TYPE, name: 'vertices', - components: components || 3, - type: Buffer.ELEMENT_ATTRIBUTE_TYPE - }] - }); + components: components || 3 + }]}); } function capitalize(string) { diff --git a/js/data/bucket/circle_bucket.js b/js/data/bucket/circle_bucket.js index 666c862ef1b..89efdd04162 100644 --- a/js/data/bucket/circle_bucket.js +++ b/js/data/bucket/circle_bucket.js @@ -30,7 +30,7 @@ CircleBucket.prototype.shaderInterfaces = { attributes: [{ name: 'pos', components: 2, - type: Bucket.AttributeType.SHORT, + type: 'Int16', value: [ '(x * 2) + ((extrudeX + 1) / 2)', '(y * 2) + ((extrudeY + 1) / 2)' diff --git a/js/data/bucket/fill_bucket.js b/js/data/bucket/fill_bucket.js index c74dbe29f28..fe93e7aaf99 100644 --- a/js/data/bucket/fill_bucket.js +++ b/js/data/bucket/fill_bucket.js @@ -24,7 +24,7 @@ FillBucket.prototype.shaderInterfaces = { attributes: [{ name: 'pos', components: 2, - type: Bucket.AttributeType.SHORT, + type: 'Int16', value: ['x', 'y'] }] } diff --git a/js/data/bucket/line_bucket.js b/js/data/bucket/line_bucket.js index 7bea75a4971..5d84231301d 100644 --- a/js/data/bucket/line_bucket.js +++ b/js/data/bucket/line_bucket.js @@ -60,7 +60,7 @@ LineBucket.prototype.shaderInterfaces = { attributes: [{ name: 'pos', components: 2, - type: Bucket.AttributeType.SHORT, + type: 'Int16', value: [ '(point.x << 1) | tx', '(point.y << 1) | ty' @@ -68,7 +68,7 @@ LineBucket.prototype.shaderInterfaces = { }, { name: 'data', components: 4, - type: Bucket.AttributeType.UNSIGNED_BYTE, + type: 'Uint8', value: [ // add 128 to store an byte in an unsigned byte 'Math.round(' + EXTRUDE_SCALE + ' * extrude.x) + 128', diff --git a/js/data/bucket/symbol_bucket.js b/js/data/bucket/symbol_bucket.js index 7294b549711..b4f9fcbdd96 100644 --- a/js/data/bucket/symbol_bucket.js +++ b/js/data/bucket/symbol_bucket.js @@ -38,12 +38,12 @@ var shaderAttributeArgs = ['x', 'y', 'ox', 'oy', 'tx', 'ty', 'minzoom', 'maxzoom var shaderAttributes = [{ name: 'pos', components: 2, - type: Bucket.AttributeType.SHORT, + type: 'Int16', value: ['x', 'y'] }, { name: 'offset', components: 2, - type: Bucket.AttributeType.SHORT, + type: 'Int16', value: [ 'Math.round(ox * 64)', // use 1/64 pixels for placement 'Math.round(oy * 64)' @@ -51,7 +51,7 @@ var shaderAttributes = [{ }, { name: 'data1', components: 4, - type: Bucket.AttributeType.UNSIGNED_BYTE, + type: 'Uint8', value: [ 'tx / 4', // tex 'ty / 4', // tex @@ -61,7 +61,7 @@ var shaderAttributes = [{ }, { name: 'data2', components: 2, - type: Bucket.AttributeType.UNSIGNED_BYTE, + type: 'Uint8', value: [ '(minzoom || 0) * 10', // minzoom 'Math.min(maxzoom || 25, 25) * 10' // minzoom @@ -92,12 +92,12 @@ SymbolBucket.prototype.shaderInterfaces = { attributes: [{ name: 'pos', components: 2, - type: Bucket.AttributeType.SHORT, + type: 'Int16', value: [ 'point.x', 'point.y' ] }, { name: 'extrude', components: 2, - type: Bucket.AttributeType.SHORT, + type: 'Int16', value: [ 'Math.round(extrude.x)', 'Math.round(extrude.y)' @@ -105,7 +105,7 @@ SymbolBucket.prototype.shaderInterfaces = { }, { name: 'data', components: 2, - type: Bucket.AttributeType.UNSIGNED_BYTE, + type: 'Uint8', value: [ 'maxZoom * 10', 'placementZoom * 10' @@ -230,7 +230,7 @@ SymbolBucket.prototype.populateBuffers = function(collisionTile, stacks, icons) this.placeFeatures(collisionTile, this.showCollisionBoxes); - this.trimBuffers(); + this.trimArrays(); }; SymbolBucket.prototype.addFeature = function(lines, shapedText, shapedIcon, featureIndex) { @@ -335,7 +335,7 @@ SymbolBucket.prototype.placeFeatures = function(collisionTile, showCollisionBoxe // Calculate which labels can be shown and when they can be shown and // create the bufers used for rendering. - this.createBuffers(); + this.createStructArrays(); var elementGroups = this.elementGroups = { glyph: [], @@ -512,7 +512,7 @@ SymbolBucket.prototype.addToDebugBuffers = function(collisionTile) { if (!feature) continue; for (var b = feature.boxStartIndex; b < feature.boxEndIndex; b++) { - var box = this.collisionBoxArray.at(b); + var box = this.collisionBoxArray.get(b); var anchorPoint = box.anchorPoint; var tl = new Point(box.x1, box.y1 * yStretch)._rotate(angle); diff --git a/js/data/buffer.js b/js/data/buffer.js index 4c28963691c..1f9f8b87367 100644 --- a/js/data/buffer.js +++ b/js/data/buffer.js @@ -1,90 +1,25 @@ 'use strict'; -// Note: all "sizes" are measured in bytes - -var assert = require('assert'); +module.exports = Buffer; /** - * The `Buffer` class is responsible for managing one instance of `ArrayBuffer`. `ArrayBuffer`s - * provide low-level read/write access to a chunk of memory. `ArrayBuffer`s are populated with - * per-vertex data, uploaded to the GPU, and used in rendering. - * - * `Buffer` provides an abstraction over `ArrayBuffer`, making it behave like an array of - * statically typed structs. A buffer is comprised of items. An item is comprised of a set of - * attributes. Attributes are defined when the class is constructed. - * - * Though the buffers are intended for WebGL, this class should have no formal code dependencies - * on WebGL. Though the buffers are populated by vector tile features, this class should have - * no domain knowledge about vector tiles, coordinate systems, etc. + * The `Buffer` class turns a `StructArray` into a WebGL buffer. Each member of the StructArray's + * Struct type is converted to a WebGL atribute. * * @class Buffer * @private - * @param options - * @param {BufferType} options.type - * @param {Array.} options.attributes + * @param {object} structArray A serialized StructArray. + * @param {object} structArrayType A serialized StructArrayType. + * @param {BufferType} type */ -function Buffer(options) { - - this.type = options.type; - - // Clone an existing Buffer - if (options.arrayBuffer) { - - this.capacity = options.capacity; - this.arrayBuffer = options.arrayBuffer; - this.attributes = options.attributes; - this.itemSize = options.itemSize; - this.length = options.length; - - // Create a new Buffer - } else { - - this.capacity = align(Buffer.CAPACITY_DEFAULT, Buffer.CAPACITY_ALIGNMENT); - this.arrayBuffer = new ArrayBuffer(this.capacity); - this.attributes = []; - this.itemSize = 0; - this.length = 0; - - // Vertex buffer attributes must be aligned to word boundaries but - // element buffer attributes do not need to be aligned. - var attributeAlignment = this.type === Buffer.BufferType.VERTEX ? Buffer.VERTEX_ATTRIBUTE_ALIGNMENT : 1; - - this.attributes = options.attributes.map(function(attributeOptions) { - var attribute = {}; - - attribute.name = attributeOptions.name; - attribute.components = attributeOptions.components || 1; - attribute.type = attributeOptions.type || Buffer.AttributeType.UNSIGNED_BYTE; - attribute.size = attribute.type.size * attribute.components; - attribute.offset = this.itemSize; - - this.itemSize = align(attribute.offset + attribute.size, attributeAlignment); - - assert(!isNaN(this.itemSize)); - assert(!isNaN(attribute.size)); - assert(attribute.type.name in Buffer.AttributeType); - - return attribute; - }, this); - - // These are expensive calls. Because we only push things to buffers in - // the worker thread, we can skip in the "clone an existing buffer" case. - this._createPushMethod(); - this._refreshViews(); - } +function Buffer(structArray, structArrayType, type) { + this.arrayBuffer = structArray.arrayBuffer; + this.length = structArray.length; + this.attributes = structArrayType.members; + this.itemSize = structArrayType.BYTES_PER_ELEMENT; + this.type = type; } -Buffer.prototype.serialize = function() { - return { - type: this.type, - capacity: this.capacity, - arrayBuffer: this.arrayBuffer, - attributes: this.attributes, - itemSize: this.itemSize, - length: this.length - }; -}; - /** * Bind this buffer to a WebGL context. * @private @@ -116,6 +51,18 @@ Buffer.prototype.destroy = function(gl) { } }; +/** + * @enum {string} BufferAttributeType + * @private + * @readonly + */ +var AttributeType = { + Int8: 'BYTE', + Uint8: 'UNSIGNED_BYTE', + Int16: 'SHORT', + Uint16: 'UNSIGNED_SHORT' +}; + /** * Set the attribute pointers in a WebGL context according to the buffer's attribute layout * @private @@ -128,120 +75,11 @@ Buffer.prototype.setAttribPointers = function(gl, shader, offset) { var attrib = this.attributes[i]; gl.vertexAttribPointer( - shader['a_' + attrib.name], attrib.components, gl[attrib.type.name], + shader['a_' + attrib.name], attrib.components, gl[AttributeType[attrib.type]], false, this.itemSize, offset + attrib.offset); } }; -/** - * Resize the buffer to discard unused capacity. - * @private - */ -Buffer.prototype.trim = function() { - this.capacity = align(this.itemSize * this.length, Buffer.CAPACITY_ALIGNMENT); - this.arrayBuffer = this.arrayBuffer.slice(0, this.capacity); - this._refreshViews(); -}; - -/** - * Get an item from the `ArrayBuffer`. Only used for debugging. - * @private - * @param {number} index The index of the item to get - * @returns {Object.>} - */ -Buffer.prototype.get = function(index) { - this._refreshViews(); - - var item = {}; - var offset = index * this.itemSize; - - for (var i = 0; i < this.attributes.length; i++) { - var attribute = this.attributes[i]; - var values = item[attribute.name] = []; - - for (var j = 0; j < attribute.components; j++) { - var componentOffset = ((offset + attribute.offset) / attribute.type.size) + j; - values.push(this.views[attribute.type.name][componentOffset]); - } - } - return item; -}; - -/** - * Check that a buffer item is well formed and throw an error if not. Only - * used for debugging. - * @private - * @param {number} args The "arguments" object from Buffer::push - */ -Buffer.prototype.validate = function(args) { - var argIndex = 0; - for (var i = 0; i < this.attributes.length; i++) { - for (var j = 0; j < this.attributes[i].components; j++) { - assert(!isNaN(args[argIndex++])); - } - } - assert(argIndex === args.length); -}; - -Buffer.prototype._resize = function(capacity) { - var oldUByteView = this.views.UNSIGNED_BYTE; - this.capacity = align(capacity, Buffer.CAPACITY_ALIGNMENT); - this.arrayBuffer = new ArrayBuffer(this.capacity); - this._refreshViews(); - this.views.UNSIGNED_BYTE.set(oldUByteView); -}; - -Buffer.prototype._refreshViews = function() { - this.views = { - UNSIGNED_BYTE: new Uint8Array(this.arrayBuffer), - BYTE: new Int8Array(this.arrayBuffer), - UNSIGNED_SHORT: new Uint16Array(this.arrayBuffer), - SHORT: new Int16Array(this.arrayBuffer) - }; -}; - -var createPushMethodCache = {}; -Buffer.prototype._createPushMethod = function() { - var body = ''; - var argNames = []; - - body += 'var i = this.length++;\n'; - body += 'var o = i * ' + this.itemSize + ';\n'; - body += 'if (o + ' + this.itemSize + ' > this.capacity) { this._resize(this.capacity * ' + Buffer.CAPACITY_RESIZE_MULTIPLIER + '); }\n'; - - for (var i = 0; i < this.attributes.length; i++) { - var attribute = this.attributes[i]; - var offsetId = 'o' + i; - - body += '\nvar ' + offsetId + ' = (o + ' + attribute.offset + ') / ' + attribute.type.size + ';\n'; - - for (var j = 0; j < attribute.components; j++) { - var rvalue = 'v' + argNames.length; - var lvalue = 'this.views.' + attribute.type.name + '[' + offsetId + ' + ' + j + ']'; - body += lvalue + ' = ' + rvalue + ';\n'; - argNames.push(rvalue); - } - } - - body += '\nreturn i;\n'; - - if (!createPushMethodCache[body]) { - createPushMethodCache[body] = new Function(argNames, body); - } - - this.push = createPushMethodCache[body]; -}; - -/** - * @typedef {Object} BufferAttribute - * @private - * @property {string} name - * @property {number} components - * @property {BufferAttributeType} type - * @property {number} size - * @property {number} offset - */ - /** * @enum {string} BufferType * @private @@ -252,18 +90,6 @@ Buffer.BufferType = { ELEMENT: 'ELEMENT_ARRAY_BUFFER' }; -/** - * @enum {{size: number, name: string}} BufferAttributeType - * @private - * @readonly - */ -Buffer.AttributeType = { - BYTE: { size: 1, name: 'BYTE' }, - UNSIGNED_BYTE: { size: 1, name: 'UNSIGNED_BYTE' }, - SHORT: { size: 2, name: 'SHORT' }, - UNSIGNED_SHORT: { size: 2, name: 'UNSIGNED_SHORT' } -}; - /** * An `BufferType.ELEMENT` buffer holds indicies of a corresponding `BufferType.VERTEX` buffer. * These indicies are stored in the `BufferType.ELEMENT` buffer as `UNSIGNED_SHORT`s. @@ -271,26 +97,7 @@ Buffer.AttributeType = { * @private * @readonly */ -Buffer.ELEMENT_ATTRIBUTE_TYPE = Buffer.AttributeType.UNSIGNED_SHORT; - -/** - * @private - * @readonly - */ -Buffer.CAPACITY_DEFAULT = 1024; - -/** - * @private - * @readonly - */ -Buffer.CAPACITY_RESIZE_MULTIPLIER = 5; - -/** - * WebGL performs best if buffer sizes are aligned to 2 byte boundaries. - * @private - * @readonly - */ -Buffer.CAPACITY_ALIGNMENT = 2; +Buffer.ELEMENT_ATTRIBUTE_TYPE = 'Uint16'; /** * WebGL performs best if vertex attribute offsets are aligned to 4 byte boundaries. @@ -298,14 +105,3 @@ Buffer.CAPACITY_ALIGNMENT = 2; * @readonly */ Buffer.VERTEX_ATTRIBUTE_ALIGNMENT = 4; - -function align(value, alignment) { - alignment = alignment || 1; - var remainder = value % alignment; - if (alignment !== 1 && remainder !== 0) { - value += (alignment - remainder); - } - return value; -} - -module.exports = Buffer; diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index dd74c53e9f3..75d286f3700 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -4,23 +4,25 @@ var Point = require('point-geometry'); var loadGeometry = require('./load_geometry'); var EXTENT = require('./bucket').EXTENT; var featureFilter = require('feature-filter'); -var createStructArrayType = require('../util/struct_array'); +var StructArrayType = require('../util/struct_array'); var Grid = require('../util/grid'); var StringNumberMapping = require('../util/string_number_mapping'); var vt = require('vector-tile'); var Protobuf = require('pbf'); var GeoJSONFeature = require('../util/vectortile_to_geojson'); -var FeatureIndexArray = createStructArrayType([ +var FeatureIndexArray = new StructArrayType({ + members: [ // the index of the feature in the original vectortile { type: 'Uint32', name: 'featureIndex' }, // the source layer the feature appears in { type: 'Uint16', name: 'sourceLayerIndex' }, // the bucket the feature appears in { type: 'Uint16', name: 'bucketIndex' } -]); + ]}); -var FilteredFeatureIndexArray = createStructArrayType([ +var FilteredFeatureIndexArray = new StructArrayType({ + members: [ // the index of the feature in the original vectortile { type: 'Uint32', name: 'featureIndex' }, // the source layer the feature appears in @@ -29,7 +31,7 @@ var FilteredFeatureIndexArray = createStructArrayType([ { type: 'Uint16', name: 'bucketIndex' }, // the layer the feature appears in { type: 'Uint16', name: 'layerIndex' } -]); + ]}); module.exports = FeatureTree; @@ -87,12 +89,12 @@ FeatureTree.prototype.serialize = function() { coord: this.coord, overscaling: this.overscaling, grid: this.grid.toArrayBuffer(), - featureIndexArray: this.featureIndexArray.arrayBuffer, + featureIndexArray: this.featureIndexArray.serialize(), numberToLayerIDs: this.numberToLayerIDs }; return { data: data, - transferables: [data.grid, data.featureIndexArray] + transferables: [data.grid, data.featureIndexArray.arrayBuffer] }; }; @@ -152,11 +154,11 @@ FeatureTree.prototype.query = function(args, styleLayersByID, returnGeoJSON) { var matching = this.grid.query(minX - additionalRadius, minY - additionalRadius, maxX + additionalRadius, maxY + additionalRadius); matching.sort(topDown); - var match = this.featureIndexArray.at(0); + var match = this.featureIndexArray.get(0); this.filterMatching(result, matching, match, queryGeometry, filter, params.layerIDs, styleLayersByID, args.bearing, pixelsToTileUnits, returnGeoJSON); var matchingSymbols = this.collisionTile.queryRenderedSymbols(minX, minY, maxX, maxY, args.scale); - var match2 = this.collisionTile.collisionBoxArray.at(0); + var match2 = this.collisionTile.collisionBoxArray.get(0); matchingSymbols.sort(); this.filterMatching(result, matchingSymbols, match2, queryGeometry, filter, params.layerIDs, styleLayersByID, args.bearing, pixelsToTileUnits, returnGeoJSON); @@ -266,7 +268,7 @@ FeatureTree.prototype.makeGeoJSON = function(featureIndexArray, styleLayers) { var result = {}; featureIndexArray = new FilteredFeatureIndexArray(featureIndexArray); - var indexes = featureIndexArray.at(0); + var indexes = featureIndexArray.get(0); var cachedLayerFeatures = {}; for (var i = 0; i < featureIndexArray.length; i++) { diff --git a/js/source/worker.js b/js/source/worker.js index 3b6710eaa7c..50e6966ae24 100644 --- a/js/source/worker.js +++ b/js/source/worker.js @@ -187,7 +187,7 @@ util.extend(Worker.prototype, { if (geoJSONTile) { var geojsonWrapper = new GeoJSONWrapper(geoJSONTile.features); geojsonWrapper.name = '_geojsonTileLayer'; - var rawTileData = vtpbf({ layers: { '_geojsonTileLayer': geojsonWrapper }}); + var rawTileData = vtpbf({ layers: { '_geojsonTileLayer': geojsonWrapper }}).buffer; tile.parse(geojsonWrapper, this.layers, this.actor, callback, rawTileData); } else { return callback(null, null); // nothing in the given tile @@ -218,8 +218,8 @@ util.extend(Worker.prototype, { var collisionTile = new CollisionTile(params.collisionTile, tile.collisionBoxArray); var featureTree = new FeatureTree(params.featureTree, params.rawTileData, collisionTile); - var featureArrayBuffer = featureTree.query(params, this.styleLayersByID, false).arrayBuffer; - callback(null, featureArrayBuffer, [featureArrayBuffer]); + var featureArray = featureTree.query(params, this.styleLayersByID, false).serialize(); + callback(null, featureArray, [featureArray.arrayBuffer]); } else { callback(null, []); } diff --git a/js/source/worker_tile.js b/js/source/worker_tile.js index 2dcb1fd7ab5..2f4a5d91f34 100644 --- a/js/source/worker_tile.js +++ b/js/source/worker_tile.js @@ -204,17 +204,19 @@ WorkerTile.prototype.parse = function(data, layers, actor, callback, rawTileData var featureTree_ = featureTree.serialize(); var collisionTile_ = collisionTile.serialize(); - var collisionBoxArray = tile.collisionBoxArray.arrayBuffer.slice(0); - var transferables = [rawTileData, collisionBoxArray].concat(featureTree.transferables).concat(collisionTile_.transferables); + var collisionBoxArray = tile.collisionBoxArray.serialize(); + var transferables = [rawTileData].concat(featureTree_.transferables).concat(collisionTile_.transferables); + + var nonEmptyBuckets = buckets.filter(isBucketEmpty); callback(null, { - buckets: buckets.filter(isBucketEmpty).map(serializeBucket), + buckets: nonEmptyBuckets.map(serializeBucket), bucketStats: stats, // TODO put this in a separate message? featureTree: featureTree_.data, collisionTile: collisionTile_.data, collisionBoxArray: collisionBoxArray, rawTileData: rawTileData - }, getTransferables(buckets).concat(transferables)); + }, getTransferables(nonEmptyBuckets).concat(transferables)); } }; @@ -235,18 +237,20 @@ WorkerTile.prototype.redoPlacement = function(angle, pitch, showCollisionBoxes) var collisionTile_ = collisionTile.serialize(); + var nonEmptyBuckets = buckets.filter(isBucketEmpty); + return { result: { - buckets: buckets.filter(isBucketEmpty).map(serializeBucket), + buckets: nonEmptyBuckets.map(serializeBucket), collisionTile: collisionTile_.data }, - transferables: getTransferables(buckets).concat(collisionTile_.transferables) + transferables: getTransferables(nonEmptyBuckets).concat(collisionTile_.transferables) }; }; function isBucketEmpty(bucket) { - for (var bufferName in bucket.buffers) { - if (bucket.buffers[bufferName].length > 0) return true; + for (var bufferName in bucket.structArrays) { + if (bucket.structArrays[bufferName].length > 0) return true; } return false; } @@ -259,8 +263,8 @@ function getTransferables(buckets) { var transferables = []; for (var i in buckets) { var bucket = buckets[i]; - for (var j in bucket.buffers) { - transferables.push(bucket.buffers[j].arrayBuffer); + for (var j in bucket.structArrays) { + transferables.push(bucket.structArrays[j].arrayBuffer); } } return transferables; diff --git a/js/symbol/collision_box.js b/js/symbol/collision_box.js index 66d84139070..bca45d4b0b0 100644 --- a/js/symbol/collision_box.js +++ b/js/symbol/collision_box.js @@ -1,6 +1,7 @@ 'use strict'; -var createStructArrayType = require('../util/struct_array'); +var StructArrayType = require('../util/struct_array'); +var util = require('../util/util'); var Point = require('point-geometry'); /** @@ -39,7 +40,8 @@ var Point = require('point-geometry'); * @private */ -module.exports = createStructArrayType([ +var CollisionBoxArray = module.exports = new StructArrayType({ + members: [ // the box is centered around the anchor point { type: 'Int16', name: 'anchorPointX' }, { type: 'Int16', name: 'anchorPointY' }, @@ -68,7 +70,9 @@ module.exports = createStructArrayType([ { type: 'Int16', name: 'bbox3' }, { type: 'Float32', name: 'placementScale' } -], { + ]}); + +util.extendAll(CollisionBoxArray.prototype.StructType.prototype, { get anchorPoint() { return new Point(this.anchorPointX, this.anchorPointY); } diff --git a/js/symbol/collision_tile.js b/js/symbol/collision_tile.js index da5d7737990..9cf77f4c343 100644 --- a/js/symbol/collision_tile.js +++ b/js/symbol/collision_tile.js @@ -70,16 +70,16 @@ function CollisionTile(angle, pitch, collisionBoxArray) { 0); } - this.tempCollisionBox = collisionBoxArray.at(0); + this.tempCollisionBox = collisionBoxArray.get(0); this.edges = [ - collisionBoxArray.at(1), - collisionBoxArray.at(2), - collisionBoxArray.at(3), - collisionBoxArray.at(4) + collisionBoxArray.get(1), + collisionBoxArray.get(2), + collisionBoxArray.get(3), + collisionBoxArray.get(4) ]; - this.box = collisionBoxArray.at(0); - this.blocking = collisionBoxArray.at(0); + this.box = collisionBoxArray.get(0); + this.blocking = collisionBoxArray.get(0); } CollisionTile.prototype.serialize = function() { diff --git a/js/util/struct_array.js b/js/util/struct_array.js index eb8f734fa32..bbcbdb84f92 100644 --- a/js/util/struct_array.js +++ b/js/util/struct_array.js @@ -1,8 +1,10 @@ 'use strict'; -var inherit = require('./util').inherit; +// Note: all "sizes" are measured in bytes -module.exports = createStructArrayType; +var assert = require('assert'); + +module.exports = StructArrayType; var viewTypes = { 'Int8': Int8Array, @@ -16,51 +18,136 @@ var viewTypes = { 'Float64': Float64Array }; -function createStructArrayType(members, methods) { - if (methods === undefined) methods = {}; +/** + * @typedef StructMember + * @private + * @property {string} name + * @property {string} type + * @property {number} components + */ + +var structArrayTypeCache = {}; + +/** + * `StructArrayType` is used to create new `StructArray` types. + * + * `StructArray` provides an abstraction over `ArrayBuffer` and `TypedArray` making it behave like + * an array of typed structs. A StructArray is comprised of elements. Each element has a set of + * members that are defined when the `StructArrayType` is created. + * + * StructArrays useful for creating large arrays that: + * - can be transferred from workers as a Transferable object + * - can be copied cheaply + * - use less memory for lower-precision members + * - can be used as buffers in WebGL. + * + * @class StructArrayType + * @param {Array.} + * @param options + * @param {number} options.alignment Use `4` to align members to 4 byte boundaries. Default is 1. + * + * @example + * + * var PointArrayType = new StructArrayType({ + * members: [ + * { type: 'Int16', name: 'x' }, + * { type: 'Int16', name: 'y' } + * ]}); + * + * var pointArray = new PointArrayType(); + * pointArray.emplaceBack(10, 15); + * pointArray.emplaceBack(20, 35); + * + * point = pointArray.get(0); + * assert(point.x === 10); + * assert(point.y === 15); + * point._setIndex(1); + * assert(point.x === 20); + * assert(point.y === 35); + * + * @private + */ +function StructArrayType(options) { + + var key = JSON.stringify(options); + if (structArrayTypeCache[key]) { + return structArrayTypeCache[key]; + } + + if (options.alignment === undefined) options.alignment = 1; function StructType() { Struct.apply(this, arguments); } - StructType.prototype = inherit(Struct, methods); + StructType.prototype = Object.create(Struct.prototype); var offset = 0; var maxSize = 0; + var usedTypes = ['Uint8']; - for (var m = 0; m < members.length; m++) { - var member = members[m]; + StructType.prototype.members = options.members.map(function(member) { + member = { + name: member.name, + type: member.type, + components: member.components || 1 + }; - if (!viewTypes[member.type]) { - throw new Error(JSON.stringify(member.type) + ' is not a valid type'); - } + assert(member.name.length); + assert(member.type in viewTypes); - var size = sizeOf(member.type); - maxSize = Math.max(maxSize, size); - offset = member.offset = align(offset, size); + if (usedTypes.indexOf(member.type) < 0) usedTypes.push(member.type); - Object.defineProperty(StructType.prototype, member.name, { - get: createGetter(member.type, offset), - set: createSetter(member.type, offset) - }); + var typeSize = sizeOf(member.type); + maxSize = Math.max(maxSize, typeSize); + member.offset = offset = align(offset, Math.max(options.alignment, typeSize)); - offset += size; - } + for (var c = 0; c < member.components; c++) { + Object.defineProperty(StructType.prototype, member.name + (member.components === 1 ? '' : c), { + get: createGetter(member, c), + set: createSetter(member, c) + }); + } + + offset += typeSize * member.components; - StructType.prototype.BYTE_SIZE = align(offset, maxSize); + return member; + }); + + StructType.prototype.alignment = options.alignment; + StructType.prototype.BYTE_SIZE = align(offset, Math.max(maxSize, options.alignment)); function StructArrayType() { StructArray.apply(this, arguments); } + StructArrayType.serialize = serializeStructArrayType; + StructArrayType.prototype = Object.create(StructArray.prototype); StructArrayType.prototype.StructType = StructType; StructArrayType.prototype.BYTES_PER_ELEMENT = StructType.prototype.BYTE_SIZE; - StructArrayType.prototype.emplaceBack = createEmplaceBack(members, StructType.prototype.BYTE_SIZE); + StructArrayType.prototype.emplaceBack = createEmplaceBack(StructType.prototype.members, StructType.prototype.BYTE_SIZE); + StructArrayType.prototype._usedTypes = usedTypes; + + + structArrayTypeCache[key] = StructArrayType; return StructArrayType; } +/** + * Serialize the StructArray type. This serializes the *type* not an instance of the type. + * @private + */ +function serializeStructArrayType() { + return { + members: this.prototype.StructType.prototype.members, + alignment: this.prototype.StructType.prototype.alignment, + BYTES_PER_ELEMENT: this.prototype.BYTES_PER_ELEMENT + }; +} + + function align(offset, size) { return Math.ceil(offset / size) * size; } @@ -70,88 +157,178 @@ function sizeOf(type) { } function getArrayViewName(type) { - return type.toLowerCase() + 'Array'; + return type.toLowerCase(); } +/* + * > I saw major perf gains by shortening the source of these generated methods (i.e. renaming + * > elementIndex to i) (likely due to v8 inlining heuristics). + * - lucaswoj + */ function createEmplaceBack(members, BYTES_PER_ELEMENT) { + var usedTypeSizes = []; var argNames = []; var body = '' + - 'var pos1 = this.length * ' + BYTES_PER_ELEMENT.toFixed(0) + ';\n' + - 'var pos2 = pos1 / 2;\n' + - 'var pos4 = pos1 / 4;\n' + + 'var i = this.length;\n' + 'this.length++;\n' + - 'this.metadataArray[0]++;\n' + - 'if (this.length > this.allocatedLength) this.resize(this.length);\n'; + 'if (this.length > this.capacity) this._resize(this.length);\n'; + for (var m = 0; m < members.length; m++) { var member = members[m]; - var argName = 'arg_' + m; - var index = 'pos' + sizeOf(member.type).toFixed(0) + ' + ' + (member.offset / sizeOf(member.type)).toFixed(0); - body += 'this.' + getArrayViewName(member.type) + '[' + index + '] = ' + argName + ';\n'; - argNames.push(argName); + var size = sizeOf(member.type); + + if (usedTypeSizes.indexOf(size) < 0) { + usedTypeSizes.push(size); + body += 'var o' + size.toFixed(0) + ' = i * ' + (BYTES_PER_ELEMENT / size).toFixed(0) + ';\n'; + } + + for (var c = 0; c < member.components; c++) { + var argName = 'v' + argNames.length; + var index = 'o' + size.toFixed(0) + ' + ' + (member.offset / size + c).toFixed(0); + body += 'this.' + getArrayViewName(member.type) + '[' + index + '] = ' + argName + ';\n'; + argNames.push(argName); + } } + + body += 'return i;'; + return new Function(argNames, body); } -function createGetter(type, offset) { - var index = 'this._pos' + sizeOf(type).toFixed(0) + ' + ' + (offset / sizeOf(type)).toFixed(0); - return new Function([], 'return this._structArray.' + getArrayViewName(type) + '[' + index + '];'); +function createMemberComponentString(member, component) { + var elementOffset = 'this._pos' + sizeOf(member.type).toFixed(0); + var componentOffset = (member.offset / sizeOf(member.type) + component).toFixed(0); + var index = elementOffset + ' + ' + componentOffset; + return 'this._structArray.' + getArrayViewName(member.type) + '[' + index + ']'; + } -function createSetter(type, offset) { - var index = 'this._pos' + sizeOf(type).toFixed(0) + ' + ' + (offset / sizeOf(type)).toFixed(0); - return new Function(['x'], 'this._structArray.' + getArrayViewName(type) + '[' + index + '] = x;'); +function createGetter(member, c) { + return new Function([], 'return ' + createMemberComponentString(member, c) + ';'); } +function createSetter(member, c) { + return new Function(['x'], createMemberComponentString(member, c) + ' = x;'); +} +/** + * @class Struct + * @param {StructArray} structArray The StructArray the struct is stored in + * @param {number} index The index of the struct in the StructArray. + * @private + */ function Struct(structArray, index) { this._structArray = structArray; this._setIndex(index); } +/** + * Make this Struct object point to a different instance in the same array. + * It can be cheaper to use .setIndex to re-use an existing Struct than to + * create a new one. + * @param {number} index The index of the struct in the StructArray; + * @private + */ Struct.prototype._setIndex = function(index) { this._pos1 = index * this.BYTE_SIZE; this._pos2 = this._pos1 / 2; this._pos4 = this._pos1 / 4; + this._pos8 = this._pos1 / 8; }; -function StructArray(initialAllocatedLength) { - if (initialAllocatedLength instanceof ArrayBuffer) { - this.arrayBuffer = initialAllocatedLength; +/** + * @class StructArray + * The StructArray class is inherited by the custom StructArrayType classes created with + * `new StructArrayType(members, options)`. + * @private + */ +function StructArray(serialized) { + if (serialized !== undefined) { + // Create from an serialized StructArray + this.arrayBuffer = serialized.arrayBuffer; + this.length = serialized.length; + this.capacity = this.arrayBuffer.byteLength / this.BYTES_PER_ELEMENT; this._refreshViews(); - this.length = this.metadataArray[0]; - this.allocatedLength = this.uint8Array.length / this.BYTES_PER_ELEMENT; + + // Create a new StructArray } else { - if (initialAllocatedLength === undefined) { - initialAllocatedLength = this.DEFAULT_ALLOCATED_LENGTH; - } - this.resize(initialAllocatedLength); + this.length = 0; + this.capacity = 0; + this._resize(this.DEFAULT_CAPACITY); } } -StructArray.prototype.DEFAULT_ALLOCATED_LENGTH = 100; -StructArray.prototype.RESIZE_FACTOR = 1.5; -StructArray.prototype.allocatedLength = 0; -StructArray.prototype.length = 0; -var METADATA_BYTES = align(4, 8); +/** + * @property {number} + * @private + * @readonly + */ +StructArray.prototype.DEFAULT_CAPACITY = 128; + +/** + * @property {number} + * @private + * @readonly + */ +StructArray.prototype.RESIZE_MULTIPLIER = 5; + +/** + * Serialize this StructArray instance + * @private + */ +StructArray.prototype.serialize = function() { + this.trim(); + return { + length: this.length, + arrayBuffer: this.arrayBuffer + }; +}; -StructArray.prototype.resize = function(n) { - this.allocatedLength = Math.max(n, Math.floor(this.allocatedLength * this.RESIZE_FACTOR)); - this.arrayBuffer = new ArrayBuffer(METADATA_BYTES + align(this.allocatedLength * this.BYTES_PER_ELEMENT, 8)); +/** + * Return the Struct at the given location in the array. + * @private + * @param {number} index The index of the element. + */ +StructArray.prototype.get = function(index) { + return new this.StructType(this, index); +}; - var oldUint8Array = this.uint8Array; +/** + * Resize the buffer to discard unused capacity. + * @private + */ +StructArray.prototype.trim = function() { + if (this.length !== this.capacity) { + this.capacity = this.length; + this.arrayBuffer = this.arrayBuffer.slice(0, this.length * this.BYTES_PER_ELEMENT); + this._refreshViews(); + } +}; + +/** + * Resize the array so that it fits at least `n` elements. + * @private + * @param {number} n The number of elements that must fit in the array after the resize. + */ +StructArray.prototype._resize = function(n) { + this.capacity = Math.max(n, Math.floor(this.capacity * this.RESIZE_MULTIPLIER)); + this.arrayBuffer = new ArrayBuffer(this.capacity * this.BYTES_PER_ELEMENT); + + var oldUint8Array = this.uint8; this._refreshViews(); - if (oldUint8Array) this.uint8Array.set(oldUint8Array); + if (oldUint8Array) this.uint8.set(oldUint8Array); }; +/** + * Create TypedArray views for the current ArrayBuffer. + * @private + */ StructArray.prototype._refreshViews = function() { - for (var t in viewTypes) { - this[getArrayViewName(t)] = new viewTypes[t](this.arrayBuffer, METADATA_BYTES); + for (var t = 0; t < this._usedTypes.length; t++) { + var type = this._usedTypes[t]; + this[getArrayViewName(type)] = new viewTypes[type](this.arrayBuffer); } - this.metadataArray = new Uint32Array(this.arrayBuffer, 0, 1); }; -StructArray.prototype.at = function(index) { - return new this.StructType(this, index); -}; diff --git a/test/js/data/bucket.test.js b/test/js/data/bucket.test.js index 14ebf61129d..36177d18d1a 100644 --- a/test/js/data/bucket.test.js +++ b/test/js/data/bucket.test.js @@ -1,7 +1,6 @@ 'use strict'; var test = require('tap').test; -var Buffer = require('../../../js/data/buffer'); var Bucket = require('../../../js/data/bucket'); var util = require('../../../js/util/util'); @@ -25,11 +24,12 @@ test('Bucket', function(t) { attributes: [{ name: 'map', + type: 'Int16', value: ['x'] }, { name: 'box', components: 2, - type: Buffer.AttributeType.SHORT, + type: 'Int16', value: ['x * 2', 'y * 2'] }] } @@ -68,20 +68,25 @@ test('Bucket', function(t) { bucket.features = [createFeature(17, 42)]; bucket.populateBuffers(); - var testVertex = bucket.buffers.testVertex; - t.equal(testVertex.type, Buffer.BufferType.VERTEX); + var testVertex = bucket.structArrays.testVertex; t.equal(testVertex.length, 1); - t.deepEqual(testVertex.get(0), { map: [17], box: [34, 84] }); + var v0 = testVertex.get(0); + t.equal(v0.map, 17); + t.equal(v0.box0, 34); + t.equal(v0.box1, 84); - var testElement = bucket.buffers.testElement; - t.equal(testElement.type, Buffer.BufferType.ELEMENT); + var testElement = bucket.structArrays.testElement; t.equal(testElement.length, 1); - t.deepEqual(testElement.get(0), { vertices: [1, 2, 3] }); + var e1 = testElement.get(0); + t.equal(e1.vertices0, 1); + t.equal(e1.vertices1, 2); + t.equal(e1.vertices2, 3); - var testSecondElement = bucket.buffers.testSecondElement; - t.equal(testSecondElement.type, Buffer.BufferType.ELEMENT); + var testSecondElement = bucket.structArrays.testSecondElement; t.equal(testSecondElement.length, 1); - t.deepEqual(testSecondElement.get(0), { vertices: [17, 42] }); + var e2 = testSecondElement.get(0); + t.equal(e2.vertices0, 17); + t.equal(e2.vertices1, 42); t.end(); }); @@ -92,12 +97,12 @@ test('Bucket', function(t) { bucket.features = [createFeature(17, 42)]; bucket.populateBuffers(); - bucket.createBuffers(); - var buffers = bucket.buffers; + bucket.createStructArrays(); + var structArrays = bucket.structArrays; - t.equal(bucket.buffers, buffers); - t.equal(buffers.testElement.length, 0); - t.equal(buffers.testSecondElement.length, 0); + t.equal(bucket.structArrays, structArrays); + t.equal(structArrays.testElement.length, 0); + t.equal(structArrays.testSecondElement.length, 0); t.equal(bucket.elementGroups.test.length, 0); t.end(); @@ -108,21 +113,29 @@ test('Bucket', function(t) { bucket.features = [createFeature(1, 5)]; bucket.populateBuffers(); - bucket.createBuffers(); + bucket.createStructArrays(); bucket.features = [createFeature(17, 42)]; bucket.populateBuffers(); - var testVertex = bucket.buffers.testVertex; + var testVertex = bucket.structArrays.testVertex; t.equal(testVertex.length, 1); - t.deepEqual(testVertex.get(0), { map: [17], box: [34, 84] }); + var v0 = testVertex.get(0); + t.equal(v0.map, 17); + t.equal(v0.box0, 34); + t.equal(v0.box1, 84); - var testElement = bucket.buffers.testElement; + var testElement = bucket.structArrays.testElement; t.equal(testElement.length, 1); - t.deepEqual(testElement.get(0), { vertices: [1, 2, 3] }); + var e1 = testElement.get(0); + t.equal(e1.vertices0, 1); + t.equal(e1.vertices1, 2); + t.equal(e1.vertices2, 3); - var testSecondElement = bucket.buffers.testSecondElement; + var testSecondElement = bucket.structArrays.testSecondElement; t.equal(testSecondElement.length, 1); - t.deepEqual(testSecondElement.get(0), { vertices: [17, 42] }); + var e2 = testSecondElement.get(0); + t.equal(e2.vertices0, 17); + t.equal(e2.vertices1, 42); t.end(); }); diff --git a/test/js/data/buffer.test.js b/test/js/data/buffer.test.js index dc4c52a7bcd..b5a691f0c61 100644 --- a/test/js/data/buffer.test.js +++ b/test/js/data/buffer.test.js @@ -2,87 +2,48 @@ var test = require('tap').test; var Buffer = require('../../../js/data/buffer'); -var util = require('../../../js/util/util'); +var StructArrayType = require('../../../js/util/struct_array'); test('Buffer', function(t) { - function create(options) { - return new Buffer(util.extend({}, { - type: Buffer.BufferType.VERTEX, - attributes: [ - { name: 'map' }, - { name: 'box', components: 2, type: Buffer.AttributeType.SHORT } - ] - }, options)); - } - - t.test('constructs itself', function(t) { - var buffer = create(); - - t.equal(buffer.type, Buffer.BufferType.VERTEX); - t.equal(buffer.capacity, 1024); - t.equal(buffer.length, 0); - t.equal(buffer.itemSize, 8); - t.ok(buffer.arrayBuffer); - - t.deepEqual(buffer.attributes, [{ - name: 'map', - components: 1, - type: Buffer.AttributeType.UNSIGNED_BYTE, - size: 1, - offset: 0 - }, { - name: 'box', - components: 2, - type: Buffer.AttributeType.SHORT, - size: 4, - offset: 4 - }]); - - t.end(); - }); - - t.test('pushes items', function(t) { - var buffer = create(); - - t.equal(0, buffer.push(1, 7, 3)); - t.equal(1, buffer.push(4, 2, 5)); - - t.equal(buffer.length, 2); - - t.deepEqual(buffer.get(0), {map: [1], box: [7, 3]}); - t.deepEqual(buffer.get(1), {map: [4], box: [2, 5]}); - - t.end(); + var TestArray = new StructArrayType({ + members: [ + { type: 'Int16', name: 'map' }, + { type: 'Int16', name: 'box', components: 2 } + ], + alignment: 4 }); - t.test('automatically resizes', function(t) { - var buffer = create(); - var capacityInitial = buffer.capacity; - - while (capacityInitial > buffer.length * buffer.itemSize) { - buffer.push(1, 1, 1); - } - t.equal(buffer.capacity, capacityInitial); - - buffer.push(1, 1, 1); - t.ok(buffer.capacity > capacityInitial); + t.test('constructs itself', function(t) { + var array = new TestArray(); + array.emplaceBack(1, 1, 1); + array.emplaceBack(1, 1, 1); + array.emplaceBack(1, 1, 1); + + var buffer = new Buffer(array.serialize(), TestArray.serialize(), Buffer.BufferType.VERTEX); + + t.deepEqual(buffer.attributes, [ + { + name: 'map', + components: 1, + type: 'Int16', + offset: 0 + }, { + name: 'box', + components: 2, + type: 'Int16', + offset: 4 + }]); + + t.deepEqual(buffer.itemSize, 8); + t.deepEqual(buffer.length, 3); + t.ok(buffer.arrayBuffer); + t.equal(buffer.type, Buffer.BufferType.VERTEX); t.end(); - }); - - t.test('trims', function(t) { - var buffer = create(); - var capacityInitial = buffer.capacity; - - buffer.push(1, 1, 1); - t.equal(buffer.capacity, capacityInitial); - buffer.trim(); - t.equal(buffer.capacity, buffer.itemSize); - - t.end(); }); t.end(); + }); diff --git a/test/js/data/fill_bucket.test.js b/test/js/data/fill_bucket.test.js index 86b19e896f3..80838797b77 100644 --- a/test/js/data/fill_bucket.test.js +++ b/test/js/data/fill_bucket.test.js @@ -21,7 +21,7 @@ test('FillBucket', function(t) { buffers: {}, layer: { id: 'test', type: 'fill', layout: {} } }); - bucket.createBuffers(); + bucket.createStructArrays(); t.equal(bucket.addFill([ new Point(0, 0), diff --git a/test/js/data/line_bucket.test.js b/test/js/data/line_bucket.test.js index 3cb3f93c638..465e84a10d4 100644 --- a/test/js/data/line_bucket.test.js +++ b/test/js/data/line_bucket.test.js @@ -17,7 +17,7 @@ test('LineBucket', function(t) { buffers: {}, layer: { id: 'test', type: 'line', layout: {} } }); - bucket.createBuffers(); + bucket.createStructArrays(); var pointWithScale = new Point(0, 0); pointWithScale.scale = 10; diff --git a/test/js/data/symbol_bucket.test.js b/test/js/data/symbol_bucket.test.js index bf94968f142..1611ca0952f 100644 --- a/test/js/data/symbol_bucket.test.js +++ b/test/js/data/symbol_bucket.test.js @@ -37,7 +37,7 @@ test('SymbolBucket', function(t) { layer: { id: 'test', type: 'symbol', layout: {'text-font': ['Test'] }}, tileExtent: 4096 }); - bucket.createBuffers(); + bucket.createStructArrays(); bucket.textFeatures = ['abcde']; bucket.features = [feature]; return bucket; diff --git a/test/js/symbol/collision_feature.js b/test/js/symbol/collision_feature.js index 3da2ca447e6..a6a9f6ac982 100644 --- a/test/js/symbol/collision_feature.js +++ b/test/js/symbol/collision_feature.js @@ -24,7 +24,7 @@ test('CollisionFeature', function(t) { var cf = new CollisionFeature(collisionBoxArray, [point], anchor, 0, 0, 0, shapedText, 1, 0, false); t.equal(cf.boxEndIndex - cf.boxStartIndex, 1); - var box = collisionBoxArray.at(cf.boxStartIndex); + var box = collisionBoxArray.get(cf.boxStartIndex); t.equal(box.x1, -50); t.equal(box.x2, 50); t.equal(box.y1, -10); @@ -129,7 +129,7 @@ test('CollisionFeature', function(t) { function pluckAnchorPoints(cf) { var result = []; for (var i = cf.boxStartIndex; i < cf.boxEndIndex; i++) { - result.push(collisionBoxArray.at(i).anchorPoint); + result.push(collisionBoxArray.get(i).anchorPoint); } return result; } diff --git a/test/js/util/struct_array.test.js b/test/js/util/struct_array.test.js new file mode 100644 index 00000000000..195190fc09b --- /dev/null +++ b/test/js/util/struct_array.test.js @@ -0,0 +1,98 @@ +'use strict'; + +var test = require('tap').test; +var StructArrayType = require('../../../js/util/struct_array'); + +test('StructArray', function(t) { + + var TestArray = new StructArrayType({ + members: [ + { type: 'Int16', name: 'map' }, + { type: 'Int16', name: 'box', components: 2 } + ], + alignment: 4 + }); + + t.test('type defined', function(t) { + + t.deepEqual(TestArray.serialize(), { + members: [{ + name: 'map', + components: 1, + type: 'Int16', + offset: 0 + }, { + name: 'box', + components: 2, + type: 'Int16', + offset: 4 + }], + BYTES_PER_ELEMENT: 8, + alignment: 4 + }); + + t.end(); + }); + + t.test('array constructs itself', function(t) { + var array = new TestArray(); + + t.equal(array.length, 0); + t.equal(array.BYTES_PER_ELEMENT, 8); + t.ok(array.arrayBuffer); + + t.end(); + }); + + t.test('emplaceBack', function(t) { + var array = new TestArray(); + + t.equal(0, array.emplaceBack(1, 7, 3)); + t.equal(1, array.emplaceBack(4, 2, 5)); + + t.equal(array.length, 2); + + var e0 = array.get(0); + t.equal(e0.map, 1); + t.equal(e0.box0, 7); + t.equal(e0.box1, 3); + var e1 = array.get(1); + t.equal(e1.map, 4); + t.equal(e1.box0, 2); + t.equal(e1.box1, 5); + + t.end(); + }); + + t.test('automatically resizes', function(t) { + var array = new TestArray(); + var initialCapacity = array.capacity; + + while (initialCapacity > array.length) { + array.emplaceBack(1, 1, 1); + } + + t.equal(array.capacity, initialCapacity); + + array.emplaceBack(1, 1, 1); + t.ok(array.capacity > initialCapacity); + + t.end(); + }); + + t.test('trims', function(t) { + var array = new TestArray(); + var capacityInitial = array.capacity; + + array.emplaceBack(1, 1, 1); + t.equal(array.capacity, capacityInitial); + + array.trim(); + t.equal(array.capacity, 1); + t.equal(array.arrayBuffer.byteLength, array.BYTES_PER_ELEMENT); + + t.end(); + }); + + t.end(); +}); From 1594938f83078f3ef855372d27b6d954fabba721 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Mon, 14 Mar 2016 16:56:28 -0700 Subject: [PATCH 32/48] remove dead code --- js/source/worker.js | 9 ---- js/source/worker_tile.js | 25 ---------- test/js/source/tile.test.js | 74 ++++++++++++++++++++++++++++++ test/js/source/worker_tile.test.js | 52 +-------------------- 4 files changed, 75 insertions(+), 85 deletions(-) create mode 100644 test/js/source/tile.test.js diff --git a/js/source/worker.js b/js/source/worker.js index 50e6966ae24..593e7da3749 100644 --- a/js/source/worker.js +++ b/js/source/worker.js @@ -223,14 +223,5 @@ util.extend(Worker.prototype, { } else { callback(null, []); } - }, - - 'query source features': function(params, callback) { - var tile = this.loaded[params.source] && this.loaded[params.source][params.uid]; - if (tile) { - callback(null, tile.querySourceFeatures(params.params)); - } else { - callback(null, null); - } } }); diff --git a/js/source/worker_tile.js b/js/source/worker_tile.js index 2f4a5d91f34..d737b32c4d2 100644 --- a/js/source/worker_tile.js +++ b/js/source/worker_tile.js @@ -3,7 +3,6 @@ var FeatureTree = require('../data/feature_tree'); var CollisionTile = require('../symbol/collision_tile'); var Bucket = require('../data/bucket'); -var featureFilter = require('feature-filter'); var CollisionBoxArray = require('../symbol/collision_box'); var StringNumberMapping = require('../util/string_number_mapping'); @@ -269,27 +268,3 @@ function getTransferables(buckets) { } return transferables; } - -WorkerTile.prototype.querySourceFeatures = function(params) { - if (!this.data) return null; - - var layer = this.data.layers ? - this.data.layers[params.sourceLayer] : - this.data; - - if (!layer) return null; - - var filter = featureFilter(params.filter); - - var features = []; - for (var i = 0; i < layer.length; i++) { - var feature = layer.feature(i); - if (filter(feature)) { - var geojsonFeature = feature.toGeoJSON(this.coord.x, this.coord.y, this.coord.z); - geojsonFeature.tile = { z: this.coord.z, x: this.coord.x, y: this.coord.y }; - features.push(geojsonFeature); - } - } - - return features; -}; diff --git a/test/js/source/tile.test.js b/test/js/source/tile.test.js new file mode 100644 index 00000000000..716ba205e77 --- /dev/null +++ b/test/js/source/tile.test.js @@ -0,0 +1,74 @@ +'use strict'; + +var test = require('tap').test; +var Tile = require('../../../js/source/tile'); +var GeoJSONWrapper = require('../../../js/source/geojson_wrapper'); +var TileCoord = require('../../../js/source/tile_coord'); +var fs = require('fs'); +var path = require('path'); +var vtpbf = require('vt-pbf'); + +test('querySourceFeatures', function(t) { + var features = [{ + type: 1, + geometry: [0, 0], + tags: { oneway: true } + }]; + + + t.test('geojson tile', function(t) { + var tile = new Tile(new TileCoord(1, 1, 1)); + var result; + + result = []; + tile.querySourceFeatures(result, {}); + t.equal(result.length, 0); + + var geojsonWrapper = new GeoJSONWrapper(features); + geojsonWrapper.name = '_geojsonTileLayer'; + tile.rawTileData = vtpbf({ layers: { '_geojsonTileLayer': geojsonWrapper }}); + + result = []; + tile.querySourceFeatures(result, {}); + t.equal(result.length, 1); + t.deepEqual(result[0].properties, features[0].tags); + result = []; + tile.querySourceFeatures(result, { filter: ['==', 'oneway', true]}); + t.equal(result.length, 1); + result = []; + tile.querySourceFeatures(result, { filter: ['!=', 'oneway', true]}); + t.equal(result.length, 0); + t.end(); + }); + + t.test('vector tile', function(t) { + var tile = new Tile(new TileCoord(1, 1, 1)); + var result; + + result = []; + tile.querySourceFeatures(result, {}); + t.equal(result.length, 0); + + tile.rawTileData = fs.readFileSync(path.join(__dirname, '/../../fixtures/mbsv5-6-18-23.vector.pbf')); + + result = []; + tile.querySourceFeatures(result, { 'sourceLayer': 'does-not-exist'}); + t.equal(result.length, 0); + + result = []; + tile.querySourceFeatures(result, { 'sourceLayer': 'road' }); + t.equal(result.length, 3); + + result = []; + tile.querySourceFeatures(result, { 'sourceLayer': 'road', filter: ['==', 'class', 'main'] }); + t.equal(result.length, 1); + result = []; + tile.querySourceFeatures(result, { 'sourceLayer': 'road', filter: ['!=', 'class', 'main'] }); + t.equal(result.length, 2); + + t.end(); + }); + + + t.end(); +}); diff --git a/test/js/source/worker_tile.test.js b/test/js/source/worker_tile.test.js index 8b66490a9b3..0ff073eb146 100644 --- a/test/js/source/worker_tile.test.js +++ b/test/js/source/worker_tile.test.js @@ -4,10 +4,6 @@ var test = require('tap').test; var WorkerTile = require('../../../js/source/worker_tile'); var Wrapper = require('../../../js/source/geojson_wrapper'); var TileCoord = require('../../../js/source/tile_coord'); -var vt = require('vector-tile'); -var fs = require('fs'); -var path = require('path'); -var Protobuf = require('pbf'); test('basic', function(t) { var buckets = [{ @@ -43,58 +39,12 @@ test('basic', function(t) { layout: { visibility: 'none' }, compare: function () { return true; } }); - tile.parse(new Wrapper(features), buckets, {}, function(err, result) { + tile.parse(new Wrapper(features), buckets, {}, null, function(err, result) { t.equal(err, null); t.equal(Object.keys(result.buckets[0].elementGroups).length, 1, 'element groups exclude hidden layer'); t.end(); }); }); - t.end(); -}); - -test('querySourceFeatures', function(t) { - var features = [{ - type: 1, - geometry: [0, 0], - tags: { oneway: true } - }]; - - - t.test('geojson tile', function(t) { - var tile = new WorkerTile({uid: '', zoom: 0, maxZoom: 20, tileSize: 512, source: 'source', - coord: new TileCoord(1, 1, 1), overscaling: 1 }); - - t.equal(tile.querySourceFeatures({}), null); - - tile.data = new Wrapper(features); - - t.equal(tile.querySourceFeatures({}).length, 1); - t.deepEqual(tile.querySourceFeatures({})[0].properties, features[0].tags); - t.equal(tile.querySourceFeatures({ filter: ['==', 'oneway', true]}).length, 1); - t.equal(tile.querySourceFeatures({ filter: ['!=', 'oneway', true]}).length, 0); - t.end(); - }); - - t.test('vector tile', function(t) { - var tile = new WorkerTile({uid: '', zoom: 0, maxZoom: 20, tileSize: 512, source: 'source', - coord: new TileCoord(1, 1, 1), overscaling: 1 }); - - t.equal(tile.querySourceFeatures({}), null); - - tile.data = new vt.VectorTile(new Protobuf(new Uint8Array(fs.readFileSync(path.join(__dirname, '/../../fixtures/mbsv5-6-18-23.vector.pbf'))))); - - t.equal(tile.querySourceFeatures({ 'sourceLayer': 'does-not-exist'}), null); - - var roads = tile.querySourceFeatures({ 'sourceLayer': 'road' }); - t.equal(roads.length, 3); - - t.equal(tile.querySourceFeatures({ 'sourceLayer': 'road', filter: ['==', 'class', 'main'] }).length, 1); - t.equal(tile.querySourceFeatures({ 'sourceLayer': 'road', filter: ['!=', 'class', 'main'] }).length, 2); - - t.end(); - }); - - t.end(); }); From e56a49ef65fa6ac3246a835d15840b81ff682c89 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Mon, 14 Mar 2016 18:02:55 -0700 Subject: [PATCH 33/48] fix querying empty geojson tiles --- js/source/source.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/source/source.js b/js/source/source.js index 5a8cf412e3e..ebcca1e5a0f 100644 --- a/js/source/source.js +++ b/js/source/source.js @@ -144,7 +144,7 @@ exports._queryRenderedVectorFeatures = function(queryGeometry, params, classes, var renderedFeatureLayers = []; for (var r = 0; r < tilesIn.length; r++) { var tileIn = tilesIn[r]; - if (!tileIn.tile.loaded) continue; + if (!tileIn.tile.featureTree) continue; renderedFeatureLayers.push(tileIn.tile.featureTree.query({ queryGeometry: tileIn.queryGeometry, From dfb96c85439ae05c830bf89dc44ad13b00475404 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Mon, 14 Mar 2016 18:21:11 -0700 Subject: [PATCH 34/48] rename layers property from `layer` to `layers` and accept only an array of strings. fix #2230 --- js/data/feature_tree.js | 4 ++-- js/style/style.js | 8 -------- js/ui/map.js | 12 ++++++------ test/js/style/style.test.js | 10 +++++----- 4 files changed, 13 insertions(+), 21 deletions(-) diff --git a/js/data/feature_tree.js b/js/data/feature_tree.js index 75d286f3700..954037f39f9 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_tree.js @@ -155,12 +155,12 @@ FeatureTree.prototype.query = function(args, styleLayersByID, returnGeoJSON) { var matching = this.grid.query(minX - additionalRadius, minY - additionalRadius, maxX + additionalRadius, maxY + additionalRadius); matching.sort(topDown); var match = this.featureIndexArray.get(0); - this.filterMatching(result, matching, match, queryGeometry, filter, params.layerIDs, styleLayersByID, args.bearing, pixelsToTileUnits, returnGeoJSON); + this.filterMatching(result, matching, match, queryGeometry, filter, params.layers, styleLayersByID, args.bearing, pixelsToTileUnits, returnGeoJSON); var matchingSymbols = this.collisionTile.queryRenderedSymbols(minX, minY, maxX, maxY, args.scale); var match2 = this.collisionTile.collisionBoxArray.get(0); matchingSymbols.sort(); - this.filterMatching(result, matchingSymbols, match2, queryGeometry, filter, params.layerIDs, styleLayersByID, args.bearing, pixelsToTileUnits, returnGeoJSON); + this.filterMatching(result, matchingSymbols, match2, queryGeometry, filter, params.layers, styleLayersByID, args.bearing, pixelsToTileUnits, returnGeoJSON); return result; }; diff --git a/js/style/style.js b/js/style/style.js index 09895fa2b7d..cf6bf23de65 100644 --- a/js/style/style.js +++ b/js/style/style.js @@ -427,10 +427,6 @@ Style.prototype = util.inherit(Evented, { }, queryRenderedFeaturesAsync: function(queryGeometry, params, classes, zoom, bearing, callback) { - if (params.layer) { - params.layerIDs = Array.isArray(params.layer) ? params.layer : [params.layer]; - } - util.asyncAll(Object.keys(this.sources), function(id, callback) { var source = this.sources[id]; if (source.queryRenderedFeaturesAsync) { @@ -445,10 +441,6 @@ Style.prototype = util.inherit(Evented, { }, queryRenderedFeatures: function(queryGeometry, params, classes, zoom, bearing) { - if (params.layer) { - params.layerIDs = Array.isArray(params.layer) ? params.layer : [params.layer]; - } - var sourceResults = []; for (var id in this.sources) { var source = this.sources[id]; diff --git a/js/ui/map.js b/js/ui/map.js index 596c8f3f077..756e0f240c1 100644 --- a/js/ui/map.js +++ b/js/ui/map.js @@ -383,19 +383,19 @@ util.extend(Map.prototype, /** @lends Map.prototype */{ * * @param {Point|Array|Array|Array>} [pointOrBox] Either [x, y] pixel coordinates of a point, or [[x1, y1], [x2, y2]] pixel coordinates of opposite corners of bounding rectangle. Optional: use entire viewport if omitted. * @param {Object} params - * @param {string|Array} [params.layer] Only return features from a given layer or layers + * @param {Array} [params.layers] Only query features from layers with these layer IDs. * @param {Array} [params.filter] A mapbox-gl-style-spec filter. * @param {featuresCallback} callback function that receives the results * * @returns {Map} `this` * * @example - * map.queryRenderedFeaturesAsync([20, 35], { layer: 'my-layer-name' }, function(err, features) { + * map.queryRenderedFeaturesAsync([20, 35], { layers: ['my-layer-name'] }, function(err, features) { * console.log(features); * }); * * @example - * map.queryRenderedFeaturesAsync([[10, 20], [30, 50]], { layer: 'my-layer-name' }, function(err, features) { + * map.queryRenderedFeaturesAsync([[10, 20], [30, 50]], { layers: ['my-layer-name'] }, function(err, features) { * console.log(features); * }); */ @@ -415,16 +415,16 @@ util.extend(Map.prototype, /** @lends Map.prototype */{ * * @param {Point|Array|Array|Array>} [pointOrBox] Either [x, y] pixel coordinates of a point, or [[x1, y1], [x2, y2]] pixel coordinates of opposite corners of bounding rectangle. Optional: use entire viewport if omitted. * @param {Object} params - * @param {string|Array} [params.layer] Only return features from a given layer or layers + * @param {Array} [params.layers] Only query features from layers with these layer IDs. * @param {Array} [params.filter] A mapbox-gl-style-spec filter. * * @returns {Array} features - An array of [GeoJSON](http://geojson.org/) features matching the query parameters. The GeoJSON properties of each feature are taken from the original source. Each feature object also contains a top-level `layer` property whose value is an object representing the style layer to which the feature belongs. Layout and paint properties in this object contain values which are fully evaluated for the given zoom level and feature. * * @example - * var features = map.queryRenderedFeatures([20, 35], { layer: 'my-layer-name' }); + * var features = map.queryRenderedFeatures([20, 35], { layers: ['my-layer-name'] }); * * @example - * var features = map.queryRenderedFeaturesAsync([[10, 20], [30, 50]], { layer: 'my-layer-name' }); + * var features = map.queryRenderedFeaturesAsync([[10, 20], [30, 50]], { layers: ['my-layer-name'] }); */ queryRenderedFeatures: function(pointOrBox, params) { if (!(pointOrBox instanceof Point || Array.isArray(pointOrBox))) { diff --git a/test/js/style/style.test.js b/test/js/style/style.test.js index 793be7d4064..6aa9ddb12d0 100644 --- a/test/js/style/style.test.js +++ b/test/js/style/style.test.js @@ -983,9 +983,9 @@ test('Style#queryRenderedFeaturesAsync', function(t) { }] }; - if (params.layer) { + if (params.layers) { for (var l in features) { - if (params.layerIDs.indexOf(l) < 0) { + if (params.layers.indexOf(l) < 0) { delete features[l]; } } @@ -1004,8 +1004,8 @@ test('Style#queryRenderedFeaturesAsync', function(t) { }); }); - t.test('filters by `layer` option', function(t) { - style.queryRenderedFeaturesAsync([{column: 1, row: 1, zoom: 1}], {layer: 'land'}, {}, 0, 0, function(err, results) { + t.test('filters by `layers` option', function(t) { + style.queryRenderedFeaturesAsync([{column: 1, row: 1, zoom: 1}], {layers: 'land'}, {}, 0, 0, function(err, results) { t.error(err); t.equal(results.length, 2); t.end(); @@ -1056,7 +1056,7 @@ test('Style#queryRenderedFeaturesAsync', function(t) { }); t.test('include multiple layers', function(t) { - style.queryRenderedFeaturesAsync([{column: 1, row: 1, zoom: 1}], {layer: ['land', 'landref']}, {}, 0, 0, function(err, results) { + style.queryRenderedFeaturesAsync([{column: 1, row: 1, zoom: 1}], {layers: ['land', 'landref']}, {}, 0, 0, function(err, results) { t.error(err); t.equals(results.length, 3); t.end(); From 3164a64d5f7e32b7ad6e80c494ac9ae1aa73c342 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Mon, 14 Mar 2016 19:11:59 -0700 Subject: [PATCH 35/48] fix querying after reloading vector tiles --- bench/buffer_benchmark.js | 2 +- js/source/vector_tile_source.js | 1 + js/source/worker.js | 6 +++--- js/source/worker_tile.js | 2 +- test/js/source/worker_tile.test.js | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/bench/buffer_benchmark.js b/bench/buffer_benchmark.js index 271a87f6d22..702033f2a17 100644 --- a/bench/buffer_benchmark.js +++ b/bench/buffer_benchmark.js @@ -163,7 +163,7 @@ function runSample(stylesheet, getGlyphs, getIcons, getTile, callback) { getTile(url, function(err, response) { if (err) throw err; var data = new VT.VectorTile(new Protobuf(new Uint8Array(response))); - workerTile.parse(data, layers, actor, function(err) { + workerTile.parse(data, layers, actor, null, function(err) { if (err) return callback(err); eachCallback(); }); diff --git a/js/source/vector_tile_source.js b/js/source/vector_tile_source.js index cd1f5544260..822c5273680 100644 --- a/js/source/vector_tile_source.js +++ b/js/source/vector_tile_source.js @@ -73,6 +73,7 @@ VectorTileSource.prototype = util.inherit(Evented, { }; if (tile.workerID) { + params.rawTileData = tile.rawTileData; this.dispatcher.send('reload tile', params, this._tileLoaded.bind(this, tile), tile.workerID); } else { tile.workerID = this.dispatcher.send('load tile', params, this._tileLoaded.bind(this, tile)); diff --git a/js/source/worker.js b/js/source/worker.js index 593e7da3749..668a48265d2 100644 --- a/js/source/worker.js +++ b/js/source/worker.js @@ -84,7 +84,7 @@ util.extend(Worker.prototype, { if (err) return callback(err); tile.data = new vt.VectorTile(new Protobuf(new Uint8Array(data))); - tile.parse(tile.data, this.layers, this.actor, callback, data); + tile.parse(tile.data, this.layers, this.actor, data, callback); this.loaded[source] = this.loaded[source] || {}; this.loaded[source][uid] = tile; @@ -96,7 +96,7 @@ util.extend(Worker.prototype, { uid = params.uid; if (loaded && loaded[uid]) { var tile = loaded[uid]; - tile.parse(tile.data, this.layers, this.actor, callback); + tile.parse(tile.data, this.layers, this.actor, params.rawTileData, callback); } }, @@ -188,7 +188,7 @@ util.extend(Worker.prototype, { var geojsonWrapper = new GeoJSONWrapper(geoJSONTile.features); geojsonWrapper.name = '_geojsonTileLayer'; var rawTileData = vtpbf({ layers: { '_geojsonTileLayer': geojsonWrapper }}).buffer; - tile.parse(geojsonWrapper, this.layers, this.actor, callback, rawTileData); + tile.parse(geojsonWrapper, this.layers, this.actor, rawTileData, callback); } else { return callback(null, null); // nothing in the given tile } diff --git a/js/source/worker_tile.js b/js/source/worker_tile.js index d737b32c4d2..514cf37150d 100644 --- a/js/source/worker_tile.js +++ b/js/source/worker_tile.js @@ -20,7 +20,7 @@ function WorkerTile(params) { this.showCollisionBoxes = params.showCollisionBoxes; } -WorkerTile.prototype.parse = function(data, layers, actor, callback, rawTileData) { +WorkerTile.prototype.parse = function(data, layers, actor, rawTileData, callback) { this.status = 'parsing'; this.data = data; diff --git a/test/js/source/worker_tile.test.js b/test/js/source/worker_tile.test.js index 0ff073eb146..a39351296e9 100644 --- a/test/js/source/worker_tile.test.js +++ b/test/js/source/worker_tile.test.js @@ -24,7 +24,7 @@ test('basic', function(t) { coord: new TileCoord(1, 1, 1), overscaling: 1 }); t.test('basic worker tile', function(t) { - tile.parse(new Wrapper(features), buckets, {}, function(err, result) { + tile.parse(new Wrapper(features), buckets, {}, null, function(err, result) { t.equal(err, null); t.ok(result.buckets[0]); t.end(); From b6fe1c026a55ed25aadd187e4a1e0e1f2b4f614c Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 15 Mar 2016 18:25:15 -0700 Subject: [PATCH 36/48] test both sync and async queries --- package.json | 2 +- test/suite_implementation.js | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 9deaf546e7c..7582fa244e3 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "express": "^4.13.4", "gl": "^2.1.5", "istanbul": "^0.4.2", - "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#13bfab2d5937b241259586cbf545713b251305e5", + "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#37fb41dad2d9651ee25f92ffd61ed97e516d5e8e", "nyc": "^6.1.1", "sinon": "^1.15.4", "st": "^1.0.0", diff --git a/test/suite_implementation.js b/test/suite_implementation.js index 9eae5be025b..cd612649fcf 100644 --- a/test/suite_implementation.js +++ b/test/suite_implementation.js @@ -47,26 +47,30 @@ module.exports = function(style, options, callback) { tmp.copy(data, end); } + var syncResults = []; if (options.queryGeometry) { - done(null, map.queryRenderedFeatures(options.queryGeometry, options)); + syncResults = map.queryRenderedFeatures(options.queryGeometry, options); + map.queryRenderedFeaturesAsync(options.queryGeometry, options, done); } else { done(null, []); } - function done(err, results) { + function done(err, asyncResults) { map.remove(); gl.destroy(); if (err) return callback(err); - results = results.map(function (r) { - delete r.layer; - r.geometry = null; - return JSON.parse(JSON.stringify(r)); - }); + syncResults = syncResults.map(prepareFeatures); + asyncResults = asyncResults.map(prepareFeatures); - callback(null, data, results); + callback(null, data, syncResults, asyncResults); } + function prepareFeatures(r) { + delete r.layer; + r.geometry = null; + return JSON.parse(JSON.stringify(r)); + } }); }; From 64a77b9a5815e056e5e5fca7bfcd082c46991044 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 16 Mar 2016 13:54:39 -0700 Subject: [PATCH 37/48] add queryRenderedFeatures changes to changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7127c18f152..ccb4baa2e79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ ## master +#### Breaking Changes + +- `map.featuresAt` and `map.featuresIn` are removed. Use `map.queryRenderedFeatures` or `map.querySourceFeatures` instead. To migrate: + - replace `featuresAt` and `featuresIn` with `queryRenderedFeatures` + - `queryRenderedFeatures` is synchronous. Remove the callback and use the return value. An async version `queryRenderedFeaturesAsync` exists for really slow queries. + - rename the `layer` parameters to `layers` and make it an array of strings. + - remove the `radius` paramter. `queryRenderedFeatures` automatically uses style property values when doing queries. + - remove the `includeGeometry` parameter. `queryRenderedFeatures` always includes geometries. + #### New Features & Improvements Improve overall rendering performance. (#2221) @@ -8,6 +17,10 @@ Add `Map#setMaxBounds` method (#2234) Add `isActive` and `isEnabled` methods to interaction handlers (#2238) Add `Map#setZoomBounds` method (#2243) Add touch events (#2195) +- `map.queryRenderedFeatures` can be used to query the styled and rendered representations of features +- `map.queryRenderedFeaturesAsync` is an asynchronous version of `map.queryRenderedFeatures` +- `map.querySourceFeatures` can be used to get features directly from vector tiles, independent of the style. +- interaction for labels (#303) and style-property-aware hit testing (#316) are possible with `map.queryRenderedFeatures` #### Bugfixes From 41c4b70075e3fc4c759ab70cc0a513cb30f1fa7c Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 16 Mar 2016 15:55:32 -0700 Subject: [PATCH 38/48] fix querying symbols with `-allow-overlap: true` --- ... => 3400-01-05-queryrenderedfeatures.html} | 0 js/symbol/collision_tile.js | 24 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) rename docs/_posts/examples/{3400-01-05-featuresat.html => 3400-01-05-queryrenderedfeatures.html} (100%) diff --git a/docs/_posts/examples/3400-01-05-featuresat.html b/docs/_posts/examples/3400-01-05-queryrenderedfeatures.html similarity index 100% rename from docs/_posts/examples/3400-01-05-featuresat.html rename to docs/_posts/examples/3400-01-05-queryrenderedfeatures.html diff --git a/js/symbol/collision_tile.js b/js/symbol/collision_tile.js index 9cf77f4c343..2d091f74470 100644 --- a/js/symbol/collision_tile.js +++ b/js/symbol/collision_tile.js @@ -119,21 +119,21 @@ CollisionTile.prototype.placeCollisionFeature = function(collisionFeature, allow box._setIndex(b); - if (!allowOverlap) { - var anchorPoint = box.anchorPoint._matMult(rotationMatrix); - var x = anchorPoint.x; - var y = anchorPoint.y; + var anchorPoint = box.anchorPoint._matMult(rotationMatrix); + var x = anchorPoint.x; + var y = anchorPoint.y; - var x1 = x + box.x1; - var y1 = y + box.y1 * yStretch; - var x2 = x + box.x2; - var y2 = y + box.y2 * yStretch; + var x1 = x + box.x1; + var y1 = y + box.y1 * yStretch; + var x2 = x + box.x2; + var y2 = y + box.y2 * yStretch; - box.bbox0 = x1; - box.bbox1 = y1; - box.bbox2 = x2; - box.bbox3 = y2; + box.bbox0 = x1; + box.bbox1 = y1; + box.bbox2 = x2; + box.bbox3 = y2; + if (!allowOverlap) { var blockingBoxes = this.grid.query(x1, y1, x2, y2); for (var i = 0; i < blockingBoxes.length; i++) { From 20cc52602780cce3342c928bef5e8f3b420b48c7 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 16 Mar 2016 16:25:57 -0700 Subject: [PATCH 39/48] update examples for new queryRenderedFeatures --- .../examples/3400-01-04-center-on-symbol.html | 26 ++--- .../examples/3400-01-04-hover-styles.html | 18 ++- .../3400-01-05-queryrenderedfeatures.html | 8 +- .../3400-01-06-polygon-popup-on-click.html | 33 ++---- .../examples/3400-01-06-popup-on-click.html | 36 +++--- .../examples/3400-01-06-popup-on-hover.html | 31 +++-- .../3400-01-17-updating-choropleth.html | 34 +++--- ...1-19-using-box-queryrenderedfeatures.html} | 65 +++++------ .../3400-01-20-timeline-animation.html | 62 +++++----- docs/_posts/examples/3400-01-22-measure.html | 110 ++++++++---------- js/ui/map.js | 2 +- 11 files changed, 180 insertions(+), 245 deletions(-) rename docs/_posts/examples/{3400-01-19-using-featuresin.html => 3400-01-19-using-box-queryrenderedfeatures.html} (76%) diff --git a/docs/_posts/examples/3400-01-04-center-on-symbol.html b/docs/_posts/examples/3400-01-04-center-on-symbol.html index 04dc4dfa81c..c5021aa56da 100644 --- a/docs/_posts/examples/3400-01-04-center-on-symbol.html +++ b/docs/_posts/examples/3400-01-04-center-on-symbol.html @@ -2,7 +2,7 @@ layout: example category: example title: Center the map on a clicked marker -description: Using featuresAt and flyTo to center the map on a symbol +description: Using queryRenderedFeatures and flyTo to center the map on a symbol ---
diff --git a/docs/_posts/examples/3400-01-04-hover-styles.html b/docs/_posts/examples/3400-01-04-hover-styles.html index 9812785c764..b7588a92c3d 100644 --- a/docs/_posts/examples/3400-01-04-hover-styles.html +++ b/docs/_posts/examples/3400-01-04-hover-styles.html @@ -2,7 +2,7 @@ layout: example category: example title: Highlight features under the mouse pointer -description: Using featuresAt and a filter to change hover styles +description: Using queryRenderedFeatures and a filter to change hover styles ---
diff --git a/docs/_posts/examples/3400-01-05-queryrenderedfeatures.html b/docs/_posts/examples/3400-01-05-queryrenderedfeatures.html index f7e14277cbc..41ca91e58fb 100644 --- a/docs/_posts/examples/3400-01-05-queryrenderedfeatures.html +++ b/docs/_posts/examples/3400-01-05-queryrenderedfeatures.html @@ -2,7 +2,7 @@ layout: example category: example title: Get features under the mouse pointer -description: Using the featuresAt API to show properties of hovered-over map elements. +description: Using the queryRenderedFeatures API to show properties of hovered-over map elements. ---