diff --git a/examples/hitdetect-vector.html b/examples/hitdetect-vector.html new file mode 100644 index 0000000000..8186f940f6 --- /dev/null +++ b/examples/hitdetect-vector.html @@ -0,0 +1,15 @@ +--- +layout: example.html +title: Vector Layer Hit Detection +shortdesc: Example of hit detection on a countries vector layer with country information. +docs: > + The countries are loaded from a GeoJSON file. Information about countries is + on hover and click is retrieved using the layer's `getFeatures()` method. For + vector layers, this function resolves with an array of only the topmost + feature. It uses a very efficient hit detection algorithm, at the cost of + accuracy. For pixel exact hit detection, when performance is not a concern, + use the map's `getFeaturesAtPixel()` or `forEachFeatureAtPixel()` methods. +tags: "vector, geojson, click, hover, hit detection" +--- +
+
 
diff --git a/examples/hitdetect-vector.js b/examples/hitdetect-vector.js new file mode 100644 index 0000000000..d09cc96c4e --- /dev/null +++ b/examples/hitdetect-vector.js @@ -0,0 +1,113 @@ +import Map from '../src/ol/Map.js'; +import View from '../src/ol/View.js'; +import GeoJSON from '../src/ol/format/GeoJSON.js'; +import VectorLayer from '../src/ol/layer/Vector.js'; +import VectorSource from '../src/ol/source/Vector.js'; +import {Fill, Stroke, Style, Text} from '../src/ol/style.js'; + + +const style = new Style({ + fill: new Fill({ + color: 'rgba(255, 255, 255, 0.6)' + }), + stroke: new Stroke({ + color: '#319FD3', + width: 1 + }), + text: new Text({ + font: '12px Calibri,sans-serif', + fill: new Fill({ + color: '#000' + }), + stroke: new Stroke({ + color: '#fff', + width: 3 + }) + }) +}); + +const vectorLayer = new VectorLayer({ + source: new VectorSource({ + url: 'data/geojson/countries.geojson', + format: new GeoJSON() + }), + style: function(feature) { + style.getText().setText(feature.get('name')); + return style; + } +}); + +const map = new Map({ + layers: [vectorLayer], + target: 'map', + view: new View({ + center: [0, 0], + zoom: 1 + }) +}); + +const highlightStyle = new Style({ + stroke: new Stroke({ + color: '#f00', + width: 1 + }), + fill: new Fill({ + color: 'rgba(255,0,0,0.1)' + }), + text: new Text({ + font: '12px Calibri,sans-serif', + fill: new Fill({ + color: '#000' + }), + stroke: new Stroke({ + color: '#f00', + width: 3 + }) + }) +}); + +const featureOverlay = new VectorLayer({ + source: new VectorSource(), + map: map, + style: function(feature) { + highlightStyle.getText().setText(feature.get('name')); + return highlightStyle; + } +}); + +let highlight; +const displayFeatureInfo = function(pixel) { + + vectorLayer.getFeatures(pixel).then(function(features) { + const feature = features.length ? features[0] : undefined; + const info = document.getElementById('info'); + if (features.length) { + info.innerHTML = feature.getId() + ': ' + feature.get('name'); + } else { + info.innerHTML = ' '; + } + + if (feature !== highlight) { + if (highlight) { + featureOverlay.getSource().removeFeature(highlight); + } + if (feature) { + featureOverlay.getSource().addFeature(feature); + } + highlight = feature; + } + }); + +}; + +map.on('pointermove', function(evt) { + if (evt.dragging) { + return; + } + const pixel = map.getEventPixel(evt.originalEvent); + displayFeatureInfo(pixel); +}); + +map.on('click', function(evt) { + displayFeatureInfo(evt.pixel); +}); diff --git a/src/ol/layer/BaseVector.js b/src/ol/layer/BaseVector.js index 8c2024ffd6..7201c448b1 100644 --- a/src/ol/layer/BaseVector.js +++ b/src/ol/layer/BaseVector.js @@ -135,6 +135,24 @@ class BaseVectorLayer extends Layer { return this.declutter_; } + /** + * 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 {number|undefined} Render buffer. */ diff --git a/src/ol/layer/Layer.js b/src/ol/layer/Layer.js index 1e0491608c..d7e54859fa 100644 --- a/src/ol/layer/Layer.js +++ b/src/ol/layer/Layer.js @@ -192,6 +192,15 @@ class Layer extends BaseLayer { this.changed(); } + /** + * @param {import("../pixel").Pixel} pixel Pixel. + * @return {Promise>} Promise that resolves with + * an array of features. + */ + getFeatures(pixel) { + return this.renderer_.getFeatures(pixel); + } + /** * In charge to manage the rendering of the layer. One layer type is * bounded with one layer renderer. diff --git a/src/ol/render/canvas/Immediate.js b/src/ol/render/canvas/Immediate.js index 2f0894b4f1..78d7b12368 100644 --- a/src/ol/render/canvas/Immediate.js +++ b/src/ol/render/canvas/Immediate.js @@ -438,6 +438,13 @@ class CanvasImmediateRenderer extends VectorContext { this.setTextStyle(style.getText()); } + /** + * @param {import("../../transform.js").Transform} transform Transform. + */ + setTransform(transform) { + this.transform_ = transform; + } + /** * Render a geometry into the canvas. Call * {@link module:ol/render/canvas/Immediate#setStyle} first to set the rendering style. diff --git a/src/ol/renderer/Layer.js b/src/ol/renderer/Layer.js index d5860dcaa6..072e043906 100644 --- a/src/ol/renderer/Layer.js +++ b/src/ol/renderer/Layer.js @@ -30,6 +30,16 @@ class LayerRenderer extends Observable { } + /** + * Asynchronous layer level hit detection. + * @param {import("../pixel.js").Pixel} pixel Pixel. + * @return {Promise>} Promise that resolves with + * an array of features. + */ + getFeatures(pixel) { + return abstract(); + } + /** * Determine whether render should be called. * @abstract diff --git a/src/ol/renderer/canvas/Layer.js b/src/ol/renderer/canvas/Layer.js index 39d8ce195e..34ff9fbd62 100644 --- a/src/ol/renderer/canvas/Layer.js +++ b/src/ol/renderer/canvas/Layer.js @@ -213,23 +213,24 @@ class CanvasLayerRenderer extends LayerRenderer { /** * Creates a transform for rendering to an element that will be rotated after rendering. - * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. + * @param {import("../../coordinate.js").Coordinate} center Center. + * @param {number} resolution Resolution. + * @param {number} rotation Rotation. + * @param {number} pixelRatio Pixel ratio. * @param {number} width Width of the rendered element (in pixels). * @param {number} height Height of the rendered element (in pixels). * @param {number} offsetX Offset on the x-axis in view coordinates. * @protected * @return {!import("../../transform.js").Transform} Transform. */ - getRenderTransform(frameState, width, height, offsetX) { - const viewState = frameState.viewState; - const pixelRatio = frameState.pixelRatio; + getRenderTransform(center, resolution, rotation, pixelRatio, width, height, offsetX) { const dx1 = width / 2; const dy1 = height / 2; - const sx = pixelRatio / viewState.resolution; + const sx = pixelRatio / resolution; const sy = -sx; - const dx2 = -viewState.center[0] + offsetX; - const dy2 = -viewState.center[1]; - return composeTransform(this.tempTransform_, dx1, dy1, sx, sy, -viewState.rotation, dx2, dy2); + const dx2 = -center[0] + offsetX; + const dy2 = -center[1]; + return composeTransform(this.tempTransform_, dx1, dy1, sx, sy, -rotation, dx2, dy2); } /** diff --git a/src/ol/renderer/canvas/VectorLayer.js b/src/ol/renderer/canvas/VectorLayer.js index 4cb11e29da..ae8d756a00 100644 --- a/src/ol/renderer/canvas/VectorLayer.js +++ b/src/ol/renderer/canvas/VectorLayer.js @@ -9,7 +9,13 @@ import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; import ExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js'; import CanvasLayerRenderer from './Layer.js'; import {defaultOrder as defaultRenderOrder, getTolerance as getRenderTolerance, getSquaredTolerance as getSquaredRenderTolerance, renderFeature} from '../vector.js'; -import {toString as transformToString, makeScale, makeInverse} from '../../transform.js'; +import {toString as transformToString, makeScale, makeInverse, apply} from '../../transform.js'; +import {createCanvasContext2D} from '../../dom.js'; +import CanvasImmediateRenderer from '../../render/canvas/Immediate.js'; +import {Icon} from '../../style.js'; +import IconAnchorUnits from '../../style/IconAnchorUnits.js'; +import GeometryType from '../../geom/GeometryType.js'; +import {numberSafeCompareFunction} from '../../array.js'; /** * @classdesc @@ -28,6 +34,10 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { /** @private */ this.boundHandleStyleImageChange_ = this.handleStyleImageChange_.bind(this); + /** + * @type {boolean} + */ + this.animatingOrInteracting_; /** * @private @@ -35,6 +45,16 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { */ this.dirty_ = false; + /** + * @type {ImageData} + */ + this.hitDetectionImageData_ = null; + + /** + * @type {Array} + */ + this.renderedFeatures_ = null; + /** * @private * @type {number} @@ -53,6 +73,24 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { */ this.renderedExtent_ = createEmpty(); + /** + * @private + * @type {number} + */ + this.renderedRotation_; + + /** + * @private + * @type {import("../../coordinate").Coordinate} + */ + this.renderedCenter_ = null; + + /** + * @private + * @type {import("../../proj/Projection").default} + */ + this.renderedProjection_ = null; + /** * @private * @type {function(import("../../Feature.js").default, import("../../Feature.js").default): number|null} @@ -124,6 +162,8 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { const extent = frameState.extent; const viewState = frameState.viewState; + const center = viewState.center; + const resolution = viewState.resolution; const projection = viewState.projection; const rotation = viewState.rotation; const projectionExtent = projection.getExtent(); @@ -143,7 +183,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { const viewHints = frameState.viewHints; const snapToPixel = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]); - const transform = this.getRenderTransform(frameState, width, height, 0); + const transform = this.getRenderTransform(center, resolution, rotation, pixelRatio, width, height, 0); const declutterReplays = this.getLayer().getDeclutter() ? {} : null; replayGroup.execute(context, transform, rotation, snapToPixel, undefined, declutterReplays); @@ -155,7 +195,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { while (startX < projectionExtent[0]) { --world; offsetX = worldWidth * world; - const transform = this.getRenderTransform(frameState, width, height, offsetX); + const transform = this.getRenderTransform(center, resolution, rotation, pixelRatio, width, height, offsetX); replayGroup.execute(context, transform, rotation, snapToPixel, undefined, declutterReplays); startX += worldWidth; } @@ -164,7 +204,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { while (startX > projectionExtent[2]) { ++world; offsetX = worldWidth * world; - const transform = this.getRenderTransform(frameState, width, height, offsetX); + const transform = this.getRenderTransform(center, resolution, rotation, pixelRatio, width, height, offsetX); replayGroup.execute(context, transform, rotation, snapToPixel, undefined, declutterReplays); startX -= worldWidth; } @@ -190,6 +230,175 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { return this.container; } + /** + * @private + */ + createHitDetectionImageData_() { + const features = this.renderedFeatures_; + const layer = this.getLayer(); + const resolution = this.renderedResolution_; + const size = [this.context.canvas.width, this.context.canvas.height]; + apply(this.pixelTransform, size); + const width = size[0] / 2; + const height = size[1] / 2; + const context = createCanvasContext2D(width, height); + context.imageSmoothingEnabled = false; + const canvas = context.canvas; + const styleFunction = layer.getStyleFunction(); + const center = this.renderedCenter_; + const rotation = this.renderedRotation_; + const projection = this.renderedProjection_; + const extent = this.renderedExtent_; + const transforms = []; + transforms.push(this.getRenderTransform(center, resolution, rotation, 0.5, width, height, 0).slice()); + const source = layer.getSource(); + const projectionExtent = projection.getExtent(); + if (source.getWrapX() && projection.canWrapX() && !containsExtent(projectionExtent, extent)) { + let startX = extent[0]; + const worldWidth = getWidth(projectionExtent); + let world = 0; + let offsetX; + while (startX < projectionExtent[0]) { + --world; + offsetX = worldWidth * world; + transforms.push(this.getRenderTransform(center, resolution, rotation, 0.5, width, height, offsetX).slice()); + startX += worldWidth; + } + world = 0; + startX = extent[2]; + while (startX > projectionExtent[2]) { + ++world; + offsetX = worldWidth * world; + transforms.push(this.getRenderTransform(center, resolution, rotation, 0.5, width, height, offsetX).slice()); + startX -= worldWidth; + } + } + const renderer = new CanvasImmediateRenderer(context, 0.5, extent, null, rotation); + const featureCount = features.length; + // Stretch hit detection index to use the whole available color range + const indexFactor = Math.ceil((256 * 256 * 256) / featureCount); + const featuresByZIndex = {}; + for (let i = 0; i < featureCount; ++i) { + const feature = features[i]; + const featureStyleFunction = feature.getStyleFunction() || styleFunction; + let styles = featureStyleFunction(feature, resolution); + if (!Array.isArray(styles)) { + styles = [styles]; + } + const index = i * indexFactor; + const color = '#' + ('000000' + index.toString(16)).slice(-6); + for (let j = 0, jj = styles.length; j < jj; ++j) { + const originalStyle = styles[j]; + const style = originalStyle.clone(); + const fill = style.getFill(); + if (fill) { + fill.setColor(color); + } + const stroke = style.getStroke(); + if (stroke) { + stroke.setColor(color); + } + style.setText(undefined); + const image = originalStyle.getImage(); + if (image) { + const imgSize = image.getImageSize(); + const imgContext = createCanvasContext2D(imgSize[0], imgSize[1]); + imgContext.fillStyle = color; + const img = imgContext.canvas; + imgContext.fillRect(0, 0, img.width, img.height); + const width = imgSize ? imgSize[0] : img.width; + const height = imgSize ? imgSize[1] : img.height; + const iconContext = createCanvasContext2D(width, height); + iconContext.drawImage(img, 0, 0); + style.setImage(new Icon({ + img: img, + imgSize: imgSize, + anchor: image.getAnchor(), + anchorXUnits: IconAnchorUnits.PIXELS, + anchorYUnits: IconAnchorUnits.PIXELS, + offset: image.getOrigin(), + size: image.getSize(), + opacity: image.getOpacity(), + scale: image.getScale(), + rotation: image.getRotation(), + rotateWithView: image.getRotateWithView() + })); + } + const zIndex = Number(style.getZIndex()); + let byGeometryType = featuresByZIndex[zIndex]; + if (!byGeometryType) { + byGeometryType = featuresByZIndex[zIndex] = {}; + byGeometryType[GeometryType.POLYGON] = []; + byGeometryType[GeometryType.CIRCLE] = []; + byGeometryType[GeometryType.LINE_STRING] = []; + byGeometryType[GeometryType.POINT] = []; + } + const geometry = style.getGeometryFunction()(feature); + if (geometry && intersectsExtent(extent, geometry.getExtent())) { + byGeometryType[geometry.getType().replace('Multi', '')].push(geometry, style); + } + } + } + + const zIndexKeys = Object.keys(featuresByZIndex).map(Number).sort(numberSafeCompareFunction); + for (let i = 0, ii = zIndexKeys.length; i < ii; ++i) { + const byGeometryType = featuresByZIndex[zIndexKeys[i]]; + for (const type in byGeometryType) { + const geomAndStyle = byGeometryType[type]; + for (let j = 0, jj = geomAndStyle.length; j < jj; j += 2) { + renderer.setStyle(geomAndStyle[j + 1]); + for (let k = 0, kk = transforms.length; k < kk; ++k) { + renderer.setTransform(transforms[k]); + renderer.drawGeometry(geomAndStyle[j]); + } + } + } + } + this.hitDetectionImageData_ = context.getImageData(0, 0, canvas.width, canvas.height); + } + + /** + * @param {import("../../pixel").Pixel} pixel Pixel. + * @return {Array} features Features. + */ + hitDetect_(pixel) { + const renderPixel = apply(this.pixelTransform, pixel.slice()); + const features = this.renderedFeatures_; + const imageData = this.hitDetectionImageData_; + const resultFeatures = []; + if (imageData) { + const index = (Math.round(renderPixel[0]) + Math.round(renderPixel[1]) * imageData.width) * 4; + const r = imageData.data[index]; + const g = imageData.data[index + 1]; + const b = imageData.data[index + 2]; + const a = imageData.data[index + 3]; + if (a === 255) { + const i = b + (256 * (g + (256 * r))); + const indexFactor = Math.ceil((256 * 256 * 256) / features.length); + if (i % indexFactor === 0) { + resultFeatures.push(features[i / indexFactor]); + } + } + } + return resultFeatures; + } + + /** + * @inheritDoc + */ + getFeatures(pixel) { + return new Promise(function(resolve, reject) { + if (!this.hitDetectionImageData_ && !this.animatingOrInteracting_) { + requestAnimationFrame(function() { + this.createHitDetectionImageData_(); + resolve(this.hitDetect_(pixel)); + }.bind(this)); + } else { + resolve(this.hitDetect_(pixel)); + } + }.bind(this)); + } + /** * @inheritDoc */ @@ -253,8 +462,10 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { if (!this.dirty_ && (!updateWhileAnimating && animating) || (!updateWhileInteracting && interacting)) { + this.animatingOrInteracting_ = true; return true; } + this.animatingOrInteracting_ = false; const frameStateExtent = frameState.extent; const viewState = frameState.viewState; @@ -269,6 +480,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { vectorLayerRenderOrder = defaultRenderOrder; } + const center = viewState.center.slice(); const extent = buffer(frameStateExtent, vectorLayerRenderBuffer * resolution); const projectionExtent = viewState.projection.getExtent(); @@ -284,6 +496,8 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { const gutter = Math.max(getWidth(extent) / 2, worldWidth); extent[0] = projectionExtent[0] - gutter; extent[2] = projectionExtent[2] + gutter; + const worldsAway = Math.floor((center[0] - projectionExtent[0]) / worldWidth); + center[0] -= (worldsAway * worldWidth); } if (!this.dirty_ && @@ -334,23 +548,15 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { }.bind(this); const userExtent = toUserExtent(extent, projection); + /** @type {Array} */ + const features = vectorSource.getFeaturesInExtent(userExtent); if (vectorLayerRenderOrder) { - /** @type {Array} */ - const features = []; - vectorSource.forEachFeatureInExtent(userExtent, - /** - * @param {import("../../Feature.js").default} feature Feature. - */ - function(feature) { - features.push(feature); - }); features.sort(vectorLayerRenderOrder); - for (let i = 0, ii = features.length; i < ii; ++i) { - render(features[i]); - } - } else { - vectorSource.forEachFeatureInExtent(userExtent, render); } + for (let i = 0, ii = features.length; i < ii; ++i) { + render(features[i]); + } + this.renderedFeatures_ = features; const replayGroupInstructions = replayGroup.finish(); const executorGroup = new ExecutorGroup(extent, resolution, @@ -361,7 +567,11 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { this.renderedRevision_ = vectorLayerRevision; this.renderedRenderOrder_ = vectorLayerRenderOrder; this.renderedExtent_ = extent; + this.renderedRotation_ = viewState.rotation; + this.renderedCenter_ = center; + this.renderedProjection_ = projection; this.replayGroup_ = executorGroup; + this.hitDetectionImageData_ = null; this.replayGroupChanged = true; return true; diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index c84d44ff38..ea643ff69a 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -428,7 +428,10 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { const declutterReplays = layer.getDeclutter() ? {} : null; const replayTypes = VECTOR_REPLAYS[renderMode]; const pixelRatio = frameState.pixelRatio; - const rotation = frameState.viewState.rotation; + const viewState = frameState.viewState; + const center = viewState.center; + const resolution = viewState.resolution; + const rotation = viewState.rotation; const size = frameState.size; // set forward and inverse pixel transforms @@ -462,7 +465,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { const tileCoord = tile.tileCoord; const tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord); const worldOffset = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent)[0] - tileExtent[0]; - const transform = this.getRenderTransform(frameState, width, height, worldOffset); + const transform = this.getRenderTransform(center, resolution, rotation, pixelRatio, width, height, worldOffset); const executorGroups = tile.executorGroups[getUid(layer)]; let clipped = false; for (let t = 0, tt = executorGroups.length; t < tt; ++t) { diff --git a/src/ol/source/Vector.js b/src/ol/source/Vector.js index fc2d7f938a..0d0905e5c2 100644 --- a/src/ol/source/Vector.js +++ b/src/ol/source/Vector.js @@ -675,14 +675,21 @@ class VectorSource extends Source { * all features intersecting the given extent in random order (so it may include * features whose geometries do not intersect the extent). * - * This method is not available when the source is configured with - * `useSpatialIndex` set to `false`. + * When `useSpatialIndex` is set to false, this method will return all + * features. + * * @param {import("../extent.js").Extent} extent Extent. * @return {Array>} Features. * @api */ getFeaturesInExtent(extent) { - return this.featuresRtree_.getInExtent(extent); + if (this.featuresRtree_) { + return this.featuresRtree_.getInExtent(extent); + } else if (this.featuresCollection_) { + return this.featuresCollection_.getArray(); + } else { + return []; + } } diff --git a/test/spec/ol/renderer/canvas/vectorimage.test.js b/test/spec/ol/renderer/canvas/vectorimage.test.js index fcb6fe505c..98d41e1fda 100644 --- a/test/spec/ol/renderer/canvas/vectorimage.test.js +++ b/test/spec/ol/renderer/canvas/vectorimage.test.js @@ -40,6 +40,7 @@ describe('ol/renderer/canvas/VectorImageLayer', function() { extent: extent, viewHints: [], viewState: { + center: [0, 0], projection: projection, resolution: 1, rotation: 0 diff --git a/test/spec/ol/renderer/canvas/vectorlayer.test.js b/test/spec/ol/renderer/canvas/vectorlayer.test.js index 368a4c5ade..280fc4d8c6 100644 --- a/test/spec/ol/renderer/canvas/vectorlayer.test.js +++ b/test/spec/ol/renderer/canvas/vectorlayer.test.js @@ -208,6 +208,7 @@ describe('ol.renderer.canvas.VectorLayer', function() { const frameState = { layerStatesArray: [{}], viewState: { + center: [0, 0], resolution: 1, rotation: 0 } @@ -234,6 +235,7 @@ describe('ol.renderer.canvas.VectorLayer', function() { frameState = { viewHints: [], viewState: { + center: [0, 0], projection: projection, resolution: 1, rotation: 0 diff --git a/test/spec/ol/renderer/canvas/vectortilelayer.test.js b/test/spec/ol/renderer/canvas/vectortilelayer.test.js index 926c2d1724..7d91af8e51 100644 --- a/test/spec/ol/renderer/canvas/vectortilelayer.test.js +++ b/test/spec/ol/renderer/canvas/vectortilelayer.test.js @@ -22,6 +22,7 @@ import VectorTileRenderType from '../../../../../src/ol/layer/VectorTileRenderTy import {getUid} from '../../../../../src/ol/util.js'; import TileLayer from '../../../../../src/ol/layer/Tile.js'; import XYZ from '../../../../../src/ol/source/XYZ.js'; +import {create} from '../../../../../src/ol/transform.js'; describe('ol.renderer.canvas.VectorTileLayer', function() { @@ -262,6 +263,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { layerIndex: 0, extent: proj.getExtent(), pixelRatio: 1, + pixelToCoordinateTransform: create(), time: Date.now(), viewHints: [], viewState: {