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