diff --git a/src/ol/VectorTile.js b/src/ol/VectorTile.js index 9d18626e1e..7c79cbc36f 100644 --- a/src/ol/VectorTile.js +++ b/src/ol/VectorTile.js @@ -105,7 +105,9 @@ class VectorTile extends Tile { if (this.state == TileState.IDLE) { this.setState(TileState.LOADING); this.tileLoadFunction_(this, this.url_); - this.loader_(this.extent, this.resolution, this.projection); + if (this.loader_) { + this.loader_(this.extent, this.resolution, this.projection); + } } } diff --git a/src/ol/source/VectorTile.js b/src/ol/source/VectorTile.js index 8fb5496cce..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'; @@ -52,6 +52,17 @@ import {listen, unlistenByKey} from '../events.js'; * }); * } * ``` + * If you do not need extent, resolution and projection to get the features for a tile (e.g. + * for GeoJSON tiles), your `tileLoadFunction` does not need a `setLoader()` call. Only make sure + * to call `setFeatures()` on the tile: + * ```js + * const format = new GeoJSON({featureProjection: map.getView().getProjection()}); + * async function tileLoadFunction(tile, url) { + * const response = await fetch(url); + * const data = await response.json(); + * tile.setFeatures(format.readFeatures(data)); + * } + * ``` * @property {import("../Tile.js").UrlFunction} [tileUrlFunction] Optional function to get tile URL given a tile coordinate and the projection. * @property {string} [url] URL template. Must include `{x}`, `{y}` or `{-y}`, and `{z}` placeholders. * A `{?-?}` template pattern, for example `subdomain{a-f}.domain.com`, may be @@ -159,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. */ @@ -232,7 +288,7 @@ class VectorTile extends UrlTile { sourceTile.load(); } } - covered = false; + covered = covered && sourceTile && sourceTile.getState() === TileState.LOADED; if (!sourceTile) { return; } 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(); + }); + }); + }); + }); + });