diff --git a/doc/errors/index.md b/doc/errors/index.md index d4deea08cf..a762cfa599 100644 --- a/doc/errors/index.md +++ b/doc/errors/index.md @@ -111,7 +111,7 @@ Features for `updates` must have an id set by the feature reader or `ol.Feature# ### 28 -`renderMode` must be `'image'` or `'hybrid'`. +`renderMode` must be `'image'`, `'hybrid'` or `'vector'`. ### 29 diff --git a/examples/vector-tile-selection.html b/examples/vector-tile-selection.html index b856bb6543..ba352f8c6d 100644 --- a/examples/vector-tile-selection.html +++ b/examples/vector-tile-selection.html @@ -5,7 +5,11 @@ shortdesc: Select features from vector tiles. docs: >
Click a rendered vector-tile feature to highlight it on the map. Click on an empty spot (ocean) to reset the selection. - By changing the action type to "Multi Select" you can select multiple features at a time. + By changing the action type to "Multi Select" you can select multiple features at a time. With "Single Select on hover", + features will be higlighted when the pointer is above them. +
+ The selection layer is configured with `renderMode: 'vector'` for better performance on frequent redraws, + i.e. when 'Single select on hover' is selected.
tags: "vector tiles, selection" --- @@ -15,5 +19,6 @@ tags: "vector tiles, selection" diff --git a/examples/vector-tile-selection.js b/examples/vector-tile-selection.js index b351c08771..79cf73cd7a 100644 --- a/examples/vector-tile-selection.js +++ b/examples/vector-tile-selection.js @@ -8,6 +8,25 @@ import {Fill, Stroke, Style} from '../src/ol/style.js'; // lookup for selection objects let selection = {}; +const country = new Style({ + stroke: new Stroke({ + color: 'gray', + width: 1 + }), + fill: new Fill({ + color: 'rgba(20,20,20,0.9)' + }) +}); +const selectedCountry = new Style({ + stroke: new Stroke({ + color: 'rgba(200,20,20,0.8)', + width: 2 + }), + fill: new Fill({ + color: 'rgba(200,20,20,0.4)' + }) +}); + const vtLayer = new VectorTileLayer({ declutter: true, source: new VectorTileSource({ @@ -18,18 +37,7 @@ const vtLayer = new VectorTileLayer({ url: 'https://ahocevar.com/geoserver/gwc/service/tms/1.0.0/' + 'ne:ne_10m_admin_0_countries@EPSG%3A900913@pbf/{z}/{x}/{-y}.pbf' }), - style: function(feature) { - const selected = !!selection[feature.getId()]; - return new Style({ - stroke: new Stroke({ - color: selected ? 'rgba(200,20,20,0.8)' : 'gray', - width: selected ? 2 : 1 - }), - fill: new Fill({ - color: selected ? 'rgba(200,20,20,0.2)' : 'rgba(20,20,20,0.9)' - }) - }); - } + style: country }); const map = new Map({ @@ -39,18 +47,34 @@ const map = new Map({ target: 'map', view: new View({ center: [0, 0], - zoom: 2 + zoom: 2, + multiWorld: true }) }); +// Selection +const selectionLayer = new VectorTileLayer({ + map: map, + renderMode: 'vector', + source: vtLayer.getSource(), + style: function(feature) { + if (feature.getId() in selection) { + return selectedCountry; + } + } +}); + const selectElement = document.getElementById('type'); -map.on('click', function(event) { +map.on(['click', 'pointermove'], function(event) { + if (selectElement.value === 'singleselect-hover' && event.type !== 'pointermove' || + selectElement.value !== 'singleselect-hover' && event.type === 'pointermove') { + return; + } vtLayer.getFeatures(event.pixel).then(function(features) { if (!features.length) { selection = {}; - // force redraw of layer style - vtLayer.setStyle(vtLayer.getStyle()); + selectionLayer.changed(); return; } const feature = features[0]; @@ -59,14 +83,12 @@ map.on('click', function(event) { } const fid = feature.getId(); - if (selectElement.value === 'singleselect') { + if (selectElement.value.startsWith('singleselect')) { selection = {}; } // add selected feature to lookup selection[fid] = feature; - // force redraw of layer style - vtLayer.setStyle(vtLayer.getStyle()); + selectionLayer.changed(); }); - }); diff --git a/rendering/cases/layer-vectortile-rendermode-vector/expected.png b/rendering/cases/layer-vectortile-rendermode-vector/expected.png new file mode 100644 index 0000000000..76bfd13499 Binary files /dev/null and b/rendering/cases/layer-vectortile-rendermode-vector/expected.png differ diff --git a/rendering/cases/layer-vectortile-rendermode-vector/main.js b/rendering/cases/layer-vectortile-rendermode-vector/main.js new file mode 100644 index 0000000000..0d136106df --- /dev/null +++ b/rendering/cases/layer-vectortile-rendermode-vector/main.js @@ -0,0 +1,31 @@ +import Map from '../../../src/ol/Map.js'; +import View from '../../../src/ol/View.js'; +import VectorTileSource from '../../../src/ol/source/VectorTile.js'; +import MVT from '../../../src/ol/format/MVT.js'; +import {createXYZ} from '../../../src/ol/tilegrid.js'; +import VectorTileLayer from '../../../src/ol/layer/VectorTile.js'; +import VectorTileRenderType from '../../../src/ol/layer/VectorTileRenderType.js'; + +new Map({ + layers: [ + new VectorTileLayer({ + renderMode: VectorTileRenderType.VECTOR, + source: new VectorTileSource({ + format: new MVT(), + tileGrid: createXYZ(), + url: '/data/tiles/mapbox-streets-v6/{z}/{x}/{y}.vector.pbf', + transition: 0 + }) + }) + ], + target: 'map', + view: new View({ + center: [1825927.7316762917, 6143091.089223046], + zoom: 14 + }) +}); + +render({ + message: 'Vector tile layer renders with vector render mode', + tolerance: 0.01 +}); diff --git a/src/ol/layer/VectorTile.js b/src/ol/layer/VectorTile.js index 8ede1b7c81..a53e377215 100644 --- a/src/ol/layer/VectorTile.js +++ b/src/ol/layer/VectorTile.js @@ -40,7 +40,9 @@ import {assign} from '../obj.js'; * * `'hybrid'`: Polygon and line elements are rendered as images, so pixels are scaled during zoom * animations. Point symbols and texts are accurately rendered as vectors and can stay upright on * rotated views. - * + * * `'vector'`: Everything is rendered as vectors. Use this mode for improved performance on vector + * tile layers with only a few rendered features (e.g. for highlighting a subset of features of + * another layer with the same source). * @property {import("../source/VectorTile.js").default} [source] Source. * @property {import("../PluggableMap.js").default} [map] Sets the layer as overlay on a map. The map will not manage * this layer in its layers collection, and the layer will be rendered on top. This is useful for @@ -92,8 +94,9 @@ class VectorTileLayer extends BaseVectorLayer { const renderMode = options.renderMode || VectorTileRenderType.HYBRID; assert(renderMode == undefined || renderMode == VectorTileRenderType.IMAGE || - renderMode == VectorTileRenderType.HYBRID, - 28); // `renderMode` must be `'image'` or `'hybrid'` + renderMode == VectorTileRenderType.HYBRID || + renderMode == VectorTileRenderType.VECTOR, + 28); // `renderMode` must be `'image'`, `'hybrid'` or `'vector'`. /** * @private diff --git a/src/ol/layer/VectorTileRenderType.js b/src/ol/layer/VectorTileRenderType.js index e4acef8f6b..69ec94fd87 100644 --- a/src/ol/layer/VectorTileRenderType.js +++ b/src/ol/layer/VectorTileRenderType.js @@ -11,9 +11,14 @@ * * `'hybrid'`: Polygon and line elements are rendered as images, so pixels * are scaled during zoom animations. Point symbols and texts are accurately * rendered as vectors and can stay upright on rotated views. + * * `'vector'`: Everything is rendered as vectors. Use this mode for improved + * performance on vector tile layers with only a few rendered features (e.g. + * for highlighting a subset of features of another layer with the same + * source). * @api */ export default { IMAGE: 'image', - HYBRID: 'hybrid' + HYBRID: 'hybrid', + VECTOR: 'vector' }; diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index df3e1b8011..bcb25d234d 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -33,7 +33,8 @@ import {createHitDetectionImageData, hitDetect} from '../../render/canvas/hitdet const IMAGE_REPLAYS = { 'image': [ReplayType.POLYGON, ReplayType.CIRCLE, ReplayType.LINE_STRING, ReplayType.IMAGE, ReplayType.TEXT], - 'hybrid': [ReplayType.POLYGON, ReplayType.LINE_STRING] + 'hybrid': [ReplayType.POLYGON, ReplayType.LINE_STRING], + 'vector': [] }; @@ -42,7 +43,8 @@ const IMAGE_REPLAYS = { */ const VECTOR_REPLAYS = { 'image': [ReplayType.DEFAULT], - 'hybrid': [ReplayType.IMAGE, ReplayType.TEXT, ReplayType.DEFAULT] + 'hybrid': [ReplayType.IMAGE, ReplayType.TEXT, ReplayType.DEFAULT], + 'vector': [ReplayType.POLYGON, ReplayType.CIRCLE, ReplayType.LINE_STRING, ReplayType.IMAGE, ReplayType.TEXT, ReplayType.DEFAULT] }; @@ -156,7 +158,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { tile.wantedResolution = resolution; } const render = this.prepareTile(tile, pixelRatio, projection, false); - if (render) { + if (render && this.getLayer().getRenderMode() !== VectorTileRenderType.VECTOR) { this.renderTileImage_(tile, frameState); } } @@ -167,7 +169,8 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { * @inheritdoc */ isDrawableTile(tile) { - return super.isDrawableTile(tile) && tile.hasContext(this.getLayer()); + const layer = this.getLayer(); + return super.isDrawableTile(tile) && layer.getRenderMode() === VectorTileRenderType.VECTOR || tile.hasContext(layer); } /** @@ -267,7 +270,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { } const executorGroupInstructions = builderGroup.finish(); // no need to clip when the render tile is covered by a single source tile - const replayExtent = layer.getDeclutter() && sourceTiles.length === 1 ? + const replayExtent = layer.getRenderMode() !== VectorTileRenderType.VECTOR && layer.getDeclutter() && sourceTiles.length === 1 ? null : sharedExtent; const renderingReplayGroup = new CanvasExecutorGroup(replayExtent, resolution, diff --git a/test/spec/ol/renderer/canvas/vectortilelayer.test.js b/test/spec/ol/renderer/canvas/vectortilelayer.test.js index 631fa0487b..72475e4db9 100644 --- a/test/spec/ol/renderer/canvas/vectortilelayer.test.js +++ b/test/spec/ol/renderer/canvas/vectortilelayer.test.js @@ -122,6 +122,22 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { spy.restore(); }); + it('does not render images for pure vector rendering', function() { + const testLayer = new VectorTileLayer({ + renderMode: VectorTileRenderType.VECTOR, + source: source, + style: layerStyle + }); + map.removeLayer(layer); + map.addLayer(testLayer); + const spy = sinon.spy(CanvasVectorTileLayerRenderer.prototype, + 'renderTileImage_'); + map.renderSync(); + expect(spy.callCount).to.be(0); + spy.restore(); + }); + + it('renders both replays and images for hybrid rendering', function() { const spy1 = sinon.spy(CanvasVectorTileLayerRenderer.prototype, 'getRenderTransform');