mapbox-gl
Version:
A WebGL interactive maps library
298 lines (248 loc) • 11.3 kB
JavaScript
'use strict';
var Point = require('point-geometry');
var loadGeometry = require('./load_geometry');
var EXTENT = require('./bucket').EXTENT;
var featureFilter = require('feature-filter');
var StructArrayType = require('../util/struct_array');
var Grid = require('grid-index');
var DictionaryCoder = require('../util/dictionary_coder');
var vt = require('vector-tile');
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
{ 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 = FeatureIndex;
function FeatureIndex(coord, overscaling, collisionTile) {
if (coord.grid) {
var serialized = coord;
var rawTileData = overscaling;
coord = serialized.coord;
overscaling = serialized.overscaling;
this.grid = new Grid(serialized.grid);
this.featureIndexArray = new FeatureIndexArray(serialized.featureIndexArray);
this.rawTileData = rawTileData;
this.bucketLayerIDs = serialized.bucketLayerIDs;
} else {
this.grid = new Grid(EXTENT, 16, 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.setCollisionTile(collisionTile);
}
FeatureIndex.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];
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]);
}
};
FeatureIndex.prototype.setCollisionTile = function(collisionTile) {
this.collisionTile = collisionTile;
};
FeatureIndex.prototype.serialize = function() {
var data = {
coord: this.coord,
overscaling: this.overscaling,
grid: this.grid.toArrayBuffer(),
featureIndexArray: this.featureIndexArray.serialize(),
bucketLayerIDs: this.bucketLayerIDs
};
return {
data: data,
transferables: [data.grid, data.featureIndexArray.arrayBuffer]
};
};
function translateDistance(translate) {
return Math.sqrt(translate[0] * translate[0] + translate[1] * translate[1]);
}
// Finds features in this tile at a particular position.
FeatureIndex.prototype.query = function(args, styleLayers) {
if (!this.vtLayers) {
this.vtLayers = new vt.VectorTile(new Protobuf(this.rawTileData)).layers;
this.sourceLayerCoder = new DictionaryCoder(this.vtLayers ? Object.keys(this.vtLayers).sort() : ['_geojsonTileLayer']);
}
var result = {};
var params = args.params || {},
pixelsToTileUnits = EXTENT / args.tileSize / args.scale,
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
// expanded by to include these features.
var additionalRadius = 0;
for (var id in styleLayers) {
var styleLayer = styleLayers[id];
var paint = styleLayer.paint;
var styleLayerDistance = 0;
if (styleLayer.type === 'line') {
styleLayerDistance = getLineWidth(paint) / 2 + Math.abs(paint['line-offset']) + translateDistance(paint['line-translate']);
} else if (styleLayer.type === 'fill') {
styleLayerDistance = translateDistance(paint['fill-translate']);
} else if (styleLayer.type === 'circle') {
styleLayerDistance = paint['circle-radius'] + translateDistance(paint['circle-translate']);
}
additionalRadius = Math.max(additionalRadius, styleLayerDistance * pixelsToTileUnits);
}
var queryGeometry = args.queryGeometry.map(function(q) {
return q.map(function(p) {
return new Point(p.x, p.y);
});
});
var minX = Infinity;
var minY = Infinity;
var maxX = -Infinity;
var maxY = -Infinity;
for (var i = 0; i < queryGeometry.length; i++) {
var ring = queryGeometry[i];
for (var k = 0; k < ring.length; k++) {
var p = ring[k];
minX = Math.min(minX, p.x);
minY = Math.min(minY, p.y);
maxX = Math.max(maxX, p.x);
maxY = Math.max(maxY, p.y);
}
}
var matching = this.grid.query(minX - additionalRadius, minY - additionalRadius, maxX + additionalRadius, maxY + additionalRadius);
matching.sort(topDownFeatureComparator);
this.filterMatching(result, matching, this.featureIndexArray, queryGeometry, filter, params.layers, styleLayers, args.bearing, pixelsToTileUnits);
var matchingSymbols = this.collisionTile.queryRenderedSymbols(minX, minY, maxX, maxY, args.scale);
matchingSymbols.sort();
this.filterMatching(result, matchingSymbols, this.collisionTile.collisionBoxArray, queryGeometry, filter, params.layers, styleLayers, args.bearing, pixelsToTileUnits);
return result;
};
function topDownFeatureComparator(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'];
}
}
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];
// don't check the same feature more than once
if (index === previousIndex) continue;
previousIndex = index;
var match = array.get(index);
var layerIDs = this.bucketLayerIDs[match.bucketIndex];
if (filterLayerIDs && !arraysIntersect(filterLayerIDs, layerIDs)) continue;
var sourceLayerName = this.sourceLayerCoder.decode(match.sourceLayerIndex);
var sourceLayer = this.vtLayers[sourceLayerName];
var feature = sourceLayer.feature(match.featureIndex);
if (!filter(feature)) continue;
var geometry = null;
for (var l = 0; l < layerIDs.length; l++) {
var layerID = layerIDs[l];
if (filterLayerIDs && filterLayerIDs.indexOf(layerID) < 0) {
continue;
}
var styleLayer = styleLayers[layerID];
if (!styleLayer) continue;
var translatedPolygon;
if (styleLayer.type !== 'symbol') {
// all symbols already match the style
if (!geometry) geometry = loadGeometry(feature);
var paint = styleLayer.paint;
if (styleLayer.type === 'line') {
translatedPolygon = translate(queryGeometry,
paint['line-translate'], paint['line-translate-anchor'],
bearing, pixelsToTileUnits);
var halfWidth = getLineWidth(paint) / 2 * pixelsToTileUnits;
if (paint['line-offset']) {
geometry = offsetLine(geometry, paint['line-offset'] * pixelsToTileUnits);
}
if (!multiPolygonIntersectsBufferedMultiLine(translatedPolygon, geometry, halfWidth)) continue;
} else if (styleLayer.type === 'fill') {
translatedPolygon = translate(queryGeometry,
paint['fill-translate'], paint['fill-translate-anchor'],
bearing, pixelsToTileUnits);
if (!multiPolygonIntersectsMultiPolygon(translatedPolygon, geometry)) continue;
} else if (styleLayer.type === 'circle') {
translatedPolygon = translate(queryGeometry,
paint['circle-translate'], paint['circle-translate-anchor'],
bearing, pixelsToTileUnits);
var circleRadius = paint['circle-radius'] * pixelsToTileUnits;
if (!multiPolygonIntersectsBufferedMultiPoint(translatedPolygon, geometry, circleRadius)) continue;
}
}
var geojsonFeature = new GeoJSONFeature(feature, this.z, this.x, this.y);
geojsonFeature.layer = styleLayer.serialize({
includeRefProperties: true
});
var layerResult = result[layerID];
if (layerResult === undefined) {
layerResult = result[layerID] = [];
}
layerResult.push(geojsonFeature);
}
}
};
function translate(queryGeometry, translate, translateAnchor, bearing, pixelsToTileUnits) {
if (!translate[0] && !translate[1]) {
return queryGeometry;
}
translate = Point.convert(translate);
if (translateAnchor === "viewport") {
translate._rotate(-bearing);
}
var translated = [];
for (var i = 0; i < queryGeometry.length; i++) {
var ring = queryGeometry[i];
var translatedRing = [];
for (var k = 0; k < ring.length; k++) {
translatedRing.push(ring[k].sub(translate._mult(pixelsToTileUnits)));
}
translated.push(translatedRing);
}
return translated;
}
function offsetLine(rings, offset) {
var newRings = [];
var zero = new Point(0, 0);
for (var k = 0; k < rings.length; k++) {
var ring = rings[k];
var newRing = [];
for (var i = 0; i < ring.length; i++) {
var a = ring[i - 1];
var b = ring[i];
var c = ring[i + 1];
var aToB = i === 0 ? zero : b.sub(a)._unit()._perp();
var bToC = i === ring.length - 1 ? zero : c.sub(b)._unit()._perp();
var extrude = aToB._add(bToC)._unit();
var cosHalfAngle = extrude.x * bToC.x + extrude.y * bToC.y;
extrude._mult(1 / cosHalfAngle);
newRing.push(extrude._mult(offset)._add(b));
}
newRings.push(newRing);
}
return newRings;
}