diff --git a/src/ol/source/VectorTile.js b/src/ol/source/VectorTile.js index bda808a53c..c82d5951e4 100644 --- a/src/ol/source/VectorTile.js +++ b/src/ol/source/VectorTile.js @@ -7,7 +7,7 @@ import VectorRenderTile from '../VectorRenderTile.js'; import Tile from '../VectorTile.js'; import {toSize} from '../size.js'; import UrlTile from './UrlTile.js'; -import {getKeyZXY} from '../tilecoord.js'; +import {getKeyZXY, fromKey} from '../tilecoord.js'; import {createXYZ, extentFromProjection, createForProjection} from '../tilegrid.js'; import {buffer as bufferExtent, getIntersection, intersects} from '../extent.js'; import EventType from '../events/EventType.js'; @@ -170,6 +170,51 @@ class VectorTile extends UrlTile { } + /** + * Get features whose bounding box intersects the provided extent. Only features for cached + * tiles for the last rendered zoom level are available in the source. So this method is only + * suitable for requesting tiles for extents that are currently rendered. + * + * Features are returned in random tile order and as they are included in the tiles. This means + * they can be clipped, duplicated across tiles, and simplified to the render resolution. + * + * @param {import("../extent.js").Extent} extent Extent. + * @return {Array} Features. + * @api + */ + getFeaturesInExtent(extent) { + const features = []; + const tileCache = this.tileCache; + if (tileCache.getCount() === 0) { + return features; + } + const z = fromKey(tileCache.peekFirstKey())[0]; + const tileGrid = this.tileGrid; + tileCache.forEach(function(tile) { + if (tile.tileCoord[0] !== z || tile.getState() !== TileState.LOADED) { + return; + } + const sourceTiles = tile.getSourceTiles(); + for (let i = 0, ii = sourceTiles.length; i < ii; ++i) { + const sourceTile = sourceTiles[i]; + const tileCoord = sourceTile.tileCoord; + if (intersects(extent, tileGrid.getTileCoordExtent(tileCoord))) { + const tileFeatures = sourceTile.getFeatures(); + if (tileFeatures) { + for (let j = 0, jj = tileFeatures.length; j < jj; ++j) { + const candidate = tileFeatures[j]; + const geometry = candidate.getGeometry(); + if (geometry.intersectsExtent(extent)) { + features.push(candidate); + } + } + } + } + } + }); + return features; + } + /** * @return {boolean} The source can have overlapping geometries. */ diff --git a/test/spec/ol/source/vectortile.test.js b/test/spec/ol/source/vectortile.test.js index f5e6883baf..e7eff460e4 100644 --- a/test/spec/ol/source/vectortile.test.js +++ b/test/spec/ol/source/vectortile.test.js @@ -13,6 +13,8 @@ import {listen, unlistenByKey} from '../../../../src/ol/events.js'; import TileState from '../../../../src/ol/TileState.js'; import {getCenter} from '../../../../src/ol/extent.js'; import {unByKey} from '../../../../src/ol/Observable.js'; +import Feature from '../../../../src/ol/Feature.js'; +import {fromExtent} from '../../../../src/ol/geom/Polygon.js'; describe('ol.source.VectorTile', function() { @@ -354,4 +356,68 @@ describe('ol.source.VectorTile', function() { }); + describe('getFeatuersInExtent', function() { + + let map, source, target; + + beforeEach(function() { + source = new VectorTileSource({ + maxZoom: 15, + tileSize: 256, + url: '{z}/{x}/{y}', + tileLoadFunction: function(tile) { + const extent = source.getTileGrid().getTileCoordExtent(tile.tileCoord); + const feature = new Feature(fromExtent(extent)); + feature.set('z', tile.tileCoord[0]); + tile.setFeatures([feature]); + } + }); + target = document.createElement('div'); + target.style.width = '100px'; + target.style.height = '100px'; + document.body.appendChild(target); + map = new Map({ + target: target, + layers: [ + new VectorTileLayer({ + source: source + }) + ], + view: new View({ + center: [0, 0], + zoom: 0 + }) + }); + }); + + afterEach(function() { + map.setTarget(null); + document.body.removeChild(target); + }); + + it('returns an empty array when no tiles are in the cache', function() { + source.tileCache.clear(); + const extent = map.getView().calculateExtent(map.getSize()); + expect(source.getFeaturesInExtent(extent).length).to.be(0); + }); + + it('returns features in extent for the last rendered z', function(done) { + map.getView().setZoom(15); + map.once('rendercomplete', function() { + const extent = map.getView().calculateExtent(map.getSize()); + const features = source.getFeaturesInExtent(extent); + expect(features.length).to.be(4); + expect(features[0].get('z')).to.be(15); + map.getView().setZoom(0); + map.once('rendercomplete', function() { + const extent = map.getView().calculateExtent(map.getSize()); + const features = source.getFeaturesInExtent(extent); + expect(features.length).to.be(1); + expect(features[0].get('z')).to.be(0); + done(); + }); + }); + }); + }); + });