import { Transform } from './Transform.js' import { Layout } from './Layout.js' import { Cache } from './Cache.js' import { BoundingBox } from './BoundingBox.js' import { addSignals } from './Signals.js' /** * @typedef {Object} LayerOptions * @property {string|Layout} [layout='image'] - Layout/format of input raster images * @property {string} [type] - Identifier for specific derived layer class * @property {string} [id] - Unique layer identifier * @property {string} [label] - Display label for UI (defaults to id) * @property {Transform} [transform] - Transform from layer to canvas coordinates * @property {boolean} [visible=true] - Whether layer should be rendered * @property {number} [zindex=0] - Stack order for rendering (higher = on top) * @property {boolean} [overlay=false] - Whether layer renders in overlay mode * @property {number} [prefetchBorder=1] - Tile prefetch threshold in tile units * @property {number} [mipmapBias=0.4] - Texture resolution selection bias (0=highest, 1=lowest) * @property {Object.<string, Shader>} [shaders] - Map of available shaders * @property {Controller[]} [controllers] - Array of active UI controllers * @property {Layer} [sourceLayer] - Layer to share tiles with * @property {number} [pixelSize=0.0] - Physical size of a pixel in mm */ /** * Layer is the core class for rendering content in OpenLIME. * It manages raster data display, tile loading, and shader-based rendering. * * Features: * - Tile-based rendering with level-of-detail * - Shader-based visualization effects * - Automatic tile prefetching and caching * - Coordinate system transformations * - Animation and interpolation of shader parameters * - Support for multiple visualization modes * - Integration with layout systems for different data formats * * Layers can be used directly or serve as a base class for specialized layer types. * The class uses a registration system where derived classes register themselves, * allowing instantiation through the 'type' option. * * @fires Layer#ready - Fired when layer is initialized * @fires Layer#update - Fired when redraw is needed * @fires Layer#loaded - Fired when all tiles are loaded * @fires Layer#updateSize - Fired when layer size changes * * @example * ```javascript * // Create a basic image layer * const layer = new OpenLIME.Layer({ * layout: 'deepzoom', * type: 'image', * url: 'path/to/image.dzi', * label: 'Main Image' * }); * * // Add to viewer * viewer.addLayer('main', layer); * * // Listen for events * layer.addEvent('ready', () => { * console.log('Layer initialized'); * }); * ``` */ class Layer { /** * Creates a Layer. Additionally, an object literal with Layer `options` can be specified. * Signals are triggered when: * ready: the size and layout of the layer is known * update: some new tile is available, or some visualization parameters has changed * loaded: is fired when all the images needed have been downloaded * @param {Object} [options] * @param {(string|Layout)} options.layout='image' The layout (the format of the input raster images). * @param {string} options.type A string identifier to select the specific derived layer class to instantiate. * @param {string} options.id The layer unique identifier. * @param {string} options.label A string with a more comprehensive definition of the layer. If it exists, it is used in the UI layer menu, otherwise the `id` value is taken. * @param {Transform} options.transform The relative coords from layer to canvas. * @param {bool} options.visible=true Whether to render the layer. * @param {number} options.zindex Stack ordering value for the rendering of layers (higher zindex on top). * @param {bool} options.overlay=false Whether the layer must be rendered in overlay mode. * @param {number} options.prefetchBorder=1 The threshold (in tile units) around the current camera position for which to prefetch tiles. * @param {number} options.mipmapBias=0.2 Determine which texture is used when scale is not a power of 2. 0: use always the highest resulution, 1 the lowest, 0.5 switch halfway. * @param {Object} options.shaders A map (shadersId, shader) of the shaders usable for the layer rendering. See @link {Shader}. * @param {Controller[]} options.controllers An array of UI device controllers active on the layer. * @param {Layer} options.sourceLayer The layer from which to take the tiles (in order to avoid tile duplication). */ constructor(options) { //create from derived class if type specified if (options.type) { let type = options.type; delete options.type; if (type in this.types) { return this.types[type](options); } throw "Layer type: " + type + " module has not been loaded"; } this.init(options); } /** @ignore */ init(options) { Object.assign(this, { transform: new Transform(), viewport: null, debug: false, visible: true, zindex: 0, overlay: false, //in the GUI it won't affect the visibility of the other layers rasters: [], layers: [], controls: {}, controllers: [], shaders: {}, layout: 'image', shader: null, //current shader. gl: null, width: 0, height: 0, prefetchBorder: 1, mipmapBias: 0.4, pixelSize: 0.0, //signals: { update: [], ready: [], updateSize: [] }, //update callbacks for a redraw, ready once layout is known. //internal stuff, should not be passed as options. tiles: new Map(), //keep references to each texture (and status) indexed by level, x and y. //each tile is tex: [.. one for raster ..], missing: 3 missing tex before tile is ready. //only raster used by the shader will be loade. queue: [], //queue of tiles to be loaded. requested: new Map, //tiles requested. }); Object.assign(this, options); if (this.sourceLayer) this.tiles = this.sourceLayer.tiles; //FIXME avoid tiles duplication this.transform = new Transform(this.transform); if (typeof (this.layout) == 'string') { let size = { width: this.width, height: this.height }; if (this.server) size.server = this.server; this.setLayout(new Layout(null, this.layout, size)); } else { this.setLayout(this.layout); } } /** * Sets the layer's viewport * @param {Object} view - Viewport specification * @param {number} view.x - X position * @param {number} view.y - Y position * @param {number} view.dx - Width * @param {number} view.dy - Height * @fires Layer#update */ setViewport(view) { this.viewport = view; this.emit('update'); } /** * Adds a filter to the current shader * @param {Object} filter - Filter specification * @throws {Error} If no shader is set */ addShaderFilter(f) { if (!this.shader) throw "Shader not implemented"; this.shader.addFilter(f); } /** * Removes a filter from the current shader * @param {Object} name - Filter name * @throws {Error} If no shader is set */ removeShaderFilter(name) { if (!this.shader) throw "Shader not implemented"; this.shader.removeFilter(name); } /** * Removes all filters from the current shader * @param {Object} name - Filter name * @throws {Error} If no shader is set */ clearShaderFilters() { if (!this.shader) throw "Shader not implemented"; this.shader.clearFilters(); } /** * Sets the layer state with optional animation * @param {Object} state - State object with controls and mode * @param {number} [dt] - Animation duration in ms * @param {string} [easing='linear'] - Easing function ('linear'|'ease-out'|'ease-in-out') */ setState(state, dt, easing = 'linear') { if ('controls' in state) for (const [key, v] of Object.entries(state.controls)) { this.setControl(key, v, dt, easing); } if ('mode' in state && state.mode) { this.setMode(state.mode); } } /** * Gets the current layer state * @param {Object} [stateMask] - Optional mask to filter returned state properties * @returns {Object} Current state object */ getState(stateMask = null) { const state = {}; state.controls = {}; for (const [key, v] of Object.entries(this.controls)) { if (!stateMask || ('controls' in stateMask && key in stateMask.controls)) state.controls[key] = v.current.value; } if (!stateMask || 'mode' in stateMask) if (this.getMode()) state.mode = this.getMode(); return state; } /** @ignore */ setLayout(layout) { /** * The event is fired when a layer is initialized. * @event Layer#ready */ /** * The event is fired if a redraw is needed. * @event Layer#update */ this.layout = layout; let callback = () => { this.status = 'ready'; this.setupTiles(); //setup expect status to be ready! this.emit('ready'); this.emit('update'); }; if (layout.status == 'ready') //layout already initialized. callback(); else layout.addEvent('ready', callback); // Set signal to acknowledge change of bbox when it is known. Let this signal go up to canvas this.layout.addEvent('updateSize', () => { if (this.shader) this.shader.setTileSize(this.layout.getTileSize()); this.emit('updateSize'); }); } /** * Sets the layer's transform * @param {Transform} tx - New transform * @fires Layer#updateSize */ setTransform(tx) { //FIXME this.transform = tx; this.emit('updateSize'); } /** * Sets the active shader * @param {string} id - Shader identifier from registered shaders * @throws {Error} If shader ID is not found * @fires Layer#update */ setShader(id) { if (!id in this.shaders) throw "Unknown shader: " + id; this.shader = this.shaders[id]; this.setupTiles(); this.shader.addEvent('update', () => { this.emit('update'); }); } /** * Gets the current shader visualization mode * @returns {string|null} Current mode or null if no shader */ getMode() { if (this.shader) return this.shader.mode; return null; } /** * Gets available shader modes * @returns {string[]} Array of available modes */ getModes() { if (this.shader) return this.shader.modes; return []; } /** * Sets shader visualization mode * @param {string} mode - Mode to set * @fires Layer#update */ setMode(mode) { this.shader.setMode(mode); this.emit('update'); } /** * Sets layer visibility * @param {boolean} visible - Whether layer should be visible * @fires Layer#update */ setVisible(visible) { this.visible = visible; this.previouslyNeeded = null; this.emit('update'); } /** * Sets layer rendering order * @param {number} zindex - Stack order value * @fires Layer#update */ setZindex(zindex) { this.zindex = zindex; this.emit('update'); } /** * Computes minimum scale across layers * @param {Object.<string, Layer>} layers - Map of layers * @param {boolean} discardHidden - Whether to ignore hidden layers * @returns {number} Minimum scale value * @static */ static computeLayersMinScale(layers, discardHidden) { if (layers == undefined || layers == null) { console.log("ASKING SCALE INFO ON NO LAYERS"); return 1; } let layersScale = 1; for (let layer of Object.values(layers)) { if (!discardHidden || layer.visible) { let s = layer.scale(); layersScale = Math.min(layersScale, s); } } return layersScale; } /** * Gets layer scale * @returns {number} Current scale value */ scale() { // FIXME: this do not consider children layers return this.transform.z; } /** * Gets pixel size in millimeters * @returns {number} Size of one pixel in mm */ pixelSizePerMM() { return this.pixelSize * this.transform.z; } /** * Gets layer bounding box in scene coordinates * @returns {BoundingBox} Bounding box */ boundingBox() { // FIXME: this do not consider children layers // Take layout bbox let result = this.layout.boundingBox(); // Apply layer transform to bbox if (this.transform != null && this.transform != undefined) { result = this.transform.transformBox(result); } return result; } /** * Computes combined bounding box of multiple layers * @param {Object.<string, Layer>} layers - Map of layers * @param {boolean} discardHidden - Whether to ignore hidden layers * @returns {BoundingBox} Combined bounding box * @static */ static computeLayersBBox(layers, discardHidden) { if (layers == undefined || layers == null) { console.log("ASKING BBOX INFO ON NO LAYERS"); let emptyBox = new BoundingBox(); return emptyBox; } let layersBbox = new BoundingBox(); for (let layer of Object.values(layers)) { if ((!discardHidden || layer.visible) && layer.layout.width) { const bbox = layer.boundingBox(); layersBbox.mergeBox(bbox); } } return layersBbox; } /** * Gets the shader parameter control corresponding to `name` * @param {*} name The name of the control. * return {*} The control */ getControl(name) { let control = this.controls[name] ? this.controls[name] : null; if (control) { let now = performance.now(); this.interpolateControl(control, now); } return control; } /** * Adds a shader parameter control * @param {string} name - Control identifier * @param {*} value - Initial value * @throws {Error} If control already exists */ addControl(name, value) { if (this.controls[name]) throw new Error(`Control "$name" already exist!`); let now = performance.now(); this.controls[name] = { 'source': { 'value': value, 't': now }, 'target': { 'value': value, 't': now }, 'current': { 'value': value, 't': now }, 'easing': 'linear' }; } /** * Sets a shader control value with optional animation * @param {string} name - Control identifier * @param {*} value - New value * @param {number} [dt] - Animation duration in ms * @param {string} [easing='linear'] - Easing function * @fires Layer#update */ setControl(name, value, dt, easing = 'linear') { //When are created? let now = performance.now(); let control = this.controls[name]; this.interpolateControl(control, now); control.source.value = [...control.current.value]; control.source.t = now; control.target.value = [...value]; control.target.t = now + dt; control.easing = easing; this.emit('update'); } /** * Updates control interpolation * @returns {boolean} Whether all interpolations are complete */ interpolateControls() { let now = performance.now(); let done = true; for (let control of Object.values(this.controls)) done = this.interpolateControl(control, now) && done; return done; } /** @ignore */ interpolateControl(control, time) { let source = control.source; let target = control.target; let current = control.current; current.t = time; if (time < source.t) { current.value = [...source.value]; return false; } if (time > target.t - 0.0001) { let done = current.value.every((e, i) => e === target.value[i]); current.value = [...target.value]; return done; } let dt = (target.t - source.t); let tt = (time - source.t) / dt; switch (control.easing) { case 'ease-out': tt = 1 - Math.pow(1 - tt, 2); break; case 'ease-in-out': tt = tt < 0.5 ? 2 * tt * tt : 1 - Math.pow(-2 * tt + 2, 2) / 2; break; } let st = 1 - tt; current.value = []; for (let i = 0; i < source.value.length; i++) current.value[i] = (st * source.value[i] + tt * target.value[i]); return false; } ///////////// /// CACHE HANDLING & RENDERING /** @ignore */ dropTile(tile) { for (let i = 0; i < tile.tex.length; i++) { if (tile.tex[i]) { this.gl.deleteTexture(tile.tex[i]); } } this.tiles.delete(tile.index); } /** * Clears layer resources and resets state * @private */ clear() { this.ibuffer = this.vbuffer = null; Cache.flushLayer(this); this.tiles = new Map(); //TODO We need to drop these tile textures before clearing Map this.setupTiles(); this.queue = []; this.previouslyNeeded = false; } /* * Renders the layer */ /** @ignore */ draw(transform, viewport) { //exception for layout image where we still do not know the image size //how linear or srgb should be specified here. // gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE); if (this.status != 'ready')// || this.tiles.size == 0) return true; if (!this.shader) throw "Shader not specified!"; let done = this.interpolateControls(); let parent_viewport = viewport; if (this.viewport) { viewport = this.viewport; this.gl.viewport(viewport.x, viewport.y, viewport.dx, viewport.dy); } this.prepareWebGL(); // find which quads to draw and in case request for them let available = this.layout.available(viewport, transform, this.transform, 0, this.mipmapBias, this.tiles); transform = this.transform.compose(transform); let matrix = transform.projectionMatrix(viewport); this.gl.uniformMatrix4fv(this.shader.matrixlocation, this.gl.FALSE, matrix); this.updateAllTileBuffers(available); // bind filter textures let iSampler = this.shader.samplers.length; for (const f of this.shader.filters) { for (let i = 0; i < f.samplers.length; i++) { this.gl.uniform1i(f.samplers[i].location, iSampler); this.gl.activeTexture(this.gl.TEXTURE0 + iSampler); this.gl.bindTexture(this.gl.TEXTURE_2D, f.samplers[i].tex); iSampler++; } } let i = 0; for (let tile of Object.values(available)) { // if(tile.complete) this.drawTile(tile, i); ++i; } if (this.vieport) this.gl.viewport(parent_viewport.x, parent_viewport.y, parent_viewport.dx, parent_viewport.dy); return done; } /** @ignore */ drawTile(tile, index) { //let tiledata = this.tiles.get(tile.index); if (tile.missing != 0) throw "Attempt to draw tile still missing textures" //coords and texture buffers updated once for all tiles from main draw() call //bind textures let gl = this.gl; for (var i = 0; i < this.shader.samplers.length; i++) { let id = this.shader.samplers[i].id; gl.uniform1i(this.shader.samplers[i].location, i); gl.activeTexture(gl.TEXTURE0 + i); gl.bindTexture(gl.TEXTURE_2D, tile.tex[id]); } // for (var i = 0; i < this.shader.samplers.length; i++) { // let id = this.shader.samplers[i].id; // gl.uniform1i(this.shader.samplers[i].location, i); // gl.activeTexture(gl.TEXTURE0 + i); // gl.bindTexture(gl.TEXTURE_2D, tile.tex[id]); // } // FIXME - TO BE REMOVED? const byteOffset = this.getTileByteOffset(index); gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, byteOffset); } getTileByteOffset(index) { return index * 6 * 2; } /* given the full pyramid of needed tiles for a certain bounding box, * starts from the preferred levels and goes up in the hierarchy if a tile is missing. * complete is true if all of the 'brothers' in the hierarchy are loaded, * drawing incomplete tiles enhance the resolution early at the cost of some overdrawing and problems with opacity. */ /** @ignore */ /*toRender(needed) { let torender = {}; //array of minlevel, actual level, x, y (referred to minlevel) let brothers = {}; let minlevel = needed.level; let box = needed.pyramid[minlevel]; for (let y = box.yLow; y < box.yHigh; y++) { for (let x = box.xLow; x < box.xHigh; x++) { let level = minlevel; while (level >= 0) { let d = minlevel - level; let index = this.layout.index(level, x >> d, y >> d); if (this.tiles.has(index) && this.tiles.get(index).missing == 0) { torender[index] = this.tiles.get(index); //{ index: index, level: level, x: x >> d, y: y >> d, complete: true }; break; } else { let sx = (x >> (d + 1)) << 1; let sy = (y >> (d + 1)) << 1; brothers[this.layout.index(level, sx, sy)] = 1; brothers[this.layout.index(level, sx + 1, sy)] = 1; brothers[this.layout.index(level, sx + 1, sy + 1)] = 1; brothers[this.layout.index(level, sx, sy + 1)] = 1; } level--; } } } for (let index in brothers) { if (index in torender) torender[index].complete = false; } return torender; }*/ /** @ignore */ // Update tile vertex and texture coords. // Currently called by derived classes updateTileBuffers(coords, tcoords) { let gl = this.gl; //TODO to reduce the number of calls (probably not needed) we can join buffers, and just make one call per draw! (except the bufferData, which is per node) gl.bindBuffer(gl.ARRAY_BUFFER, this.vbuffer); gl.bufferData(gl.ARRAY_BUFFER, coords, gl.STATIC_DRAW); //FIXME this is not needed every time. gl.vertexAttribPointer(this.shader.coordattrib, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(this.shader.coordattrib); gl.bindBuffer(gl.ARRAY_BUFFER, this.tbuffer); gl.bufferData(gl.ARRAY_BUFFER, tcoords, gl.STATIC_DRAW); gl.vertexAttribPointer(this.shader.texattrib, 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(this.shader.texattrib); } /** @ignore */ // Update tile vertex and texture coords of all the tiles in a single VBO updateAllTileBuffers(tiles) { let gl = this.gl; //use this.tiles instead. let N = Object.values(tiles).length; if (N == 0) return; const szV = 12; const szT = 8; const szI = 6; const iBuffer = new Uint16Array(szI * N); const vBuffer = new Float32Array(szV * N); const tBuffer = new Float32Array(szT * N); let i = 0; for (let tile of Object.values(tiles)) { let c = this.layout.tileCoords(tile); vBuffer.set(c.coords, i * szV); tBuffer.set(c.tcoords, i * szT); const off = i * 4; tile.indexBufferByteOffset = 2 * i * szI; iBuffer.set([off + 3, off + 2, off + 1, off + 3, off + 1, off + 0], i * szI); ++i; } gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ibuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, iBuffer, gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, this.vbuffer); gl.bufferData(gl.ARRAY_BUFFER, vBuffer, gl.STATIC_DRAW); gl.vertexAttribPointer(this.shader.coordattrib, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(this.shader.coordattrib); gl.bindBuffer(gl.ARRAY_BUFFER, this.tbuffer); gl.bufferData(gl.ARRAY_BUFFER, tBuffer, gl.STATIC_DRAW); gl.vertexAttribPointer(this.shader.texattrib, 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(this.shader.texattrib); } /* * If layout is ready and shader is assigned, creates or update tiles to keep track of what is missing. */ /** @ignore */ setupTiles() { if (!this.shader || !this.layout || this.layout.status != 'ready') return; for (let tile of this.tiles) { tile.missing = this.shader.samplers.length; for (let sampler of this.shader.samplers) { if (tile.tex[sampler.id]) tile.missing--; } } } /** @ignore */ prepareWebGL() { let gl = this.gl; if (!this.ibuffer) { //this part might go into another function. this.ibuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ibuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([3, 2, 1, 3, 1, 0]), gl.STATIC_DRAW); this.vbuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.vbuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0]), gl.STATIC_DRAW); this.tbuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.tbuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 0, 1, 1, 1, 1, 0]), gl.STATIC_DRAW); } if (this.shader.needsUpdate) { this.shader.debug = this.debug; this.shader.createProgram(gl); } gl.useProgram(this.shader.program); this.shader.updateUniforms(gl); } /** @ignore */ sameNeeded(a, b) { if (a.level != b.level) return false; for (let p of ['xLow', 'xHigh', 'yLow', 'yHigh']) if (a.pyramid[a.level][p] != b.pyramid[a.level][p]) return false; return true; } /** * Initiates tile prefetching based on viewport * @param {Transform} transform - Current view transform * @param {Object} viewport - Current viewport * @private */ prefetch(transform, viewport) { if (this.viewport) viewport = this.viewport; if (this.layers.length != 0) { //combine layers for (let layer of this.layers) layer.prefetch(transform, viewport); } if (this.rasters.length == 0) return; if (this.status != 'ready') return; if (typeof (this.layout) != 'object') throw "AH!"; /*let needed = this.layout.needed(viewport, transform, this.prefetchBorder, this.mipmapBias, this.tiles); this.queue = []; let now = performance.now(); let missing = this.shader.samplers.length; for(let tile of needed) { if(tile.missing === null) tile.missing = missing; if (tile.missing != 0 && !this.requested[index]) tmp.push(tile); } */ this.queue = this.layout.needed(viewport, transform, this.transform, this.prefetchBorder, this.mipmapBias, this.tiles); /* let needed = this.layout.neededBox(viewport, transform, this.prefetchBorder, this.mipmapBias); if (this.previouslyNeeded && this.sameNeeded(this.previouslyNeeded, needed)) return; this.previouslyNeeded = needed; this.queue = []; let now = performance.now(); //look for needed nodes and prefetched nodes (on the pos destination let missing = this.shader.samplers.length; for (let level = 0; level <= needed.level; level++) { let box = needed.pyramid[level]; let tmp = []; for (let y = box.yLow; y < box.yHigh; y++) { for (let x = box.xLow; x < box.xHigh; x++) { let index = this.layout.index(level, x, y); let tile = this.tiles.get(index) || { index, x, y, missing, tex: [], level }; tile.time = now; tile.priority = needed.level - level; if (tile.missing != 0 && !this.requested[index]) tmp.push(tile); } } let c = box.center(); //sort tiles by distance to the center TODO: check it's correct! tmp.sort(function (a, b) { return Math.abs(a.x - c[0]) + Math.abs(a.y - c[1]) - Math.abs(b.x - c[0]) - Math.abs(b.y - c[1]); }); this.queue = this.queue.concat(tmp); }*/ Cache.setCandidates(this); } /** * Loads a specific tile * @param {Object} tile - Tile specification * @param {Function} callback - Completion callback * @returns {Promise<void>} * @private */ async loadTile(tile, callback) { if (this.tiles.has(tile.index)) throw "AAARRGGHHH double tile!"; if (this.requested.has(tile.index)) { console.log("Warning: double request!"); callback("Double tile request"); return; } this.tiles.set(tile.index, tile); this.requested.set(tile.index, true); if (this.layout.type == 'itarzoom') { tile.url = this.layout.getTileURL(null, tile); let options = {}; if (tile.end) options.headers = { range: `bytes=${tile.start}-${tile.end}`, 'Accept-Encoding': 'indentity' } var response = await fetch(tile.url, options); if (!response.ok) { callback("Failed loading " + tile.url + ": " + response.statusText); return; } let blob = await response.blob(); let i = 0; for (let sampler of this.shader.samplers) { let raster = this.rasters[sampler.id]; let imgblob = blob.slice(tile.offsets[i], tile.offsets[i + 1]); const img = await raster.blobToImage(imgblob, this.gl); let tex = raster.loadTexture(this.gl, img); let size = img.width * img.height * 3; tile.size += size; tile.tex[sampler.id] = tex; tile.w = img.width; tile.h = img.height; i++; } tile.missing = 0; this.emit('update'); this.requested.delete(tile.index); if (callback) callback(tile.size); return; } tile.missing = this.shader.samplers.length; for (let sampler of this.shader.samplers) { let raster = this.rasters[sampler.id]; tile.url = this.layout.getTileURL(sampler.id, tile); const [tex, size] = await raster.loadImage(tile, this.gl); // TODO Parallelize request and url must be a parameter (implement request ques per url) if (this.layout.type == "image") { this.layout.width = raster.width; this.layout.height = raster.height; this.layout.emit('updateSize'); } tile.size += size; tile.tex[sampler.id] = tex; tile.missing--; if (tile.missing <= 0) { this.emit('update'); this.requested.delete(tile.index); if (this.requested.size == 0) this.emit('loaded'); if (callback) callback(size); } } } } Layer.prototype.types = {} addSignals(Layer, 'ready', 'update', 'loaded', 'updateSize'); export { Layer }