diff --git a/examples/cog-pyramid.html b/examples/cog-pyramid.html
index 3e32e650ad..c24f5155f6 100644
--- a/examples/cog-pyramid.html
+++ b/examples/cog-pyramid.html
@@ -4,9 +4,8 @@ title: GeoTIFF tile pyramid
shortdesc: Rendering a COG tile pyramid as layer group.
docs: >
Data from a Cloud Optimized GeoTIFF (COG) tile pyramid can be rendered as a set of layers. In this
- example, a pyramid of 3-band GeoTIFFs is used to render RGB data. For each tile of the pyramid, a
- separate layer is created on demand. The lowest resolution layer serves as preview while higher resolutions are
- loading.
+ example, a pyramid of 3-band GeoTIFFs is used to render RGB data. The `ol/source.sourcesFromTileGrid`
+ helper function creates sources from this pyramid on demand.
tags: "cog, tilepyramid, stac"
---
diff --git a/examples/cog-pyramid.js b/examples/cog-pyramid.js
index ed63732a49..65dd898e62 100644
--- a/examples/cog-pyramid.js
+++ b/examples/cog-pyramid.js
@@ -1,16 +1,16 @@
import GeoTIFF from '../src/ol/source/GeoTIFF.js';
-import LayerGroup from '../src/ol/layer/Group.js';
import Map from '../src/ol/Map.js';
import TileGrid from '../src/ol/tilegrid/TileGrid.js';
import View from '../src/ol/View.js';
import WebGLTileLayer from '../src/ol/layer/WebGLTile.js';
-import {getIntersection} from '../src/ol/extent.js';
+import {createXYZ} from '../src/ol/tilegrid.js';
+import {sourcesFromTileGrid} from '../src/ol/source.js';
// Metadata from https://s2downloads.eox.at/demo/EOxCloudless/2019/rgb/2019_EOxCloudless_rgb.json
// Tile grid of the GeoTIFF pyramid layout
const tileGrid = new TileGrid({
- origin: [-180, 90],
+ extent: [-180, -90, 180, 90],
resolutions: [0.703125, 0.3515625, 0.17578125, 8.7890625e-2, 4.39453125e-2],
tileSizes: [
[512, 256],
@@ -21,30 +21,25 @@ const tileGrid = new TileGrid({
],
});
-const pyramid = new LayerGroup();
-const layerForUrl = {};
-const zs = tileGrid.getResolutions().length;
-
-function useLayer(z, x, y) {
- const url = `https://s2downloads.eox.at/demo/EOxCloudless/2019/rgb/${z}/${y}/${x}.tif`;
- if (!(url in layerForUrl)) {
- pyramid.getLayers().push(
- new WebGLTileLayer({
- minZoom: z,
- maxZoom: z === 0 || z === zs - 1 ? undefined : z + 1,
- extent: tileGrid.getTileCoordExtent([z, x, y]),
- source: new GeoTIFF({
- sources: [
- {
- url: url,
- },
- ],
+const pyramid = new WebGLTileLayer({
+ sources: sourcesFromTileGrid(
+ tileGrid,
+ ([z, x, y]) =>
+ new GeoTIFF({
+ tileGrid: createXYZ({
+ extent: tileGrid.getTileCoordExtent([z, x, y]),
+ minZoom: z,
+ maxZoom:
+ z === tileGrid.getResolutions().length - 1 ? undefined : z + 1,
}),
+ sources: [
+ {
+ url: `https://s2downloads.eox.at/demo/EOxCloudless/2019/rgb/${z}/${y}/${x}.tif`,
+ },
+ ],
})
- );
- layerForUrl[url] = true;
- }
-}
+ ),
+});
const map = new Map({
target: 'map',
@@ -56,16 +51,3 @@ const map = new Map({
showFullExtent: true,
}),
});
-
-// Add overview layer
-useLayer(0, 0, 0);
-
-// Add layer for specific extent on demand
-map.on('moveend', () => {
- const view = map.getView();
- tileGrid.forEachTileCoord(
- getIntersection([-180, -90, 180, 90], view.calculateExtent()),
- tileGrid.getZForResolution(view.getResolution()),
- ([z, x, y]) => useLayer(z, x, y)
- );
-});
diff --git a/src/ol/layer/Base.js b/src/ol/layer/Base.js
index e2d6c5c1e5..6141cd84d0 100644
--- a/src/ol/layer/Base.js
+++ b/src/ol/layer/Base.js
@@ -163,7 +163,6 @@ class BaseLayer extends BaseObject {
});
const zIndex = this.getZIndex();
state.opacity = clamp(Math.round(this.getOpacity() * 100) / 100, 0, 1);
- state.sourceState = this.getSourceState();
state.visible = this.getVisible();
state.extent = this.getExtent();
state.zIndex = zIndex === undefined && !state.managed ? Infinity : zIndex;
diff --git a/src/ol/layer/Layer.js b/src/ol/layer/Layer.js
index 775f7eac93..87dce260ca 100644
--- a/src/ol/layer/Layer.js
+++ b/src/ol/layer/Layer.js
@@ -57,7 +57,7 @@ import {listen, unlistenByKey} from '../events.js';
* @typedef {Object} State
* @property {import("./Layer.js").default} layer Layer.
* @property {number} opacity Opacity, the value is rounded to two digits to appear after the decimal point.
- * @property {import("../source/State.js").default} sourceState SourceState.
+ * @property {import("../source/Source.js").default|undefined} source Source being rendered (only for multi-source layers).
* @property {boolean} visible Visible.
* @property {boolean} managed Managed.
* @property {import("../extent.js").Extent} [extent] Extent.
@@ -196,6 +196,13 @@ class Layer extends BaseLayer {
return /** @type {SourceType} */ (this.get(LayerProperty.SOURCE)) || null;
}
+ /**
+ * @return {SourceType} The source being rendered.
+ */
+ getRenderSource() {
+ return this.getSource();
+ }
+
/**
* @return {import("../source/State.js").default} Source state.
*/
diff --git a/src/ol/layer/WebGLTile.js b/src/ol/layer/WebGLTile.js
index 1409a69ef7..c25caf417a 100644
--- a/src/ol/layer/WebGLTile.js
+++ b/src/ol/layer/WebGLTile.js
@@ -3,6 +3,7 @@
*/
import BaseTileLayer from './BaseTile.js';
import LayerProperty from '../layer/Property.js';
+import SourceState from '../source/State.js';
import WebGLTileLayerRenderer, {
Attributes,
Uniforms,
@@ -64,6 +65,11 @@ import {assign} from '../obj.js';
* @property {number} [preload=0] Preload. Load low-resolution tiles up to `preload` levels. `0`
* means no preloading.
* @property {SourceType} [source] Source for this layer.
+ * @property {Array|function(import("../extent.js").Extent, number):Array} [sources] Array
+ * of sources for this layer. Takes precedence over `source`. Can either be an array of sources, or a function that
+ * expects an extent and a resolution (in view projection units per pixel) and returns an array of sources. See
+ * {@link module:ol/source.sourcesFromTileGrid} for a helper function to generate sources that are organized in a
+ * pyramid following the same pattern as a tile grid.
* @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
* temporary layers. The standard way to add a layer to a map and have it managed by the map is to
@@ -291,6 +297,18 @@ class WebGLTileLayer extends BaseTileLayer {
super(options);
+ /**
+ * @type {Array|function(import("../extent.js").Extent, number):Array}
+ * @private
+ */
+ this.sources_ = options.sources;
+
+ /**
+ * @type {number}
+ * @private
+ */
+ this.renderedResolution_ = NaN;
+
/**
* @type {Style}
* @private
@@ -312,6 +330,41 @@ class WebGLTileLayer extends BaseTileLayer {
this.addChangeListener(LayerProperty.SOURCE, this.handleSourceUpdate_);
}
+ /**
+ * Gets the sources for this layer, for a given extent and resolution.
+ * @param {import("../extent.js").Extent} extent Extent.
+ * @param {number} resolution Resolution.
+ * @return {Array} Sources.
+ */
+ getSources(extent, resolution) {
+ const source = this.getSource();
+ return this.sources_
+ ? typeof this.sources_ === 'function'
+ ? this.sources_(extent, resolution)
+ : this.sources_
+ : source
+ ? [source]
+ : [];
+ }
+
+ /**
+ * @return {SourceType} The source being rendered.
+ */
+ getRenderSource() {
+ return (
+ /** @type {SourceType} */ (this.getLayerState().source) ||
+ this.getSource()
+ );
+ }
+
+ /**
+ * @return {import("../source/State.js").default} Source state.
+ */
+ getSourceState() {
+ const source = this.getRenderSource();
+ return source ? source.getState() : SourceState.UNDEFINED;
+ }
+
/**
* @private
*/
@@ -340,6 +393,66 @@ class WebGLTileLayer extends BaseTileLayer {
});
}
+ /**
+ * @param {import("../PluggableMap").FrameState} frameState Frame state.
+ * @param {Array} sources Sources.
+ * @return {HTMLElement} Canvas.
+ */
+ renderSources(frameState, sources) {
+ const layerRenderer = this.getRenderer();
+ let canvas;
+ for (let i = 0, ii = sources.length; i < ii; ++i) {
+ this.getLayerState().source = sources[i];
+ if (layerRenderer.prepareFrame(frameState)) {
+ canvas = layerRenderer.renderFrame(frameState);
+ }
+ }
+ return canvas;
+ }
+
+ /**
+ * @param {?import("../PluggableMap.js").FrameState} frameState Frame state.
+ * @param {HTMLElement} target Target which the renderer may (but need not) use
+ * for rendering its content.
+ * @return {HTMLElement} The rendered element.
+ */
+ render(frameState, target) {
+ const viewState = frameState.viewState;
+ const sources = this.getSources(frameState.extent, viewState.resolution);
+ let ready = true;
+ for (let i = 0, ii = sources.length; i < ii; ++i) {
+ const source = sources[i];
+ const sourceState = source.getState();
+ if (sourceState == SourceState.LOADING) {
+ const onChange = () => {
+ if (source.getState() == SourceState.READY) {
+ source.removeEventListener('change', onChange);
+ this.changed();
+ }
+ };
+ source.addEventListener('change', onChange);
+ }
+ ready = ready && sourceState == SourceState.READY;
+ }
+ const canvas = this.renderSources(frameState, sources);
+ if (this.getRenderer().renderComplete && ready) {
+ // Fully rendered, done.
+ this.renderedResolution_ = viewState.resolution;
+ return canvas;
+ }
+ // Render sources from previously fully rendered frames
+ if (this.renderedResolution_ > 0.5 * viewState.resolution) {
+ const altSources = this.getSources(
+ frameState.extent,
+ this.renderedResolution_
+ ).filter((source) => !sources.includes(source));
+ if (altSources.length > 0) {
+ return this.renderSources(frameState, altSources);
+ }
+ }
+ return canvas;
+ }
+
/**
* Update the layer style. The `updateStyleVariables` function is a more efficient
* way to update layer rendering. In cases where the whole style needs to be updated,
diff --git a/src/ol/renderer/Composite.js b/src/ol/renderer/Composite.js
index 69d9ec3c5a..00f4d37f8c 100644
--- a/src/ol/renderer/Composite.js
+++ b/src/ol/renderer/Composite.js
@@ -110,15 +110,17 @@ class CompositeMapRenderer extends MapRenderer {
for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) {
const layerState = layerStatesArray[i];
frameState.layerIndex = i;
+
+ const layer = layerState.layer;
+ const sourceState = layer.getSourceState();
if (
!inView(layerState, viewState) ||
- (layerState.sourceState != SourceState.READY &&
- layerState.sourceState != SourceState.UNDEFINED)
+ (sourceState != SourceState.READY &&
+ sourceState != SourceState.UNDEFINED)
) {
continue;
}
- const layer = layerState.layer;
const element = layer.render(frameState, previousElement);
if (!element) {
continue;
diff --git a/src/ol/renderer/webgl/Layer.js b/src/ol/renderer/webgl/Layer.js
index b11bf36902..00f4082db6 100644
--- a/src/ol/renderer/webgl/Layer.js
+++ b/src/ol/renderer/webgl/Layer.js
@@ -161,7 +161,7 @@ class WebGLLayerRenderer extends LayerRenderer {
* @return {boolean} Layer is ready to be rendered.
*/
prepareFrame(frameState) {
- if (this.getLayer().getSource()) {
+ if (this.getLayer().getRenderSource()) {
let incrementGroup = true;
let groupNumber = -1;
let className;
diff --git a/src/ol/renderer/webgl/TileLayer.js b/src/ol/renderer/webgl/TileLayer.js
index f77b792770..2af3a33b3d 100644
--- a/src/ol/renderer/webgl/TileLayer.js
+++ b/src/ol/renderer/webgl/TileLayer.js
@@ -20,7 +20,6 @@ import {
} from '../../vec/mat4.js';
import {
createOrUpdate as createTileCoord,
- getKeyZXY,
getKey as getTileCoordKey,
} from '../../tilecoord.js';
import {fromUserExtent} from '../../proj.js';
@@ -98,7 +97,7 @@ function getRenderExtent(frameState, extent) {
);
}
const source =
- /** {import("../../source/Tile.js").default} */ layerState.layer.getSource();
+ /** {import("../../source/Tile.js").default} */ layerState.layer.getRenderSource();
if (!source.getWrapX()) {
const gridExtent = source
.getTileGridForProjection(frameState.viewState.projection)
@@ -110,6 +109,10 @@ function getRenderExtent(frameState, extent) {
return extent;
}
+function getCacheKey(source, tileCoord) {
+ return `${source.getKey()},${getTileCoordKey(tileCoord)}`;
+}
+
/**
* @typedef {Object} Options
* @property {string} vertexShader Vertex shader source.
@@ -140,6 +143,12 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
uniforms: options.uniforms,
});
+ /**
+ * The last call to `renderFrame` was completed with all tiles loaded
+ * @type {boolean}
+ */
+ this.renderComplete = false;
+
/**
* This transform converts tile i, j coordinates to screen coordinates.
* @type {import("../../transform.js").Transform}
@@ -273,7 +282,7 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
*/
prepareFrameInternal(frameState) {
const layer = this.getLayer();
- const source = layer.getSource();
+ const source = layer.getRenderSource();
if (!source) {
return false;
}
@@ -293,7 +302,7 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
enqueueTiles(frameState, extent, z, tileTexturesByZ) {
const viewState = frameState.viewState;
const tileLayer = this.getLayer();
- const tileSource = tileLayer.getSource();
+ const tileSource = tileLayer.getRenderSource();
const tileGrid = tileSource.getTileGridForProjection(viewState.projection);
const tileTextureCache = this.tileTextureCache_;
const tileRange = tileGrid.getTileRangeForExtentAndZ(
@@ -314,7 +323,7 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
for (let x = tileRange.minX; x <= tileRange.maxX; ++x) {
for (let y = tileRange.minY; y <= tileRange.maxY; ++y) {
const tileCoord = createTileCoord(z, x, y, this.tempTileCoord_);
- const tileCoordKey = getTileCoordKey(tileCoord);
+ const cacheKey = getCacheKey(tileSource, tileCoord);
/**
* @type {TileTexture}
@@ -326,8 +335,8 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
*/
let tile;
- if (tileTextureCache.containsKey(tileCoordKey)) {
- tileTexture = tileTextureCache.get(tileCoordKey);
+ if (tileTextureCache.containsKey(cacheKey)) {
+ tileTexture = tileTextureCache.get(cacheKey);
tile = tileTexture.tile;
}
if (!tileTexture || tileTexture.tile.key !== tileSource.getKey()) {
@@ -340,7 +349,7 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
);
if (!tileTexture) {
tileTexture = new TileTexture(tile, tileGrid, this.helper);
- tileTextureCache.set(tileCoordKey, tileTexture);
+ tileTextureCache.set(cacheKey, tileTexture);
} else {
if (this.isDrawableTile_(tile)) {
tileTexture.setTile(tile);
@@ -379,12 +388,13 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
* @return {HTMLElement} The rendered element.
*/
renderFrame(frameState) {
+ this.renderComplete = true;
const gl = this.helper.getGL();
this.preRender(gl, frameState);
const viewState = frameState.viewState;
const tileLayer = this.getLayer();
- const tileSource = tileLayer.getSource();
+ const tileSource = tileLayer.getRenderSource();
const tileGrid = tileSource.getTileGridForProjection(viewState.projection);
const extent = getRenderExtent(frameState, frameState.extent);
const z = tileGrid.getZForResolution(
@@ -438,6 +448,7 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
const tileCoordKey = getTileCoordKey(tileCoord);
alphaLookup[tileCoordKey] = alpha;
}
+ this.renderComplete = false;
// first look for child tiles (at z + 1)
const coveredByChildren = this.findAltTiles_(
@@ -635,9 +646,10 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
let covered = true;
const tileTextureCache = this.tileTextureCache_;
+ const source = this.getLayer().getRenderSource();
for (let x = tileRange.minX; x <= tileRange.maxX; ++x) {
for (let y = tileRange.minY; y <= tileRange.maxY; ++y) {
- const cacheKey = getKeyZXY(altZ, x, y);
+ const cacheKey = getCacheKey(source, [altZ, x, y]);
let loaded = false;
if (tileTextureCache.containsKey(cacheKey)) {
const tileTexture = tileTextureCache.get(cacheKey);
diff --git a/src/ol/source.js b/src/ol/source.js
index c1efbce95f..3939a2c4f5 100644
--- a/src/ol/source.js
+++ b/src/ol/source.js
@@ -2,6 +2,9 @@
* @module ol/source
*/
+import LRUCache from './structs/LRUCache.js';
+import {getIntersection} from './extent.js';
+
export {default as BingMaps} from './source/BingMaps.js';
export {default as CartoDB} from './source/CartoDB.js';
export {default as Cluster} from './source/Cluster.js';
@@ -31,3 +34,35 @@ export {default as VectorTile} from './source/VectorTile.js';
export {default as WMTS} from './source/WMTS.js';
export {default as XYZ} from './source/XYZ.js';
export {default as Zoomify} from './source/Zoomify.js';
+
+/**
+ * Creates a sources function from a tile grid. This function can be used as value for the
+ * `sources` property of the {@link module:ol/layer/Layer~Layer} subclasses that support it.
+ * @param {import("./tilegrid/TileGrid.js").default} tileGrid Tile grid.
+ * @param {function(import("./tilecoord.js").TileCoord): import("./source/Source.js").default} factory Source factory.
+ * This function takes a {@link module:ol/tilecoord~TileCoord} as argument and is expected to return a
+ * {@link module:ol/source/Source~Source}.
+ * @return {function(import("./extent.js").Extent, number): Array} Sources function.
+ * @api
+ */
+export function sourcesFromTileGrid(tileGrid, factory) {
+ const sourceCache = new LRUCache(32);
+ const tileGridExtent = tileGrid.getExtent();
+ return function (extent, resolution) {
+ sourceCache.expireCache();
+ if (tileGridExtent) {
+ extent = getIntersection(tileGridExtent, extent);
+ }
+ const z = tileGrid.getZForResolution(resolution);
+ const wantedSources = [];
+ tileGrid.forEachTileCoord(extent, z, (tileCoord) => {
+ const key = tileCoord.toString();
+ if (!sourceCache.containsKey(key)) {
+ const source = factory(tileCoord);
+ sourceCache.set(key, source);
+ }
+ wantedSources.push(sourceCache.get(key));
+ });
+ return wantedSources;
+ };
+}
diff --git a/src/ol/structs/LRUCache.js b/src/ol/structs/LRUCache.js
index af44ad6472..f8c7e01385 100644
--- a/src/ol/structs/LRUCache.js
+++ b/src/ol/structs/LRUCache.js
@@ -66,6 +66,16 @@ class LRUCache {
return this.highWaterMark > 0 && this.getCount() > this.highWaterMark;
}
+ /**
+ * Expire the cache.
+ * @param {!Object} [keep] Keys to keep. To be implemented by subclasses.
+ */
+ expireCache(keep) {
+ while (this.canExpireCache()) {
+ this.pop();
+ }
+ }
+
/**
* FIXME empty description for jsdoc
*/
diff --git a/test/browser/spec/ol/layer/Group.test.js b/test/browser/spec/ol/layer/Group.test.js
index c02a0fbff2..ba361f4f43 100644
--- a/test/browser/spec/ol/layer/Group.test.js
+++ b/test/browser/spec/ol/layer/Group.test.js
@@ -44,7 +44,6 @@ describe('ol/layer/Group', function () {
opacity: 1,
visible: true,
managed: true,
- sourceState: 'ready',
extent: undefined,
zIndex: undefined,
maxResolution: Infinity,
@@ -161,7 +160,6 @@ describe('ol/layer/Group', function () {
opacity: 0.5,
visible: false,
managed: true,
- sourceState: 'ready',
extent: undefined,
zIndex: 10,
maxResolution: 500,
@@ -203,7 +201,6 @@ describe('ol/layer/Group', function () {
opacity: 0.5,
visible: false,
managed: true,
- sourceState: 'ready',
extent: groupExtent,
zIndex: undefined,
maxResolution: 500,
@@ -399,7 +396,6 @@ describe('ol/layer/Group', function () {
opacity: 0.3,
visible: false,
managed: true,
- sourceState: 'ready',
extent: groupExtent,
zIndex: 10,
maxResolution: 500,
@@ -417,7 +413,6 @@ describe('ol/layer/Group', function () {
opacity: 0,
visible: false,
managed: true,
- sourceState: 'ready',
extent: undefined,
zIndex: undefined,
maxResolution: Infinity,
@@ -433,7 +428,6 @@ describe('ol/layer/Group', function () {
opacity: 1,
visible: true,
managed: true,
- sourceState: 'ready',
extent: undefined,
zIndex: undefined,
maxResolution: Infinity,
@@ -599,7 +593,6 @@ describe('ol/layer/Group', function () {
opacity: 0.25,
visible: false,
managed: true,
- sourceState: 'ready',
extent: undefined,
zIndex: undefined,
maxResolution: 150,
diff --git a/test/browser/spec/ol/layer/Layer.test.js b/test/browser/spec/ol/layer/Layer.test.js
index 03cf12f1e2..568c77665e 100644
--- a/test/browser/spec/ol/layer/Layer.test.js
+++ b/test/browser/spec/ol/layer/Layer.test.js
@@ -56,7 +56,6 @@ describe('ol/layer/Layer', function () {
opacity: 1,
visible: true,
managed: true,
- sourceState: 'ready',
extent: undefined,
zIndex: undefined,
maxResolution: Infinity,
@@ -95,7 +94,6 @@ describe('ol/layer/Layer', function () {
opacity: 0.5,
visible: false,
managed: true,
- sourceState: 'ready',
extent: undefined,
zIndex: 10,
maxResolution: 500,
@@ -430,7 +428,6 @@ describe('ol/layer/Layer', function () {
opacity: 0.33,
visible: false,
managed: true,
- sourceState: 'ready',
extent: undefined,
zIndex: 10,
maxResolution: 500,
diff --git a/test/browser/spec/ol/layer/WebGLTile.test.js b/test/browser/spec/ol/layer/WebGLTile.test.js
index cd90407c0f..6d6efa7d2d 100644
--- a/test/browser/spec/ol/layer/WebGLTile.test.js
+++ b/test/browser/spec/ol/layer/WebGLTile.test.js
@@ -373,4 +373,37 @@ describe('ol/layer/WebGLTile', function () {
done();
});
});
+
+ it('handles multiple sources correctly', () => {
+ const source = layer.getSource();
+ expect(layer.getRenderSource()).to.be(source);
+ layer.sources_ = (extent, resolution) => {
+ return [
+ {
+ getState: () => 'ready',
+ extent,
+ resolution,
+ id: 'source1',
+ },
+ {
+ getState: () => 'ready',
+ extent,
+ resolution,
+ id: 'source2',
+ },
+ ];
+ };
+ const sourceIds = [];
+ layer.getRenderer().prepareFrame = (frameState) => {
+ const renderedSource = layer.getRenderSource();
+ expect(renderedSource.extent).to.eql([0, 0, 100, 100]);
+ expect(renderedSource.resolution).to.be(1);
+ sourceIds.push(renderedSource.id);
+ };
+ layer.render({
+ extent: [0, 0, 100, 100],
+ viewState: {resolution: 1},
+ });
+ expect(sourceIds).to.eql(['source1', 'source2']);
+ });
});
diff --git a/test/browser/spec/ol/source.test.js b/test/browser/spec/ol/source.test.js
new file mode 100644
index 0000000000..475ef4aa0c
--- /dev/null
+++ b/test/browser/spec/ol/source.test.js
@@ -0,0 +1,41 @@
+import TileGrid from '../../../../src/ol/tilegrid/TileGrid.js';
+import XYZ from '../../../../src/ol/source/XYZ.js';
+import {createXYZ} from '../../../../src/ol/tilegrid.js';
+import {get} from '../../../../src/ol/proj.js';
+import {sourcesFromTileGrid} from '../../../../src/ol/source.js';
+
+describe('ol/source', function () {
+ describe('sourcesFromTileGrid()', function () {
+ it('returns a function that returns the correct source', function () {
+ const resolutions = createXYZ({maxZoom: 1}).getResolutions();
+ const tileGrid = new TileGrid({
+ extent: get('EPSG:3857').getExtent(),
+ resolutions: [resolutions[1]],
+ tileSizes: [[256, 512]],
+ });
+ const factory = function (tileCoord) {
+ return new XYZ({
+ url: tileCoord.join('-') + '/{z}/{x}/{y}.png',
+ tileGrid: new TileGrid({
+ resolutions,
+ minZoom: tileCoord[0],
+ maxZoom: tileCoord[0] + 1,
+ extent: tileGrid.getTileCoordExtent(tileCoord),
+ origin: [-20037508.342789244, 20037508.342789244],
+ }),
+ });
+ };
+ const getSources = sourcesFromTileGrid(tileGrid, factory);
+ expect(getSources(tileGrid.getExtent(), resolutions[1]).length).to.be(2);
+ expect(
+ getSources(
+ [-10000, -10000, -5000, 10000],
+ resolutions[1]
+ )[0].getUrls()[0]
+ ).to.be('0-0-0/{z}/{x}/{y}.png');
+ expect(
+ getSources([5000, -10000, 10000, 10000], resolutions[1])[0].getUrls()[0]
+ ).to.be('0-1-0/{z}/{x}/{y}.png');
+ });
+ });
+});
diff --git a/test/rendering/cases/webgl-tile-multisource/expected.png b/test/rendering/cases/webgl-tile-multisource/expected.png
new file mode 100644
index 0000000000..fb2abb6c7f
Binary files /dev/null and b/test/rendering/cases/webgl-tile-multisource/expected.png differ
diff --git a/test/rendering/cases/webgl-tile-multisource/main.js b/test/rendering/cases/webgl-tile-multisource/main.js
new file mode 100644
index 0000000000..67eef5aa3e
--- /dev/null
+++ b/test/rendering/cases/webgl-tile-multisource/main.js
@@ -0,0 +1,58 @@
+import Map from '../../../../src/ol/Map.js';
+import TileGrid from '../../../../src/ol/tilegrid/TileGrid.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 {createXYZ} from '../../../../src/ol/tilegrid.js';
+import {get} from '../../../../src/ol/proj.js';
+import {sourcesFromTileGrid} from '../../../../src/ol/source.js';
+
+const resolutions = createXYZ({maxZoom: 1}).getResolutions();
+const tilePyramid = new TileGrid({
+ extent: get('EPSG:3857').getExtent(),
+ resolutions: [resolutions[1]],
+ tileSizes: [[256, 512]],
+});
+
+new Map({
+ target: 'map',
+ layers: [
+ new TileLayer({
+ sources: sourcesFromTileGrid(tilePyramid, (tileCoord) => {
+ let source;
+ switch (tileCoord.toString()) {
+ case '0,1,0':
+ source = new XYZ({
+ url: '/data/tiles/osm/{z}/{x}/{y}.png',
+ tileGrid: new TileGrid({
+ resolutions,
+ minZoom: tileCoord[0],
+ maxZoom: tileCoord[0] + 1,
+ extent: tilePyramid.getTileCoordExtent(tileCoord),
+ origin: [-20037508.342789244, 20037508.342789244],
+ }),
+ });
+ break;
+ default:
+ source = new XYZ({
+ url: '/data/tiles/satellite/{z}/{x}/{y}.jpg',
+ tileGrid: new TileGrid({
+ resolutions,
+ minZoom: tileCoord[0],
+ maxZoom: tileCoord[0] + 1,
+ extent: tilePyramid.getTileCoordExtent(tileCoord),
+ origin: [-20037508.342789244, 20037508.342789244],
+ }),
+ });
+ }
+ return source;
+ }),
+ }),
+ ],
+ view: new View({
+ center: [0, 0],
+ zoom: 1,
+ }),
+});
+
+render();
diff --git a/test/rendering/data/tiles/osm/1/0/0.png b/test/rendering/data/tiles/osm/1/0/0.png
new file mode 100644
index 0000000000..4e10306aa8
Binary files /dev/null and b/test/rendering/data/tiles/osm/1/0/0.png differ
diff --git a/test/rendering/data/tiles/osm/1/0/1.png b/test/rendering/data/tiles/osm/1/0/1.png
new file mode 100644
index 0000000000..42792c5dde
Binary files /dev/null and b/test/rendering/data/tiles/osm/1/0/1.png differ
diff --git a/test/rendering/data/tiles/osm/1/1/0.png b/test/rendering/data/tiles/osm/1/1/0.png
new file mode 100644
index 0000000000..0252154d99
Binary files /dev/null and b/test/rendering/data/tiles/osm/1/1/0.png differ
diff --git a/test/rendering/data/tiles/osm/1/1/1.png b/test/rendering/data/tiles/osm/1/1/1.png
new file mode 100644
index 0000000000..ade9495407
Binary files /dev/null and b/test/rendering/data/tiles/osm/1/1/1.png differ