UNPKG

mapbox-gl

Version:
333 lines (278 loc) 13.4 kB
// @flow import Point from '@mapbox/point-geometry'; import loadGeometry from './load_geometry.js'; import toEvaluationFeature from './evaluation_feature.js'; import EXTENT from '../style-spec/data/extent.js'; import featureFilter from '../style-spec/feature_filter/index.js'; import Grid from 'grid-index'; import DictionaryCoder from '../util/dictionary_coder.js'; import {VectorTile} from '@mapbox/vector-tile'; import Protobuf from 'pbf'; import GeoJSONFeature from '../util/vectortile_to_geojson.js'; import {arraysIntersect, mapObject, extend} from '../util/util.js'; import {OverscaledTileID} from '../source/tile_id.js'; import {register} from '../util/web_worker_transfer.js'; import EvaluationParameters from '../style/evaluation_parameters.js'; import SourceFeatureState from '../source/source_state.js'; import {polygonIntersectsBox} from '../util/intersection_tests.js'; import {PossiblyEvaluated} from '../style/properties.js'; import {FeatureIndexArray} from './array_types.js'; import {DEMSampler} from '../terrain/elevation.js'; import type StyleLayer from '../style/style_layer.js'; import type {QueryFeature} from '../util/vectortile_to_geojson.js'; import type {FeatureStates} from "../source/source_state"; import type {FeatureFilter} from '../style-spec/feature_filter/index.js'; import type Transform from '../geo/transform.js'; import type {FilterSpecification, PromoteIdSpecification} from '../style-spec/types.js'; import type {TilespaceQueryGeometry} from '../style/query_geometry.js'; import type {FeatureIndex as FeatureIndexStruct} from './array_types.js'; import type {TileTransform} from '../geo/projection/tile_transform.js'; import type {IVectorTileLayer, IVectorTileFeature} from '@mapbox/vector-tile'; type QueryParameters = { pixelPosMatrix: Float32Array, transform: Transform, tileResult: TilespaceQueryGeometry, tileTransform: TileTransform, params: { filter: FilterSpecification, layers: Array<string>, availableImages: Array<string> } } export type QueryResult = {[_: string]: Array<{ featureIndex: number, feature: QueryFeature }>}; type FeatureIndices = { bucketIndex: number, sourceLayerIndex: number, featureIndex: number, layoutVertexArrayOffset: number } | FeatureIndexStruct; class FeatureIndex { tileID: OverscaledTileID; x: number; y: number; z: number; grid: Grid; featureIndexArray: FeatureIndexArray; promoteId: ?PromoteIdSpecification; rawTileData: ArrayBuffer; bucketLayerIDs: Array<Array<string>>; vtLayers: {[_: string]: IVectorTileLayer}; vtFeatures: {[_: string]: IVectorTileFeature[]}; sourceLayerCoder: DictionaryCoder; constructor(tileID: OverscaledTileID, promoteId?: ?PromoteIdSpecification) { this.tileID = tileID; this.x = tileID.canonical.x; this.y = tileID.canonical.y; this.z = tileID.canonical.z; this.grid = new Grid(EXTENT, 16, 0); this.featureIndexArray = new FeatureIndexArray(); this.promoteId = promoteId; } insert(feature: IVectorTileFeature, geometry: Array<Array<Point>>, featureIndex: number, sourceLayerIndex: number, bucketIndex: number, layoutVertexArrayOffset: number = 0) { const key = this.featureIndexArray.length; this.featureIndexArray.emplaceBack(featureIndex, sourceLayerIndex, bucketIndex, layoutVertexArrayOffset); const grid = this.grid; for (let r = 0; r < geometry.length; r++) { const ring = geometry[r]; const bbox = [Infinity, Infinity, -Infinity, -Infinity]; for (let i = 0; i < ring.length; i++) { const 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); } if (bbox[0] < EXTENT && bbox[1] < EXTENT && bbox[2] >= 0 && bbox[3] >= 0) { grid.insert(key, bbox[0], bbox[1], bbox[2], bbox[3]); } } } loadVTLayers(): {[_: string]: IVectorTileLayer} { if (!this.vtLayers) { this.vtLayers = new VectorTile(new Protobuf(this.rawTileData)).layers; this.sourceLayerCoder = new DictionaryCoder(this.vtLayers ? Object.keys(this.vtLayers).sort() : ['_geojsonTileLayer']); this.vtFeatures = {}; for (const layer in this.vtLayers) { this.vtFeatures[layer] = []; } } return this.vtLayers; } // Finds non-symbol features in this tile at a particular position. query(args: QueryParameters, styleLayers: {[_: string]: StyleLayer}, serializedLayers: {[_: string]: Object}, sourceFeatureState: SourceFeatureState): QueryResult { this.loadVTLayers(); const params = args.params || {}, filter = featureFilter(params.filter); const tilespaceGeometry = args.tileResult; const transform = args.transform; const bounds = tilespaceGeometry.bufferedTilespaceBounds; const queryPredicate = (bx1: number, by1: number, bx2: number, by2: number) => { return polygonIntersectsBox(tilespaceGeometry.bufferedTilespaceGeometry, bx1, by1, bx2, by2); }; const matching = this.grid.query(bounds.min.x, bounds.min.y, bounds.max.x, bounds.max.y, queryPredicate); matching.sort(topDownFeatureComparator); let elevationHelper = null; if (transform.elevation && matching.length > 0) { elevationHelper = DEMSampler.create(transform.elevation, this.tileID); } const result = {}; let previousIndex; for (let k = 0; k < matching.length; k++) { const index = matching[k]; // don't check the same feature more than once if (index === previousIndex) continue; previousIndex = index; const match = this.featureIndexArray.get(index); let featureGeometry = null; this.loadMatchingFeature( result, match, filter, params.layers, params.availableImages, styleLayers, serializedLayers, sourceFeatureState, (feature: IVectorTileFeature, styleLayer: StyleLayer, featureState: Object, layoutVertexArrayOffset: number = 0) => { if (!featureGeometry) { featureGeometry = loadGeometry(feature, this.tileID.canonical, args.tileTransform); } return styleLayer.queryIntersectsFeature(tilespaceGeometry, feature, featureState, featureGeometry, this.z, args.transform, args.pixelPosMatrix, elevationHelper, layoutVertexArrayOffset); } ); } return result; } loadMatchingFeature( result: QueryResult, featureIndexData: FeatureIndices, filter: FeatureFilter, filterLayerIDs: Array<string>, availableImages: Array<string>, styleLayers: {[_: string]: StyleLayer}, serializedLayers: {[_: string]: Object}, sourceFeatureState?: SourceFeatureState, intersectionTest?: (feature: IVectorTileFeature, styleLayer: StyleLayer, featureState: Object, layoutVertexArrayOffset: number) => boolean | number) { const {featureIndex, bucketIndex, sourceLayerIndex, layoutVertexArrayOffset} = featureIndexData; const layerIDs = this.bucketLayerIDs[bucketIndex]; if (filterLayerIDs && !arraysIntersect(filterLayerIDs, layerIDs)) return; const sourceLayerName = this.sourceLayerCoder.decode(sourceLayerIndex); const sourceLayer = this.vtLayers[sourceLayerName]; const feature = sourceLayer.feature(featureIndex); if (filter.needGeometry) { const evaluationFeature = toEvaluationFeature(feature, true); // $FlowFixMe[method-unbinding] if (!filter.filter(new EvaluationParameters(this.tileID.overscaledZ), evaluationFeature, this.tileID.canonical)) { return; } // $FlowFixMe[method-unbinding] } else if (!filter.filter(new EvaluationParameters(this.tileID.overscaledZ), feature)) { return; } const id = this.getId(feature, sourceLayerName); for (let l = 0; l < layerIDs.length; l++) { const layerID = layerIDs[l]; if (filterLayerIDs && filterLayerIDs.indexOf(layerID) < 0) { continue; } const styleLayer = styleLayers[layerID]; if (!styleLayer) continue; let featureState = {}; if (id !== undefined && sourceFeatureState) { // `feature-state` expression evaluation requires feature state to be available featureState = sourceFeatureState.getState(styleLayer.sourceLayer || '_geojsonTileLayer', id); } const serializedLayer = extend({}, serializedLayers[layerID]); serializedLayer.paint = evaluateProperties(serializedLayer.paint, styleLayer.paint, feature, featureState, availableImages); serializedLayer.layout = evaluateProperties(serializedLayer.layout, styleLayer.layout, feature, featureState, availableImages); const intersectionZ = !intersectionTest || intersectionTest(feature, styleLayer, featureState, layoutVertexArrayOffset); if (!intersectionZ) { // Only applied for non-symbol features continue; } const geojsonFeature = new GeoJSONFeature(feature, this.z, this.x, this.y, id); geojsonFeature.layer = serializedLayer; let layerResult = result[layerID]; if (layerResult === undefined) { layerResult = result[layerID] = []; } layerResult.push({featureIndex, feature: geojsonFeature, intersectionZ}); } } // Given a set of symbol indexes that have already been looked up, // return a matching set of GeoJSONFeatures lookupSymbolFeatures(symbolFeatureIndexes: Array<number>, serializedLayers: {[string]: StyleLayer}, bucketIndex: number, sourceLayerIndex: number, filterSpec: FilterSpecification, filterLayerIDs: Array<string>, availableImages: Array<string>, styleLayers: {[_: string]: StyleLayer}): QueryResult { const result = {}; this.loadVTLayers(); const filter = featureFilter(filterSpec); for (const symbolFeatureIndex of symbolFeatureIndexes) { this.loadMatchingFeature( result, { bucketIndex, sourceLayerIndex, featureIndex: symbolFeatureIndex, layoutVertexArrayOffset: 0 }, filter, filterLayerIDs, availableImages, styleLayers, serializedLayers ); } return result; } loadFeature(featureIndexData: FeatureIndices): IVectorTileFeature { const {featureIndex, sourceLayerIndex} = featureIndexData; this.loadVTLayers(); const sourceLayerName = this.sourceLayerCoder.decode(sourceLayerIndex); const featureCache = this.vtFeatures[sourceLayerName]; if (featureCache[featureIndex]) { return featureCache[featureIndex]; } const sourceLayer = this.vtLayers[sourceLayerName]; const feature = sourceLayer.feature(featureIndex); featureCache[featureIndex] = feature; return feature; } hasLayer(id: string): boolean { for (const layerIDs of this.bucketLayerIDs) { for (const layerID of layerIDs) { if (id === layerID) return true; } } return false; } getId(feature: IVectorTileFeature, sourceLayerId: string): string | number | void { let id = feature.id; if (this.promoteId) { const propName = typeof this.promoteId === 'string' ? this.promoteId : this.promoteId[sourceLayerId]; // $FlowFixMe[incompatible-type] - Flow can't narrow the id type from IVectorTileFeature.id if (propName != null) id = feature.properties[propName]; if (typeof id === 'boolean') id = Number(id); } return id; } } register(FeatureIndex, 'FeatureIndex', {omit: ['rawTileData', 'sourceLayerCoder']}); export default FeatureIndex; function evaluateProperties(serializedProperties: mixed, styleLayerProperties: mixed, feature: IVectorTileFeature, featureState: FeatureStates, availableImages: Array<string>) { return mapObject(serializedProperties, (property, key) => { const prop = styleLayerProperties instanceof PossiblyEvaluated ? styleLayerProperties.get(key) : null; return prop && prop.evaluate ? prop.evaluate(feature, featureState, availableImages) : prop; }); } function topDownFeatureComparator(a: number, b: number) { return b - a; }