Sources and formats
-
Tile sources for
ol/layer/Tile
+
Tile sources for
ol/layer/Tile or
ol/layer/WebGLTile
Image sources for
ol/layer/Image
Vector sources for
ol/layer/Vector
Vector tile sources for
ol/layer/VectorTile
@@ -71,7 +72,7 @@
diff --git a/src/ol/layer/WebGLTile.js b/src/ol/layer/WebGLTile.js
index bc90be434c..7080c7ce3c 100644
--- a/src/ol/layer/WebGLTile.js
+++ b/src/ol/layer/WebGLTile.js
@@ -74,7 +74,7 @@ import {assign} from '../obj.js';
/**
* @param {Style} style The layer style.
- * @param {number} bandCount The number of bands.
+ * @param {number} [bandCount] The number of bands.
* @return {ParsedStyle} Shaders and uniforms generated from the style.
*/
function parseStyle(style, bandCount) {
@@ -291,6 +291,7 @@ class WebGLTileLayer extends BaseTileLayer {
vertexShader: parsedStyle.vertexShader,
fragmentShader: parsedStyle.fragmentShader,
uniforms: parsedStyle.uniforms,
+ className: this.getClassName(),
});
}
diff --git a/src/ol/source/DataTile.js b/src/ol/source/DataTile.js
index 15c857aa6b..3a813aa34e 100644
--- a/src/ol/source/DataTile.js
+++ b/src/ol/source/DataTile.js
@@ -100,7 +100,7 @@ class DataTileSource extends TileSource {
* @param {number} y Tile coordinate y.
* @param {number} pixelRatio Pixel ratio.
* @param {import("../proj/Projection.js").default} projection Projection.
- * @return {!import("../Tile.js").default} Tile.
+ * @return {!DataTile} Tile.
*/
getTile(z, x, y, pixelRatio, projection) {
const tileCoordKey = getKeyZXY(z, x, y);
diff --git a/test/browser/karma.config.cjs b/test/browser/karma.config.cjs
index 296d201efe..9cba92f393 100644
--- a/test/browser/karma.config.cjs
+++ b/test/browser/karma.config.cjs
@@ -71,6 +71,13 @@ module.exports = function (karma) {
webpack: {
devtool: 'inline-source-map',
mode: 'development',
+ resolve: {
+ fallback: {
+ fs: false,
+ http: false,
+ https: false,
+ },
+ },
module: {
rules: [
{
diff --git a/test/browser/spec/ol/datatile.test.js b/test/browser/spec/ol/datatile.test.js
new file mode 100644
index 0000000000..6ec7c2168c
--- /dev/null
+++ b/test/browser/spec/ol/datatile.test.js
@@ -0,0 +1,51 @@
+import DataTile from '../../../../src/ol/DataTile.js';
+import TileState from '../../../../src/ol/TileState.js';
+
+describe('ol.DataTile', function () {
+ /** @type {Promise
{
+ const canvas = document.createElement('canvas');
+ canvas.width = 256;
+ canvas.height = 256;
+ const context = canvas.getContext('2d');
+ context.fillStyle = 'red';
+ context.fillRect(0, 0, 256, 256);
+ resolve(context.getImageData(0, 0, 256, 256).data);
+ });
+ };
+ });
+
+ describe('constructor', function () {
+ it('sets options', function () {
+ const tileCoord = [0, 0, 0];
+ const tile = new DataTile({
+ tileCoord: tileCoord,
+ loader: loader,
+ transition: 200,
+ });
+ expect(tile.tileCoord).to.equal(tileCoord);
+ expect(tile.transition_).to.be(200);
+ expect(tile.loader_).to.equal(loader);
+ });
+ });
+
+ describe('#load()', function () {
+ it('handles loading states correctly', function (done) {
+ const tileCoord = [0, 0, 0];
+ const tile = new DataTile({
+ tileCoord: tileCoord,
+ loader: loader,
+ });
+ expect(tile.getState()).to.be(TileState.IDLE);
+ tile.load();
+ expect(tile.getState()).to.be(TileState.LOADING);
+ setTimeout(() => {
+ expect(tile.getState()).to.be(TileState.LOADED);
+ done();
+ }, 16);
+ });
+ });
+});
diff --git a/test/browser/spec/ol/layer/webgltile.test.js b/test/browser/spec/ol/layer/webgltile.test.js
new file mode 100644
index 0000000000..1fae9b1771
--- /dev/null
+++ b/test/browser/spec/ol/layer/webgltile.test.js
@@ -0,0 +1,139 @@
+import DataTileSource from '../../../../../src/ol/source/DataTile.js';
+import Map from '../../../../../src/ol/Map.js';
+import View from '../../../../../src/ol/View.js';
+import WebGLHelper from '../../../../../src/ol/webgl/Helper.js';
+import WebGLTileLayer from '../../../../../src/ol/layer/WebGLTile.js';
+import {createCanvasContext2D} from '../../../../../src/ol/dom.js';
+
+describe('ol.layer.Tile', function () {
+ /** @type {WebGLTileLayer} */
+ let layer;
+ /** @type {Map} */
+ let map, target;
+
+ beforeEach(function (done) {
+ layer = new WebGLTileLayer({
+ className: 'testlayer',
+ source: new DataTileSource({
+ loader(z, x, y) {
+ return new Promise((resolve) => {
+ resolve(new ImageData(256, 256));
+ });
+ },
+ }),
+ style: {
+ variables: {
+ r: 0,
+ g: 255,
+ b: 0,
+ },
+ color: ['color', ['var', 'r'], ['var', 'g'], ['var', 'b']],
+ },
+ });
+ target = document.createElement('div');
+ target.style.width = '100px';
+ target.style.height = '100px';
+ document.body.appendChild(target);
+ map = new Map({
+ target: target,
+ layers: [layer],
+ view: new View({
+ center: [0, 0],
+ zoom: 2,
+ }),
+ });
+ map.once('rendercomplete', () => done());
+ });
+
+ afterEach(function () {
+ map.setTarget(null);
+ document.body.removeChild(target);
+ });
+
+ it('creates fragment and vertex shaders', function () {
+ const compileShaderSpy = sinon.spy(WebGLHelper.prototype, 'compileShader');
+ layer.createRenderer();
+ compileShaderSpy.restore();
+ expect(compileShaderSpy.callCount).to.be(2);
+ expect(compileShaderSpy.getCall(0).args[0].replace(/[ \n]+/g, ' ')).to.be(
+ `
+ #ifdef GL_FRAGMENT_PRECISION_HIGH
+ precision highp float;
+ #else
+ precision mediump float;
+ #endif
+ varying vec2 v_textureCoord;
+ uniform float u_transitionAlpha;
+ uniform float u_texturePixelWidth;
+ uniform float u_texturePixelHeight;
+ uniform float u_resolution;
+ uniform float u_zoom;
+ uniform float u_var_r;
+ uniform float u_var_g;
+ uniform float u_var_b;
+ uniform sampler2D u_tileTexture0;
+ void main() {
+ vec4 color0 = texture2D(u_tileTexture0, v_textureCoord);
+ vec4 color = color0;
+ color = vec4(u_var_r / 255.0, u_var_g / 255.0, u_var_b / 255.0, 1.0);
+ if (color.a == 0.0) {
+ discard;
+ }
+ gl_FragColor = color;
+ gl_FragColor.rgb *= gl_FragColor.a;
+ gl_FragColor *= u_transitionAlpha;
+ }`.replace(/[ \n]+/g, ' ')
+ );
+
+ expect(compileShaderSpy.getCall(1).args[0].replace(/[ \n]+/g, ' ')).to.be(
+ `
+ attribute vec2 a_textureCoord;
+ uniform mat4 u_tileTransform;
+ uniform float u_depth;
+
+ varying vec2 v_textureCoord;
+ void main() {
+ v_textureCoord = a_textureCoord;
+ gl_Position = u_tileTransform * vec4(a_textureCoord, u_depth, 1.0);
+ }
+ `.replace(/[ \n]+/g, ' ')
+ );
+ });
+
+ it('updates style variables', function (done) {
+ layer.updateStyleVariables({
+ r: 255,
+ g: 0,
+ b: 255,
+ });
+ expect(layer.styleVariables_['r']).to.be(255);
+ const targetContext = createCanvasContext2D(100, 100);
+ layer.on('postrender', () => {
+ targetContext.clearRect(0, 0, 100, 100);
+ targetContext.drawImage(target.querySelector('.testlayer'), 0, 0);
+ });
+ map.once('rendercomplete', () => {
+ expect(Array.from(targetContext.getImageData(0, 0, 1, 1).data)).to.eql([
+ 255, 0, 255, 255,
+ ]);
+ done();
+ });
+ });
+
+ it('throws on incorrect style configs', function () {
+ function incorrectStyle() {
+ layer.style_ = {
+ variables: {
+ 'red': 25,
+ 'green': 200,
+ },
+ exposure: 0,
+ contrast: 0,
+ saturation: 0,
+ color: ['color', ['var', 'red'], ['var', 'green'], ['var', 'blue']],
+ };
+ layer.createRenderer();
+ }
+ expect(incorrectStyle).to.throwException(); // missing 'blue' in styleVariables
+ });
+});
diff --git a/test/browser/spec/ol/renderer/webgl/tilelayer.test.js b/test/browser/spec/ol/renderer/webgl/tilelayer.test.js
new file mode 100644
index 0000000000..d1dc885246
--- /dev/null
+++ b/test/browser/spec/ol/renderer/webgl/tilelayer.test.js
@@ -0,0 +1,95 @@
+import TileQueue from '../../../../../../src/ol/TileQueue.js';
+import TileState from '../../../../../../src/ol/TileState.js';
+import WebGLTileLayer from '../../../../../../src/ol/layer/WebGLTile.js';
+import {DataTile} from '../../../../../../src/ol/source.js';
+import {VOID} from '../../../../../../src/ol/functions.js';
+import {create} from '../../../../../../src/ol/transform.js';
+import {createCanvasContext2D} from '../../../../../../src/ol/dom.js';
+import {get} from '../../../../../../src/ol/proj.js';
+
+describe('ol.renderer.webgl.TileLayer', function () {
+ /** @type {import("../../../../../../src/ol/renderer/webgl/TileLayer.js").default} */
+ let renderer;
+ /** @type {WebGLTileLayer} */
+ let tileLayer;
+ /** @type {import('../../../../../../src/ol/PluggableMap.js').FrameState} */
+ let frameState;
+ beforeEach(function () {
+ const size = 256;
+ const context = createCanvasContext2D(size, size);
+
+ tileLayer = new WebGLTileLayer({
+ source: new DataTile({
+ loader: function (z, x, y) {
+ context.clearRect(0, 0, size, size);
+ context.fillStyle = 'rgba(100, 100, 100, 0.5)';
+ context.fillRect(0, 0, size, size);
+ const data = context.getImageData(0, 0, size, size).data;
+ return Promise.resolve(data);
+ },
+ }),
+ });
+
+ renderer = tileLayer.createRenderer();
+
+ const proj = get('EPSG:3857');
+ frameState = {
+ layerStatesArray: [tileLayer.getLayerState()],
+ layerIndex: 0,
+ extent: proj.getExtent(),
+ pixelRatio: 1,
+ pixelToCoordinateTransform: create(),
+ postRenderFunctions: [],
+ time: Date.now(),
+ viewHints: [],
+ viewState: {
+ center: [0, 0],
+ resolution: 156543.03392804097,
+ projection: proj,
+ },
+ size: [256, 256],
+ usedTiles: {},
+ wantedTiles: {},
+ tileQueue: new TileQueue(VOID, VOID),
+ };
+ });
+
+ it('#prepareFrame()', function () {
+ const source = tileLayer.getSource();
+ tileLayer.setSource(null);
+ expect(renderer.prepareFrame(frameState)).to.be(false);
+ tileLayer.setSource(source);
+ expect(renderer.prepareFrame(frameState)).to.be(true);
+ const tileGrid = source.getTileGrid();
+ tileLayer.setExtent(tileGrid.getTileCoordExtent([2, 0, 0]));
+ frameState.resolution = tileGrid.getResolution(2);
+ frameState.extent = tileGrid.getTileCoordExtent([2, 2, 2]);
+ frameState.layerStatesArray = [tileLayer.getLayerState()];
+ expect(renderer.prepareFrame(frameState)).to.be(false);
+ });
+
+ it('#renderFrame()', function () {
+ const rendered = renderer.renderFrame(frameState);
+ expect(rendered).to.be.a(HTMLCanvasElement);
+ expect(frameState.tileQueue.getCount()).to.be(1);
+ expect(Object.keys(frameState.wantedTiles).length).to.be(1);
+ expect(frameState.postRenderFunctions.length).to.be(0); // no tile expired
+ expect(renderer.tileTextureCache_.count_).to.be(1);
+ });
+
+ it('#isDrawableTile()', function (done) {
+ const tile = tileLayer.getSource().getTile(0, 0, 0);
+ expect(renderer.isDrawableTile(tile)).to.be(false);
+ tileLayer.getSource().on('tileloadend', () => {
+ expect(renderer.isDrawableTile(tile)).to.be(true);
+ done();
+ });
+ tile.load();
+ const errorTile = tileLayer.getSource().getTile(1, 0, 1);
+ errorTile.setState(TileState.ERROR);
+ tileLayer.setUseInterimTilesOnError(false);
+ expect(renderer.isDrawableTile(errorTile)).to.be(true);
+ tileLayer.setUseInterimTilesOnError(true);
+ expect(renderer.isDrawableTile(errorTile)).to.be(false);
+ });
+});
diff --git a/test/browser/spec/ol/source/datatile.test.js b/test/browser/spec/ol/source/datatile.test.js
new file mode 100644
index 0000000000..0b3aafd3ef
--- /dev/null
+++ b/test/browser/spec/ol/source/datatile.test.js
@@ -0,0 +1,42 @@
+import DataTile from '../../../../../src/ol/DataTile.js';
+import DataTileSource from '../../../../../src/ol/source/DataTile.js';
+import TileState from '../../../../../src/ol/TileState.js';
+
+describe('ol.source.DataTile', function () {
+ /** @type {DataTileSource} */
+ let source;
+ beforeEach(function () {
+ const loader = function (z, x, y) {
+ return new Promise((resolve) => {
+ const canvas = document.createElement('canvas');
+ canvas.width = 256;
+ canvas.height = 256;
+ const context = canvas.getContext('2d');
+ // encode tile coordinate in rgb
+ context.fillStyle = `rgb(${z}, ${x % 255}, ${y % 255})`;
+ context.fillRect(0, 0, 256, 256);
+ resolve(context.getImageData(0, 0, 256, 256).data);
+ });
+ };
+ source = new DataTileSource({
+ loader: loader,
+ });
+ });
+
+ describe('#getTile()', function () {
+ it('gets tiles and fires a tileloadend event', function (done) {
+ const tile = source.getTile(3, 2, 1);
+ expect(tile).to.be.a(DataTile);
+ expect(tile.state).to.be(TileState.IDLE);
+
+ source.on('tileloadend', () => {
+ expect(tile.state).to.be(TileState.LOADED);
+ // decode tile coordinate from rgb
+ expect(Array.from(tile.getData().slice(0, 3))).to.eql([3, 2, 1]);
+ done();
+ });
+
+ tile.load();
+ });
+ });
+});
diff --git a/test/browser/spec/ol/source/geotiff.test.js b/test/browser/spec/ol/source/geotiff.test.js
new file mode 100644
index 0000000000..4efb2199a1
--- /dev/null
+++ b/test/browser/spec/ol/source/geotiff.test.js
@@ -0,0 +1,45 @@
+import GeoTIFFSource from '../../../../../src/ol/source/GeoTIFF.js';
+import State from '../../../../../src/ol/source/State.js';
+import TileState from '../../../../../src/ol/TileState.js';
+
+describe('ol.source.GeoTIFF', function () {
+ /** @type {GeoTIFFSource} */
+ let source;
+ beforeEach(function () {
+ source = new GeoTIFFSource({
+ sources: [
+ {
+ url: 'spec/ol/source/images/0-0-0.tif',
+ },
+ ],
+ });
+ });
+
+ it('manages load states', function (done) {
+ expect(source.getState()).to.be(State.LOADING);
+ source.on('change', () => {
+ expect(source.getState()).to.be(State.READY);
+ done();
+ });
+ });
+
+ it('configures itself from source metadata', function (done) {
+ source.on('change', () => {
+ expect(source.bandCount).to.be(3);
+ expect(source.getTileGrid().getResolutions().length).to.be(1);
+ expect(source.projection.getCode()).to.be('EPSG:4326');
+ done();
+ });
+ });
+
+ it('loads tiles', function (done) {
+ source.on('change', () => {
+ const tile = source.getTile(0, 0, 0);
+ source.on('tileloadend', () => {
+ expect(tile.getState()).to.be(TileState.LOADED);
+ done();
+ });
+ tile.load();
+ });
+ });
+});
diff --git a/test/browser/spec/ol/source/images/0-0-0.tif b/test/browser/spec/ol/source/images/0-0-0.tif
new file mode 100644
index 0000000000..3507f24029
Binary files /dev/null and b/test/browser/spec/ol/source/images/0-0-0.tif differ
diff --git a/test/browser/spec/ol/style/expressions.test.js b/test/browser/spec/ol/style/expressions.test.js
index fcf1f9e004..d2e6980b0c 100644
--- a/test/browser/spec/ol/style/expressions.test.js
+++ b/test/browser/spec/ol/style/expressions.test.js
@@ -241,6 +241,12 @@ describe('ol.style.expressions', function () {
['-', ['get', 'attr3'], ['get', 'attr2']],
])
).to.eql('abs((a_attr3 - a_attr2))');
+ expect(expressionToGlsl(context, ['sin', 1])).to.eql('sin(1.0)');
+ expect(expressionToGlsl(context, ['cos', 1])).to.eql('cos(1.0)');
+ expect(expressionToGlsl(context, ['atan', 1])).to.eql('atan(1.0)');
+ expect(expressionToGlsl(context, ['atan', 1, 0.5])).to.eql(
+ 'atan(1.0, 0.5)'
+ );
expect(expressionToGlsl(context, ['>', 10, ['get', 'attr4']])).to.eql(
'(10.0 > a_attr4)'
);
@@ -283,6 +289,10 @@ describe('ol.style.expressions', function () {
expect(
expressionToGlsl(context, ['color', ['get', 'attr4'], 1, 2, 0.5])
).to.eql('vec4(a_attr4 / 255.0, 1.0 / 255.0, 2.0 / 255.0, 0.5)');
+ expect(expressionToGlsl(context, ['band', 1])).to.eql('color0[0]');
+ expect(expressionToGlsl(context, ['band', 1, -1, 2])).to.eql(
+ 'texture2D(u_tileTexture0, v_textureCoord + vec2(-1.0 / u_texturePixelWidth, 2.0 / u_texturePixelHeight))[0]'
+ );
});
it('throws if the value does not match the type', function () {
diff --git a/test/browser/spec/ol/webgl/tiletexture.test.js b/test/browser/spec/ol/webgl/tiletexture.test.js
new file mode 100644
index 0000000000..ef6bc29733
--- /dev/null
+++ b/test/browser/spec/ol/webgl/tiletexture.test.js
@@ -0,0 +1,86 @@
+import DataTile from '../../../../../src/ol/DataTile.js';
+import DataTileSource from '../../../../../src/ol/source/DataTile.js';
+import ImageTile from '../../../../../src/ol/ImageTile.js';
+import TileState from '../../../../../src/ol/TileState.js';
+import TileTexture from '../../../../../src/ol/webgl/TileTexture.js';
+import WebGLArrayBuffer from '../../../../../src/ol/webgl/Buffer.js';
+import WebGLTileLayer from '../../../../../src/ol/layer/WebGLTile.js';
+import {createCanvasContext2D} from '../../../../../src/ol/dom.js';
+
+describe('ol.webgl.TileTexture', function () {
+ /** @type {TileTexture} */
+ let tileTexture;
+
+ beforeEach(function () {
+ const layer = new WebGLTileLayer({
+ source: new DataTileSource({
+ loader(z, x, y) {
+ return new Promise((resolve) => {
+ const context = createCanvasContext2D(256, 256);
+ context.fillStyle = `rgb(${z}, ${x % 255}, ${y % 255})`;
+ context.fillRect(0, 0, 256, 256);
+ resolve(context.getImageData(0, 0, 256, 256).data);
+ });
+ },
+ }),
+ });
+ const renderer =
+ /** @type {import("../../../../../src/ol/renderer/webgl/TileLayer.js").default} */ (
+ layer.createRenderer()
+ );
+ tileTexture = new TileTexture(
+ layer.getSource().getTile(3, 2, 1),
+ layer.getSource().getTileGrid(),
+ renderer.helper
+ );
+ });
+
+ it('constructor', function () {
+ expect(tileTexture.tile.tileCoord).to.eql([3, 2, 1]);
+ expect(tileTexture.coords).to.be.a(WebGLArrayBuffer);
+ });
+
+ it('handles data tiles', function (done) {
+ const dataTile = tileTexture.tile;
+ expect(tileTexture.loaded).to.be(false);
+ expect(dataTile.getState()).to.be(TileState.IDLE);
+ tileTexture.addEventListener('change', () => {
+ if (dataTile.getState() === TileState.LOADED) {
+ expect(tileTexture.loaded).to.be(true);
+ done();
+ }
+ });
+ dataTile.load();
+ });
+
+ it('handles image tiles', function () {
+ const imageTile = new ImageTile([0, 0, 0], TileState.LOADED);
+ tileTexture.setTile(imageTile);
+ expect(tileTexture.loaded).to.be(true);
+ });
+
+ it('registers and unregisters change listener', function () {
+ const tile = tileTexture.tile;
+ expect(tile.getListeners('change').length).to.be(2);
+ tileTexture.dispose();
+ expect(tile.getListeners('change').length).to.be(1);
+ });
+
+ it('updates metadata and unregisters change listener when setting a different tile', function (done) {
+ const tile = tileTexture.tile;
+ expect(tile.getListeners('change').length).to.be(2);
+ const differentTile = new DataTile({
+ tileCoord: [1, 0, 1],
+ loader(z, x, y) {
+ return Promise.resolve(new Uint8Array(256 * 256 * 3));
+ },
+ });
+ tileTexture.setTile(differentTile);
+ expect(tile.getListeners('change').length).to.be(1);
+ tileTexture.addEventListener('change', () => {
+ expect(tileTexture.bandCount).to.be(3);
+ done();
+ });
+ differentTile.load();
+ });
+});
diff --git a/test/rendering/cases/layer-tile-webgl/expected.png b/test/rendering/cases/layer-tile-webgl/expected.png
new file mode 100644
index 0000000000..5d7011ff7d
Binary files /dev/null and b/test/rendering/cases/layer-tile-webgl/expected.png differ
diff --git a/test/rendering/cases/layer-tile-webgl/main.js b/test/rendering/cases/layer-tile-webgl/main.js
new file mode 100644
index 0000000000..6eb87d2786
--- /dev/null
+++ b/test/rendering/cases/layer-tile-webgl/main.js
@@ -0,0 +1,26 @@
+import Map from '../../../../src/ol/Map.js';
+import TileLayer from '../../../../src/ol/layer/WebGLTile.js';
+import View from '../../../../src/ol/View.js';
+import XYZ from '../../../../src/ol/source/XYZ.js';
+import {fromLonLat} from '../../../../src/ol/proj.js';
+
+const center = fromLonLat([8.6, 50.1]);
+
+new Map({
+ layers: [
+ new TileLayer({
+ source: new XYZ({
+ url: '/data/tiles/satellite/{z}/{x}/{y}.jpg',
+ transition: 0,
+ crossOrigin: 'anonymous',
+ }),
+ }),
+ ],
+ target: 'map',
+ view: new View({
+ center: center,
+ zoom: 3,
+ }),
+});
+
+render();