diff --git a/js/data/feature_tree.js b/js/data/feature_index.js similarity index 65% rename from js/data/feature_tree.js rename to js/data/feature_index.js index 125b466f1b3..d5701702181 100644 --- a/js/data/feature_tree.js +++ b/js/data/feature_index.js @@ -12,6 +12,12 @@ var Protobuf = require('pbf'); var GeoJSONFeature = require('../util/vectortile_to_geojson'); var arraysIntersect = require('../util/util').arraysIntersect; +var intersection = require('../util/intersection_tests'); +var multiPolygonIntersectsBufferedMultiPoint = intersection.multiPolygonIntersectsBufferedMultiPoint; +var multiPolygonIntersectsMultiPolygon = intersection.multiPolygonIntersectsMultiPolygon; +var multiPolygonIntersectsBufferedMultiLine = intersection.multiPolygonIntersectsBufferedMultiLine; + + var FeatureIndexArray = new StructArrayType({ members: [ // the index of the feature in the original vectortile @@ -22,9 +28,9 @@ var FeatureIndexArray = new StructArrayType({ { type: 'Uint16', name: 'bucketIndex' } ]}); -module.exports = FeatureTree; +module.exports = FeatureIndex; -function FeatureTree(coord, overscaling, collisionTile) { +function FeatureIndex(coord, overscaling, collisionTile) { if (coord.grid) { var serialized = coord; var rawTileData = overscaling; @@ -46,7 +52,7 @@ function FeatureTree(coord, overscaling, collisionTile) { this.setCollisionTile(collisionTile); } -FeatureTree.prototype.insert = function(feature, featureIndex, sourceLayerIndex, bucketIndex) { +FeatureIndex.prototype.insert = function(feature, featureIndex, sourceLayerIndex, bucketIndex) { var key = this.featureIndexArray.length; this.featureIndexArray.emplaceBack(featureIndex, sourceLayerIndex, bucketIndex); var geometry = loadGeometry(feature); @@ -69,11 +75,11 @@ FeatureTree.prototype.insert = function(feature, featureIndex, sourceLayerIndex, } }; -FeatureTree.prototype.setCollisionTile = function(collisionTile) { +FeatureIndex.prototype.setCollisionTile = function(collisionTile) { this.collisionTile = collisionTile; }; -FeatureTree.prototype.serialize = function() { +FeatureIndex.prototype.serialize = function() { var data = { coord: this.coord, overscaling: this.overscaling, @@ -92,7 +98,7 @@ function translateDistance(translate) { } // Finds features in this tile at a particular position. -FeatureTree.prototype.query = function(args, styleLayers) { +FeatureIndex.prototype.query = function(args, styleLayers) { if (!this.vtLayers) { this.vtLayers = new vt.VectorTile(new Protobuf(new Uint8Array(this.rawTileData))).layers; this.sourceLayerCoder = new DictionaryCoder(this.vtLayers ? Object.keys(this.vtLayers).sort() : ['_geojsonTileLayer']); @@ -167,7 +173,7 @@ function getLineWidth(paint) { } } -FeatureTree.prototype.filterMatching = function(result, matching, array, queryGeometry, filter, filterLayerIDs, styleLayers, bearing, pixelsToTileUnits) { +FeatureIndex.prototype.filterMatching = function(result, matching, array, queryGeometry, filter, filterLayerIDs, styleLayers, bearing, pixelsToTileUnits) { var previousIndex; for (var k = 0; k < matching.length; k++) { var index = matching[k]; @@ -291,160 +297,3 @@ function offsetLine(rings, offset) { } return newRings; } - -function multiPolygonIntersectsBufferedMultiPoint(multiPolygon, rings, radius) { - for (var j = 0; j < multiPolygon.length; j++) { - var polygon = multiPolygon[j]; - 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 (polygonContainsPoint(polygon, point)) return true; - if (pointIntersectsBufferedLine(point, polygon, radius)) return true; - } - } - } - return false; -} - -function multiPolygonIntersectsMultiPolygon(multiPolygonA, multiPolygonB) { - - if (multiPolygonA.length === 1 && multiPolygonA[0].length === 1) { - return multiPolygonContainsPoint(multiPolygonB, multiPolygonA[0][0]); - } - - for (var m = 0; m < multiPolygonB.length; m++) { - var ring = multiPolygonB[m]; - for (var n = 0; n < ring.length; n++) { - if (multiPolygonContainsPoint(multiPolygonA, ring[n])) return true; - } - } - - for (var j = 0; j < multiPolygonA.length; j++) { - var polygon = multiPolygonA[j]; - for (var i = 0; i < polygon.length; i++) { - if (multiPolygonContainsPoint(multiPolygonB, polygon[i])) return true; - } - - for (var k = 0; k < multiPolygonB.length; k++) { - if (lineIntersectsLine(polygon, multiPolygonB[k])) return true; - } - } - - return false; -} - -function multiPolygonIntersectsBufferedMultiLine(multiPolygon, multiLine, radius) { - for (var i = 0; i < multiLine.length; i++) { - var line = multiLine[i]; - - for (var j = 0; j < multiPolygon.length; j++) { - var polygon = multiPolygon[j]; - - 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; - } - } - return false; -} - -function lineIntersectsBufferedLine(lineA, lineB, radius) { - - 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; - } - } - - for (var k = 0; k < lineA.length; k++) { - if (pointIntersectsBufferedLine(lineA[k], lineB, radius)) return true; - } - - return false; -} - -function lineIntersectsLine(lineA, lineB) { - for (var i = 0; i < lineA.length - 1; i++) { - var a0 = lineA[i]; - var a1 = lineA[i + 1]; - for (var j = 0; j < lineB.length - 1; j++) { - var b0 = lineB[j]; - var b1 = lineB[j + 1]; - if (lineSegmentIntersectsLineSegment(a0, a1, b0, b1)) return true; - } - } - return false; -} - - -// http://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/ -function isCounterClockwise(a, b, c) { - return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x); -} - -function lineSegmentIntersectsLineSegment(a0, a1, b0, b1) { - return isCounterClockwise(a0, b0, b1) !== isCounterClockwise(a1, b0, b1) && - isCounterClockwise(a0, a1, b0) !== isCounterClockwise(a0, a1, b1); -} - -function pointIntersectsBufferedLine(p, line, radius) { - var radiusSquared = radius * radius; - - if (line.length === 1) return p.distSqr(line[0]) < radiusSquared; - - for (var i = 1; i < line.length; i++) { - // Find line segments that have a distance <= radius^2 to p - // In that case, we treat the line as "containing point p". - var v = line[i - 1], w = line[i]; - if (distToSegmentSquared(p, v, w) < radiusSquared) return true; - } - return false; -} - -// Code from http://stackoverflow.com/a/1501725/331379. -function distToSegmentSquared(p, v, w) { - var l2 = v.distSqr(w); - if (l2 === 0) return p.distSqr(v); - var t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2; - if (t < 0) return p.distSqr(v); - if (t > 1) return p.distSqr(w); - return p.distSqr(w.sub(v)._mult(t)._add(v)); -} - -// point in polygon ray casting algorithm -function multiPolygonContainsPoint(rings, p) { - var c = false, - ring, p1, p2; - - for (var k = 0; k < rings.length; k++) { - ring = rings[k]; - for (var i = 0, j = ring.length - 1; i < ring.length; j = i++) { - p1 = ring[i]; - 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; -} - -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; -} diff --git a/js/source/source.js b/js/source/source.js index 755a98c3cc0..363cdf5279b 100644 --- a/js/source/source.js +++ b/js/source/source.js @@ -105,9 +105,9 @@ exports._queryRenderedVectorFeatures = function(queryGeometry, params, classes, var renderedFeatureLayers = []; for (var r = 0; r < tilesIn.length; r++) { var tileIn = tilesIn[r]; - if (!tileIn.tile.featureTree) continue; + if (!tileIn.tile.featureIndex) continue; - renderedFeatureLayers.push(tileIn.tile.featureTree.query({ + renderedFeatureLayers.push(tileIn.tile.featureIndex.query({ queryGeometry: tileIn.queryGeometry, scale: tileIn.scale, tileSize: tileIn.tile.tileSize, diff --git a/js/source/tile.js b/js/source/tile.js index 075355ae5f6..f43ed1f4f46 100644 --- a/js/source/tile.js +++ b/js/source/tile.js @@ -2,7 +2,7 @@ var util = require('../util/util'); var Bucket = require('../data/bucket'); -var FeatureTree = require('../data/feature_tree'); +var FeatureIndex = require('../data/feature_index'); var vt = require('vector-tile'); var Protobuf = require('pbf'); var GeoJSONFeature = require('../util/vectortile_to_geojson'); @@ -84,7 +84,7 @@ Tile.prototype = { 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.featureIndex = new FeatureIndex(data.featureIndex, data.rawTileData, this.collisionTile); this.rawTileData = data.rawTileData; this.buckets = unserializeBuckets(data.buckets); }, @@ -101,7 +101,7 @@ Tile.prototype = { if (this.isUnloaded) return; this.collisionTile = new CollisionTile(data.collisionTile, this.collisionBoxArray); - this.featureTree.setCollisionTile(this.collisionTile); + this.featureIndex.setCollisionTile(this.collisionTile); // Destroy and delete existing symbol buckets for (var id in this.buckets) { diff --git a/js/source/worker_tile.js b/js/source/worker_tile.js index 9841dcebc62..2d209ea4c12 100644 --- a/js/source/worker_tile.js +++ b/js/source/worker_tile.js @@ -1,6 +1,6 @@ 'use strict'; -var FeatureTree = require('../data/feature_tree'); +var FeatureIndex = require('../data/feature_index'); var CollisionTile = require('../symbol/collision_tile'); var Bucket = require('../data/bucket'); var CollisionBoxArray = require('../symbol/collision_box'); @@ -27,7 +27,7 @@ WorkerTile.prototype.parse = function(data, layers, actor, rawTileData, callback 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 featureIndex = new FeatureIndex(this.coord, this.overscaling, collisionTile, data.layers); var sourceLayerCoder = new DictionaryCoder(data.layers ? Object.keys(data.layers).sort() : ['_geojsonTileLayer']); var stats = { _total: 0 }; @@ -105,13 +105,13 @@ WorkerTile.prototype.parse = function(data, layers, actor, rawTileData, callback symbolBuckets = this.symbolBuckets = [], otherBuckets = []; - featureTree.bucketLayerIDs = {}; + featureIndex.bucketLayerIDs = {}; for (var id in bucketsById) { bucket = bucketsById[id]; if (bucket.features.length === 0) continue; - featureTree.bucketLayerIDs[bucket.index] = bucket.layerIDs; + featureIndex.bucketLayerIDs[bucket.index] = bucket.layerIDs; buckets.push(bucket); @@ -183,7 +183,7 @@ WorkerTile.prototype.parse = function(data, layers, actor, rawTileData, callback if (bucket.type !== 'symbol') { for (var i = 0; i < bucket.features.length; i++) { var feature = bucket.features[i]; - featureTree.insert(feature, feature.index, bucket.sourceLayerIndex, bucket.index); + featureIndex.insert(feature, feature.index, bucket.sourceLayerIndex, bucket.index); } } @@ -201,17 +201,17 @@ WorkerTile.prototype.parse = function(data, layers, actor, rawTileData, callback tile.redoPlacementAfterDone = false; } - var featureTree_ = featureTree.serialize(); + var featureIndex_ = featureIndex.serialize(); var collisionTile_ = collisionTile.serialize(); var collisionBoxArray = tile.collisionBoxArray.serialize(); - var transferables = [rawTileData].concat(featureTree_.transferables).concat(collisionTile_.transferables); + var transferables = [rawTileData].concat(featureIndex_.transferables).concat(collisionTile_.transferables); var nonEmptyBuckets = buckets.filter(isBucketEmpty); callback(null, { buckets: nonEmptyBuckets.map(serializeBucket), bucketStats: stats, // TODO put this in a separate message? - featureTree: featureTree_.data, + featureIndex: featureIndex_.data, collisionTile: collisionTile_.data, collisionBoxArray: collisionBoxArray, rawTileData: rawTileData diff --git a/js/util/intersection_tests.js b/js/util/intersection_tests.js new file mode 100644 index 00000000000..e21d4bb7eea --- /dev/null +++ b/js/util/intersection_tests.js @@ -0,0 +1,164 @@ +'use strict'; + +module.exports = { + multiPolygonIntersectsBufferedMultiPoint: multiPolygonIntersectsBufferedMultiPoint, + multiPolygonIntersectsMultiPolygon: multiPolygonIntersectsMultiPolygon, + multiPolygonIntersectsBufferedMultiLine: multiPolygonIntersectsBufferedMultiLine +}; + +function multiPolygonIntersectsBufferedMultiPoint(multiPolygon, rings, radius) { + for (var j = 0; j < multiPolygon.length; j++) { + var polygon = multiPolygon[j]; + 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 (polygonContainsPoint(polygon, point)) return true; + if (pointIntersectsBufferedLine(point, polygon, radius)) return true; + } + } + } + return false; +} + +function multiPolygonIntersectsMultiPolygon(multiPolygonA, multiPolygonB) { + + if (multiPolygonA.length === 1 && multiPolygonA[0].length === 1) { + return multiPolygonContainsPoint(multiPolygonB, multiPolygonA[0][0]); + } + + for (var m = 0; m < multiPolygonB.length; m++) { + var ring = multiPolygonB[m]; + for (var n = 0; n < ring.length; n++) { + if (multiPolygonContainsPoint(multiPolygonA, ring[n])) return true; + } + } + + for (var j = 0; j < multiPolygonA.length; j++) { + var polygon = multiPolygonA[j]; + for (var i = 0; i < polygon.length; i++) { + if (multiPolygonContainsPoint(multiPolygonB, polygon[i])) return true; + } + + for (var k = 0; k < multiPolygonB.length; k++) { + if (lineIntersectsLine(polygon, multiPolygonB[k])) return true; + } + } + + return false; +} + +function multiPolygonIntersectsBufferedMultiLine(multiPolygon, multiLine, radius) { + for (var i = 0; i < multiLine.length; i++) { + var line = multiLine[i]; + + for (var j = 0; j < multiPolygon.length; j++) { + var polygon = multiPolygon[j]; + + 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; + } + } + return false; +} + +function lineIntersectsBufferedLine(lineA, lineB, radius) { + + 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; + } + } + + for (var k = 0; k < lineA.length; k++) { + if (pointIntersectsBufferedLine(lineA[k], lineB, radius)) return true; + } + + return false; +} + +function lineIntersectsLine(lineA, lineB) { + for (var i = 0; i < lineA.length - 1; i++) { + var a0 = lineA[i]; + var a1 = lineA[i + 1]; + for (var j = 0; j < lineB.length - 1; j++) { + var b0 = lineB[j]; + var b1 = lineB[j + 1]; + if (lineSegmentIntersectsLineSegment(a0, a1, b0, b1)) return true; + } + } + return false; +} + + +// http://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/ +function isCounterClockwise(a, b, c) { + return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x); +} + +function lineSegmentIntersectsLineSegment(a0, a1, b0, b1) { + return isCounterClockwise(a0, b0, b1) !== isCounterClockwise(a1, b0, b1) && + isCounterClockwise(a0, a1, b0) !== isCounterClockwise(a0, a1, b1); +} + +function pointIntersectsBufferedLine(p, line, radius) { + var radiusSquared = radius * radius; + + if (line.length === 1) return p.distSqr(line[0]) < radiusSquared; + + for (var i = 1; i < line.length; i++) { + // Find line segments that have a distance <= radius^2 to p + // In that case, we treat the line as "containing point p". + var v = line[i - 1], w = line[i]; + if (distToSegmentSquared(p, v, w) < radiusSquared) return true; + } + return false; +} + +// Code from http://stackoverflow.com/a/1501725/331379. +function distToSegmentSquared(p, v, w) { + var l2 = v.distSqr(w); + if (l2 === 0) return p.distSqr(v); + var t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2; + if (t < 0) return p.distSqr(v); + if (t > 1) return p.distSqr(w); + return p.distSqr(w.sub(v)._mult(t)._add(v)); +} + +// point in polygon ray casting algorithm +function multiPolygonContainsPoint(rings, p) { + var c = false, + ring, p1, p2; + + for (var k = 0; k < rings.length; k++) { + ring = rings[k]; + for (var i = 0, j = ring.length - 1; i < ring.length; j = i++) { + p1 = ring[i]; + 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; +} + +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; +}