diff --git a/src/ol/color.js b/src/ol/color.js index 4b52b056c5..482ebb01a1 100644 --- a/src/ol/color.js +++ b/src/ol/color.js @@ -213,7 +213,7 @@ export function toString(color) { if (b != (b | 0)) { b = (b + 0.5) | 0; } - const a = color[3] === undefined ? 1 : color[3]; + const a = color[3] === undefined ? 1 : Math.round(color[3] * 100) / 100; return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; } diff --git a/src/ol/layer/Base.js b/src/ol/layer/Base.js index 4a06cb4051..e2d6c5c1e5 100644 --- a/src/ol/layer/Base.js +++ b/src/ol/layer/Base.js @@ -8,6 +8,13 @@ import {assert} from '../asserts.js'; import {assign} from '../obj.js'; import {clamp} from '../math.js'; +/** + * A css color, or a function called with a view resolution returning a css color. + * + * @typedef {string|function(number):string} BackgroundColor + * @api + */ + /** * @typedef {import("../ObjectEventType").Types|'change:extent'|'change:maxResolution'|'change:maxZoom'| * 'change:minResolution'|'change:minZoom'|'change:opacity'|'change:visible'|'change:zIndex'} BaseLayerObjectEventTypes @@ -39,6 +46,8 @@ import {clamp} from '../math.js'; * visible. * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will * be visible. + * @property {BackgroundColor} [background] Background color for the layer. If not specified, no background + * will be rendered. * @property {Object} [properties] Arbitrary observable properties. Can be accessed with `#get()` and `#set()`. */ @@ -74,6 +83,12 @@ class BaseLayer extends BaseObject { */ this.un; + /** + * @type {BackgroundColor|false} + * @private + */ + this.background_ = options.background; + /** * @type {Object} */ @@ -116,6 +131,14 @@ class BaseLayer extends BaseObject { this.state_ = null; } + /** + * Get the background for this layer. + * @return {BackgroundColor|false} Layer background. + */ + getBackground() { + return this.background_; + } + /** * @return {string} CSS class name. */ @@ -265,6 +288,15 @@ class BaseLayer extends BaseObject { return /** @type {number} */ (this.get(LayerProperty.Z_INDEX)); } + /** + * Sets the backgrlound color. + * @param {BackgroundColor} [opt_background] Background color. + */ + setBackground(opt_background) { + this.background_ = opt_background; + this.changed(); + } + /** * Set the extent at which the layer is visible. If `undefined`, the layer * will be visible at all extents. diff --git a/src/ol/layer/BaseVector.js b/src/ol/layer/BaseVector.js index 275ae720cc..b8fee1a734 100644 --- a/src/ol/layer/BaseVector.js +++ b/src/ol/layer/BaseVector.js @@ -48,6 +48,8 @@ import { * @property {import("../style/Style.js").StyleLike|null} [style] Layer style. When set to `null`, only * features that have their own style will be rendered. See {@link module:ol/style/Style~Style} for the default style * which will be used if this is not set. + * @property {import("./Base.js").BackgroundColor} [background] Background color for the layer. If not specified, no background + * will be rendered. * @property {boolean} [updateWhileAnimating=false] When set to `true`, feature batches will * be recreated during animations. This means that no vectors will be shown clipped, but the * setting will have a performance impact for large amounts of vector data. When set to `false`, diff --git a/src/ol/layer/MapboxVector.js b/src/ol/layer/MapboxVector.js index fe7b65e929..5821d98adb 100644 --- a/src/ol/layer/MapboxVector.js +++ b/src/ol/layer/MapboxVector.js @@ -3,17 +3,13 @@ */ import BaseEvent from '../events/Event.js'; import EventType from '../events/EventType.js'; -import GeometryType from '../geom/GeometryType.js'; import MVT from '../format/MVT.js'; -import RenderFeature from '../render/Feature.js'; import SourceState from '../source/State.js'; -import TileEventType from '../source/TileEventType.js'; import VectorTileLayer from '../layer/VectorTile.js'; import VectorTileSource from '../source/VectorTile.js'; -import {Fill, Style} from '../style.js'; import {applyStyle, setupVectorSource} from 'ol-mapbox-style'; -import {fromExtent} from '../geom/Polygon.js'; import {getValue} from 'ol-mapbox-style/dist/stylefunction.js'; +import {toString} from '../color.js'; const mapboxBaseUrl = 'https://api.mapbox.com'; @@ -188,6 +184,9 @@ const SourceType = { * is defined by the z-index of the layer, the `zIndex` of the style and the render order of features. * Higher z-index means higher priority. Within the same z-index, a feature rendered before another has * higher priority. + * @property {import("./Base.js").BackgroundColor|false} [background] Background color for the layer. + * If not specified, the background from the Mapbox style object will be used. Set to `false` to prevent + * the Mapbox style's background from being used. * @property {string} [className='ol-layer'] A CSS class name to set to the layer element. * @property {number} [opacity=1] Opacity (0, 1). * @property {boolean} [visible=true] Visibility. @@ -283,6 +282,7 @@ class MapboxVectorLayer extends VectorTileLayer { super({ source: source, + background: options.background, declutter: declutter, className: options.className, opacity: options.opacity, @@ -493,53 +493,36 @@ class MapboxVectorLayer extends VectorTileLayer { (layer) => layer.type === 'background' ); if ( + this.getBackground() === undefined && background && (!background.layout || background.layout.visibility !== 'none') ) { - const style = new Style({ - fill: new Fill(), - }); - targetSource.addEventListener(TileEventType.TILELOADEND, (event) => { - const tile = /** @type {import("../VectorTile.js").default} */ ( - /** @type {import("../source/Tile.js").TileSourceEvent} */ (event) - .tile + const colorFunction = (resolution) => { + const opacity = + /** @type {number} */ ( + getValue( + background, + 'paint', + 'background-opacity', + this.getSource().getTileGrid().getZForResolution(resolution) + ) + ) || 1; + const color = /** @type {*} */ ( + getValue( + background, + 'paint', + 'background-color', + this.getSource().getTileGrid().getZForResolution(resolution) + ) ); - const styleFuntion = () => { - const opacity = - /** @type {number} */ ( - getValue( - background, - 'paint', - 'background-opacity', - tile.tileCoord[0] - ) - ) || 1; - const color = /** @type {*} */ ( - getValue(background, 'paint', 'background-color', tile.tileCoord[0]) - ); - style - .getFill() - .setColor([ - color.r * 255, - color.g * 255, - color.b * 255, - color.a * opacity, - ]); - return style; - }; - const extentGeometry = fromExtent( - targetSource.tileGrid.getTileCoordExtent(tile.tileCoord) - ); - const renderFeature = new RenderFeature( - GeometryType.POLYGON, - extentGeometry.getFlatCoordinates(), - extentGeometry.getEnds(), - {layer: background.id}, - undefined - ); - renderFeature.styleFunction = styleFuntion; - tile.getFeatures().unshift(renderFeature); - }); + return toString([ + color.r * 255, + color.g * 255, + color.b * 255, + color.a * opacity, + ]); + }; + this.setBackground(colorFunction); } if (this.setMaxResolutionFromTileGrid_) { const tileGrid = targetSource.getTileGrid(); diff --git a/src/ol/layer/VectorTile.js b/src/ol/layer/VectorTile.js index 3ef6279aff..4870b02614 100644 --- a/src/ol/layer/VectorTile.js +++ b/src/ol/layer/VectorTile.js @@ -65,6 +65,8 @@ import {assign} from '../obj.js'; * @property {import("../style/Style.js").StyleLike|null} [style] Layer style. When set to `null`, only * features that have their own style will be rendered. See {@link module:ol/style/Style~Style} for the default style * which will be used if this is not set. + * @property {import("./Base.js").BackgroundColor|false} [background] Background color for the layer. If not specified, no + * background will be rendered. * @property {boolean} [updateWhileAnimating=false] When set to `true`, feature batches will be * recreated during animations. This means that no vectors will be shown clipped, but the setting * will have a performance impact for large amounts of vector data. When set to `false`, batches @@ -145,6 +147,20 @@ class VectorTileLayer extends BaseVectorLayer { ? options.useInterimTilesOnError : true ); + + /** + * @return {import("./Base.js").BackgroundColor} Background color. + * @function + * @api + */ + this.getBackground; + + /** + * @param {import("./Base.js").BackgroundColor} background Background color. + * @function + * @api + */ + this.setBackground; } createRenderer() { diff --git a/src/ol/renderer/canvas/Layer.js b/src/ol/renderer/canvas/Layer.js index 178b906f7d..5e1f3c65ef 100644 --- a/src/ol/renderer/canvas/Layer.js +++ b/src/ol/renderer/canvas/Layer.js @@ -9,6 +9,7 @@ import { compose as composeTransform, create as createTransform, } from '../../transform.js'; +import {asArray} from '../../color.js'; import { containsCoordinate, getBottomLeft, @@ -17,6 +18,7 @@ import { getTopRight, } from '../../extent.js'; import {createCanvasContext2D} from '../../dom.js'; +import {equals} from '../../array.js'; /** * @abstract @@ -83,20 +85,39 @@ class CanvasLayerRenderer extends LayerRenderer { this.pixelContext_ = null; } + /** + * @param {import('../../PluggableMap.js').FrameState} frameState Frame state. + * @return {string} Background color. + */ + getBackground(frameState) { + const layer = this.getLayer(); + let background = layer.getBackground(); + if (typeof background === 'function') { + background = background(frameState.viewState.resolution); + } + return background || undefined; + } + /** * Get a rendering container from an existing target, if compatible. * @param {HTMLElement} target Potential render target. * @param {string} transform CSS Transform. * @param {number} opacity Opacity. + * @param {string} [opt_backgroundColor] Background color. */ - useContainer(target, transform, opacity) { + useContainer(target, transform, opacity, opt_backgroundColor) { const layerClassName = this.getLayer().getClassName(); let container, context; if ( target && target.className === layerClassName && target.style.opacity === '' && - opacity === 1 + opacity === 1 && + (!opt_backgroundColor || + equals( + asArray(target.style.backgroundColor), + asArray(opt_backgroundColor) + )) ) { const canvas = target.firstElementChild; if (canvas instanceof HTMLCanvasElement) { @@ -121,6 +142,9 @@ class CanvasLayerRenderer extends LayerRenderer { style.position = 'absolute'; style.width = '100%'; style.height = '100%'; + if (opt_backgroundColor) { + style.backgroundColor = opt_backgroundColor; + } context = createCanvasContext2D(); const canvas = context.canvas; container.appendChild(canvas); diff --git a/src/ol/renderer/canvas/TileLayer.js b/src/ol/renderer/canvas/TileLayer.js index 3485fddcf7..e68f3d04af 100644 --- a/src/ol/renderer/canvas/TileLayer.js +++ b/src/ol/renderer/canvas/TileLayer.js @@ -283,7 +283,12 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { const canvasTransform = toTransformString(this.pixelTransform); - this.useContainer(target, canvasTransform, layerState.opacity); + this.useContainer( + target, + canvasTransform, + layerState.opacity, + this.getBackground(frameState) + ); const context = this.context; const canvas = context.canvas; diff --git a/test/browser/spec/ol/layer/MapboxVector.test.js b/test/browser/spec/ol/layer/MapboxVector.test.js index 38b783597d..1079f06d52 100644 --- a/test/browser/spec/ol/layer/MapboxVector.test.js +++ b/test/browser/spec/ol/layer/MapboxVector.test.js @@ -4,7 +4,7 @@ import MapboxVectorLayer, { normalizeSpriteUrl, normalizeStyleUrl, } from '../../../../../src/ol/layer/MapboxVector.js'; -import {get} from '../../../../../src/ol/proj.js'; +import {asString} from '../../../../../src/ol/color.js'; import {unByKey} from '../../../../../src/ol/Observable.js'; describe('ol/layer/MapboxVector', () => { @@ -199,7 +199,7 @@ describe('ol/layer/MapboxVector', () => { }); describe('background', function () { - it('adds a feature for the background', function (done) { + it('configures the layer with a background function', function (done) { const layer = new MapboxVectorLayer({ styleUrl: 'data:,' + @@ -229,15 +229,45 @@ describe('ol/layer/MapboxVector', () => { const key = source.on('change', function () { if (source.getState() === 'ready') { unByKey(key); - source.getTile(14, 8938, 5680, 1, get('EPSG:3857')).load(); - source.once('tileloadend', (event) => { - const features = event.tile.getFeatures(); - expect(features[0].get('layer')).to.be('background'); - expect( - features[0].getStyleFunction()().getFill().getColor() - ).to.eql([255, 0, 0, 0.8]); - done(); - }); + expect(layer.getBackground()(1)).to.eql(asString([255, 0, 0, 0.8])); + done(); + } + }); + }); + + it("avoids the style's background with `background: false`", function (done) { + const layer = new MapboxVectorLayer({ + styleUrl: + 'data:,' + + encodeURIComponent( + JSON.stringify({ + version: 8, + sources: { + 'foo': { + tiles: ['/spec/ol/data/{z}-{x}-{y}.vector.pbf'], + type: 'vector', + }, + }, + layers: [ + { + id: 'background', + type: 'background', + paint: { + 'background-color': '#ff0000', + 'background-opacity': 0.8, + }, + }, + ], + }) + ), + background: false, + }); + const source = layer.getSource(); + const key = source.on('change', function () { + if (source.getState() === 'ready') { + unByKey(key); + expect(layer.getBackground()).to.be(false); + done(); } }); }); @@ -274,15 +304,8 @@ describe('ol/layer/MapboxVector', () => { const key = source.on('change', function () { if (source.getState() === 'ready') { unByKey(key); - source.getTile(14, 8938, 5680, 1, get('EPSG:3857')).load(); - source.once('tileloadend', (event) => { - const features = event.tile.getFeatures(); - expect(features[0].get('layer')).to.be('landuse'); - expect( - layer.getStyleFunction()(features[0])[0].getFill().getColor() - ).to.eql('rgba(255,0,0,0.8)'); - done(); - }); + expect(layer.getBackground()).to.be(undefined); + done(); } }); }); diff --git a/test/browser/spec/ol/renderer/canvas/vectortilelayer.test.js b/test/browser/spec/ol/renderer/canvas/vectortilelayer.test.js index 9240cbf8bc..f30d43d55b 100644 --- a/test/browser/spec/ol/renderer/canvas/vectortilelayer.test.js +++ b/test/browser/spec/ol/renderer/canvas/vectortilelayer.test.js @@ -22,7 +22,7 @@ import {getCenter} from '../../../../../../src/ol/extent.js'; import {get as getProjection} from '../../../../../../src/ol/proj.js'; import {getUid} from '../../../../../../src/ol/util.js'; -describe('ol.renderer.canvas.VectorTileLayer', function () { +describe('ol/renderer/canvas/VectorTileLayer', function () { describe('constructor', function () { const head = document.getElementsByTagName('head')[0]; const font = document.createElement('link'); @@ -246,6 +246,78 @@ describe('ol.renderer.canvas.VectorTileLayer', function () { done(); }); }); + + it('reuses render container when previous layer has a background', function (done) { + map.getLayers().insertAt( + 0, + new TileLayer({ + background: 'rgb(255, 0, 0)', + source: new XYZ({ + url: 'rendering/ol/data/tiles/osm/{z}/{x}/{y}.png', + }), + }) + ); + map.once('rendercomplete', function () { + expect(document.querySelector('.ol-layers').childElementCount).to.be(1); + expect(document.querySelector('.ol-layer').childElementCount).to.be(1); + map.removeLayer(map.getLayers().item(1)); + map.renderSync(); + expect(document.querySelector('.ol-layer').childElementCount).to.be(1); + done(); + }); + }); + + it('does not reuse render container when backgrounds are different', function (done) { + map.getLayers().insertAt( + 0, + new TileLayer({ + background: 'rgb(255, 0, 0)', + source: new XYZ({ + url: 'rendering/ol/data/tiles/osm/{z}/{x}/{y}.png', + }), + }) + ); + map.getLayers().insertAt( + 0, + new TileLayer({ + background: 'rgba(255, 0, 0, 0.1)', + source: new XYZ({ + url: 'rendering/ol/data/tiles/osm/{z}/{x}/{y}.png', + }), + }) + ); + map.once('rendercomplete', function () { + expect(document.querySelector('.ol-layers').childElementCount).to.be(2); + expect(document.querySelector('.ol-layer').childElementCount).to.be(1); + map.removeLayer(map.getLayers().item(1)); + map.renderSync(); + expect(document.querySelector('.ol-layers').childElementCount).to.be(1); + done(); + }); + }); + + it('sets the configured background (string) on the container', function (done) { + layer.setBackground('rgba(255, 0, 0, 0.5)'); + map.once('rendercomplete', function () { + expect(layer.getRenderer().container.style.backgroundColor).to.be( + 'rgba(255, 0, 0, 0.5)' + ); + done(); + }); + }); + + it('sets the configured background (function) on the container', function (done) { + layer.setBackground(function (resolution) { + expect(resolution).to.be(map.getView().getResolution()); + return 'rgba(255, 0, 0, 0.5)'; + }); + map.once('rendercomplete', function () { + expect(layer.getRenderer().container.style.backgroundColor).to.be( + 'rgba(255, 0, 0, 0.5)' + ); + done(); + }); + }); }); describe('#prepareFrame', function () {