diff --git a/examples/vector-tile-selection.js b/examples/vector-tile-selection.js index 56c43da2db..2e541e965d 100644 --- a/examples/vector-tile-selection.js +++ b/examples/vector-tile-selection.js @@ -45,26 +45,27 @@ const map = new Map({ const selectElement = document.getElementById('type'); map.on('click', function(event) { - const features = map.getFeaturesAtPixel(event.pixel); - if (!features) { - selection = {}; + vtLayer.getFeatures(event.pixel).then(function(features) { + if (!features.length) { + selection = {}; + // force redraw of layer style + vtLayer.setStyle(vtLayer.getStyle()); + return; + } + const feature = features[0]; + if (!feature) { + return; + } + const fid = feature.get(idProp); + + if (selectElement.value === 'singleselect') { + selection = {}; + } + // add selected feature to lookup + selection[fid] = feature; + // force redraw of layer style vtLayer.setStyle(vtLayer.getStyle()); - return; - } - const feature = features[0]; - if (!feature) { - return; - } + }); - const fid = feature.get(idProp); - - if (selectElement.value === 'singleselect') { - selection = {}; - } - // add selected feature to lookup - selection[fid] = feature; - - // force redraw of layer style - vtLayer.setStyle(vtLayer.getStyle()); }); diff --git a/src/ol/Tile.js b/src/ol/Tile.js index e0a393854c..29caf105b7 100644 --- a/src/ol/Tile.js +++ b/src/ol/Tile.js @@ -87,6 +87,11 @@ class Tile extends EventTarget { const options = opt_options ? opt_options : {}; + /** + * @type {ImageData} + */ + this.hitDetectionImageData = null; + /** * @type {import("./tilecoord.js").TileCoord} */ diff --git a/src/ol/layer/VectorTile.js b/src/ol/layer/VectorTile.js index 644ac16c48..8ede1b7c81 100644 --- a/src/ol/layer/VectorTile.js +++ b/src/ol/layer/VectorTile.js @@ -116,6 +116,24 @@ class VectorTileLayer extends BaseVectorLayer { return new CanvasVectorTileLayerRenderer(this); } + /** + * Get the topmost feature that intersects the given pixel on the viewport. Returns a promise + * that resolves with an array of features. The array will either contain the topmost feature + * when a hit was detected, or it will be empty. + * + * The hit detection algorithm used for this method is optimized for performance, but is less + * accurate than the one used in {@link import("../PluggableMap.js").default#getFeaturesAtPixel}: Text + * is not considered, and icons are only represented by their bounding box instead of the exact + * image. + * + * @param {import("../pixel.js").Pixel} pixel Pixel. + * @return {Promise>} Promise that resolves with an array of features. + * @api + */ + getFeatures(pixel) { + return super.getFeatures(pixel); + } + /** * @return {VectorTileRenderType} The render mode. */ diff --git a/src/ol/render/canvas/Immediate.js b/src/ol/render/canvas/Immediate.js index 03dfbf61bb..206202d018 100644 --- a/src/ol/render/canvas/Immediate.js +++ b/src/ol/render/canvas/Immediate.js @@ -980,8 +980,8 @@ export default CanvasImmediateRenderer; * @param {Array} transforms Transforms * for rendering features to all worlds of the viewport, from coordinates to css * pixels. - * @param {Array} features Features to consider. - * for hit detection. + * @param {Array} features + * Features to consider for hit detection. * @param {import("../../style/Style.js").StyleFunction|undefined} styleFunction * Layer style function. * @param {import("../../extent.js").Extent} extent Extent. @@ -1085,11 +1085,11 @@ export function createHitDetectionImageData(size, transforms, features, styleFun /** * @param {import("../../pixel").Pixel} pixel Pixel coordinate on the hit * detection canvas in css pixels. - * @param {Array} features Features. Has to + * @param {Array} features Features. Has to * match the `features` array that was passed to `createHitDetectionImageData()`. * @param {ImageData} imageData Hit detection image data generated by * `createHitDetectionImageData()`. - * @return {Array} features Features. + * @return {Array} features Features. */ export function hitDetect(pixel, features, imageData) { const resultFeatures = []; diff --git a/src/ol/renderer/canvas/TileLayer.js b/src/ol/renderer/canvas/TileLayer.js index 6eba0a2096..cdf1683b21 100644 --- a/src/ol/renderer/canvas/TileLayer.js +++ b/src/ol/renderer/canvas/TileLayer.js @@ -35,6 +35,18 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { */ this.renderedExtent_ = null; + /** + * @protected + * @type {number} + */ + this.renderedPixelRatio; + + /** + * @protected + * @type {import("../../proj/Projection.js").default} + */ + this.renderedProjection = null; + /** * @protected * @type {number} @@ -342,6 +354,8 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { this.renderedResolution = tileResolution; this.extentChanged = !this.renderedExtent_ || !equals(this.renderedExtent_, canvasExtent); this.renderedExtent_ = canvasExtent; + this.renderedPixelRatio = pixelRatio; + this.renderedProjection = projection; this.manageTilePyramid(frameState, tileSource, tileGrid, pixelRatio, projection, extent, z, tileLayer.getPreload()); diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index ea643ff69a..3c007896f7 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -7,11 +7,12 @@ import TileState from '../../TileState.js'; import ViewHint from '../../ViewHint.js'; import {listen, unlistenByKey} from '../../events.js'; import EventType from '../../events/EventType.js'; -import {buffer, containsCoordinate, equals, getIntersection, intersects} from '../../extent.js'; +import {buffer, containsCoordinate, equals, getIntersection, intersects, containsExtent, getWidth, getTopLeft} from '../../extent.js'; import VectorTileRenderType from '../../layer/VectorTileRenderType.js'; import ReplayType from '../../render/canvas/BuilderType.js'; import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; import CanvasTileLayerRenderer from './TileLayer.js'; +import {toSize} from '../../size.js'; import {getSquaredTolerance as getSquaredRenderTolerance, renderFeature} from '../vector.js'; import { apply as applyTransform, @@ -25,6 +26,7 @@ import { } from '../../transform.js'; import CanvasExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js'; import {clear} from '../../obj.js'; +import {createHitDetectionImageData, hitDetect} from '../../render/canvas/Immediate.js'; /** @@ -99,6 +101,18 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { */ this.renderedLayerRevision_; + /** + * @private + * @type {import("../../transform").Transform} + */ + this.renderedPixelToCoordinateTransform_ = null; + + /** + * @private + * @type {number} + */ + this.renderedRotation_; + /** * @private * @type {!Object} @@ -303,6 +317,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { if (renderOrder && renderOrder !== builderState.renderedRenderOrder) { features.sort(renderOrder); } + sourceTile.hitDetectionImageData = null; for (let i = 0, ii = features.length; i < ii; ++i) { const feature = features[i]; if (!bufferedExtent || intersects(bufferedExtent, feature.getGeometry().getExtent())) { @@ -379,6 +394,69 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { return found; } + /** + * @inheritDoc + */ + getFeatures(pixel) { + return new Promise(function(resolve, reject) { + const layer = /** @type {import("../../layer/VectorTile.js").default} */ (this.getLayer()); + const source = layer.getSource(); + const projection = this.renderedProjection; + const projectionExtent = projection.getExtent(); + const resolution = this.renderedResolution; + const pixelRatio = this.renderedPixelRatio; + const tileGrid = source.getTileGridForProjection(projection); + const sourceTileGrid = source.getTileGrid(); + const coordinate = applyTransform(this.renderedPixelToCoordinateTransform_, pixel.slice()); + const tileCoord = tileGrid.getTileCoordForCoordAndResolution(coordinate, resolution); + let sourceTile; + for (let i = 0, ii = this.renderedTiles.length; i < ii; ++i) { + if (tileCoord.toString() === this.renderedTiles[i].tileCoord.toString()) { + const tile = this.renderedTiles[i]; + if (tile.getState() === TileState.LOADED && tile.hifi) { + const extent = tileGrid.getTileCoordExtent(tileCoord); + if (source.getWrapX() && projection.canWrapX() && !containsExtent(projectionExtent, extent)) { + const worldWidth = getWidth(projectionExtent); + const worldsAway = Math.floor((coordinate[0] - projectionExtent[0]) / worldWidth); + coordinate[0] -= (worldsAway * worldWidth); + } + const sourceTiles = source.getSourceTiles(pixelRatio, projection, tile); + const sourceTileCoord = sourceTileGrid.getTileCoordForCoordAndResolution(coordinate, resolution); + for (let j = 0, jj = sourceTiles.length; j < jj; ++j) { + if (sourceTileCoord.toString() === sourceTiles[j].tileCoord.toString()) { + sourceTile = sourceTiles[j]; + break; + } + } + } + break; + } + } + const corner = getTopLeft(tileGrid.getTileCoordExtent(sourceTile.tileCoord)); + const tilePixel = [ + (coordinate[0] - corner[0]) / resolution, + (corner[1] - coordinate[1]) / resolution + ]; + if (!sourceTile.hitDetectionImageData) { + const tileSize = toSize(sourceTileGrid.getTileSize(sourceTileGrid.getZForResolution(resolution))); + const size = [tileSize[0] / 2, tileSize[1] / 2]; + const rotation = this.renderedRotation_; + const transforms = [ + this.getRenderTransform(tileGrid.getTileCoordCenter(sourceTile.tileCoord), + resolution, 0, 0.5, size[0], size[1], 0) + ]; + requestAnimationFrame(function() { + sourceTile.hitDetectionImageData = createHitDetectionImageData(tileSize, transforms, + sourceTile.getFeatures(), layer.getStyleFunction(), + tileGrid.getTileCoordExtent(sourceTile.tileCoord), resolution, rotation); + resolve(hitDetect(tilePixel, sourceTile.getFeatures(), sourceTile.hitDetectionImageData)); + }); + } else { + resolve(hitDetect(tilePixel, sourceTile.getFeatures(), sourceTile.hitDetectionImageData)); + } + }.bind(this)); + } + /** * @inheritDoc */ @@ -408,6 +486,9 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { this.renderQueuedTileImages_(hifi, frameState); super.renderFrame(frameState, target); + this.renderedPixelToCoordinateTransform_ = frameState.pixelToCoordinateTransform.slice(); + this.renderedRotation_ = frameState.viewState.rotation; + const layer = /** @type {import("../../layer/VectorTile.js").default} */ (this.getLayer()); const renderMode = layer.getRenderMode();