Source: /modules/webglwebGLToOSDBridge.js

/**
* Bridge between WebGLModule and OSD. Registers appropriate callbacks.
* Written by Jiří Horák, 2021
*
* Originally based on OpenSeadragonGL plugin, but you would find little similarities by now.
* NOTE: imagePixelSizeOnScreen needs to be assigned if custom OSD used... not very clean design
* @class OpenSeadragon.BridgeGL
*/
window.OpenSeadragon.BridgeGL = class {

    constructor(openSeaDragonInstance, webGLEngine, cachedMode=true) {
        let _this  = this;
        this.openSD = openSeaDragonInstance;

        const originalReset = webGLEngine.resetCallback;
        webGLEngine.resetCallback = _ => {
            _this.clear();
            _this.redraw();
            originalReset();
        };
        this._disabled = true; //so that first enable call is executed
        this.webGLEngine = webGLEngine;
        this._refreshTimeStamp = Date.now();
        this._randomDelay = 0;

        if (!cachedMode) {
            this.uid = OpenSeadragon.BridgeGL.getUniqueId();
        }
        //todo probably bad implementation, weakmap does not count reference for _KEYS_
        this._rendering = new WeakMap();
        this.imageData = undefined;
    }

    static getUniqueId() {
        return (Date.now()).toString(36);
    }

    get isMultiplex() {
        return false;
    }

    /**
     * Add OSD World Item index of a TiledImage that is being post-processed
     * @param idx index
     */
    addLayer(idx) {
        let existing = this._rendering[idx];
        let tiledImage = this.openSD.world.getItemAt(idx);
        if (!tiledImage) throw "Invalid index: no Tiled Image with index " + idx;
        if (existing) {
            if (existing == tiledImage) return;
            else this.removeLayer(idx);
        }
        this._rendering[idx] = tiledImage;

        if (!this.uid) {
            //enable on the source by overriding its member functions
            this._bindToTiledImage(idx);
        } else {
            tiledImage.source._bridgeId = this.uid;
            tiledImage.source.__cached_hasTransparency = tiledImage.source.hasTransparency;
            tiledImage.source.hasTransparency = function(context2D, url, ajaxHeaders, post) {
                return true; //we always render transparent
            }
        }
    }

    /**
     * Remove TiledImage by it's index
     * @param idx index of the tiled image to remove
     */
    removeLayer(idx) {
        if (!this.uid) {
            const source = this._unbindFromTiledSource(idx);
            if (!source) {
                console.warn("Could not properly remove bindings on TiledImage index", idx);
            }
        } else {
            let source = this._rendering[idx];
            if (!source) {
                console.warn("Could not properly remove bindings on TiledImage index", idx);
            } else {
                delete tiledImage.source._bridgeId;
                source.hasTransparency = source.__cached_hasTransparency;
                delete source.__cached_hasTransparency;
            }
        }
        delete this._rendering[idx];
    }

    /**
     * Enable binding of the renderer to OSD
     */
    enable() {
        //this is just a placeholder, set on init(...)
    }

    /**
     * Disable binding of the renderer to OSD
     */
    disable() {
        //this is just a placeholder, set on init(...)
    }

    /**
     * Check whether rendering is running
     * @return {boolean} true if binding is active
     */
    disabled() {
        return this._disabled;
    }

    /**
     * Get Tiled Image instance that is being bound to the post-processing engine
     * @param osdItemWorldIndex index of the image, or nothing - in that case first tiled image is returned
     * @return {undefined|OpenSeadragon.TiledImage} tiled image
     */
    getTiledImage(osdItemWorldIndex=-1) {
        if (osdItemWorldIndex < 0) {
            for (let key in this._rendering) {
                if (this._rendering[key]) return this._rendering[key];
            }
            return undefined;
        }
        return this._rendering[osdItemWorldIndex];
    }

    getWorldIndex() {
        for (let key in this._rendering) {
            if (this._rendering[key]) return key;
        }
        return -1;
    }

    /**
     * Runs a callback on each visualization goal
     * @param {function} call callback to perform on each visualization goal (its object given as the only parameter)
     */
    foreachVisualization(call) {
        this.webGLEngine.foreachVisualization(call);
    }

    /**
     * Get a visualizaiton goal object
     * @returns {object} a visualizaiton goal object
     */
    visualization(index=undefined) {
        return this.webGLEngine.visualization(index === undefined ? this.webGLEngine.currentVisualizationIndex() : index);
    }

    /**
     * Get the current visualizaiton goal index
     * @returns {number} current visualizaiton goal index
     */
    currentVisualizationIndex() {
        return this.webGLEngine.currentVisualizationIndex();
    }

    /**
     * Set program shaders. Just forwards the call to webGLEngine, for easier access.
     * @param {object} visualization - objects that define the visualization (see Readme)
     * @return {boolean} true if loaded successfully
     */
    addVisualization(...visualization) {
        if (this.webGLEngine.isPrepared) {
            console.warn("Invalid action: visualizations have been already loaded.");
            return false;
        }
        return this.webGLEngine.addVisualization(...visualization);
    }

    /**
     * Set program data.
     * @param {string} data - objects that define the visualization (see Readme)
     * @return {boolean} true if loaded successfully
     */
    setData(...data) {
        if (this.webGLEngine.isPrepared) {
            console.warn("Invalid action: visualizations have been already loaded.");
            return false;
        }
        this.imageData = data.length === 0 ? undefined : data;
        return true;
    }

    /**
     * Change visualization in use
     * @param {number} visIdx index of the visualization
     */
    switchVisualization(visIdx) {
        this.webGLEngine.switchVisualization(visIdx);
    }

    /**
     * Make ViaWebGL prepare visualizations,
     * called inside init() if not called manually before
     * (sometimes it is good to start ASAP - more time to load before OSD starts drawing)
     */
    loadShaders(activeVisualizationIdx=0, onPrepared=function(){}) {
        if (this.webGLEngine.isPrepared) {
            onPrepared();
            return;
        }
        this.webGLEngine.prepare(this.imageData, onPrepared, activeVisualizationIdx);
    }

    /**
     * Reorder shader: will re-generate current visualization from dynamic data obtained from webGLEngine.shaderGenerator
     * @param {array} order array of strings that refer to ID's in the visualization
     *   data (e.g. pyramidal tiff paths in our case), first is rendered last (top)
     */
    reorder(order=undefined) {
        if (!Array.isArray(order)) {
            this.webGLEngine.rebuildVisualization(null);
        } else {
            //webGLEngine rendering is first in order: first drawn, last in order: last drawn (atop)
            this.webGLEngine.rebuildVisualization(order.reverse());
        }
        this.redraw();
    }

    /**
     * Get current timestamp
     * @return {number}
     */
    get timeStamp() {
        if (this._randomDelay < 1) return this._refreshTimeStamp;
        return Math.random() * this._randomDelay + this._refreshTimeStamp;
    }

    /**
     * Get next timestamp which is guaranteed to be higher than or equal to any
     * timestamp set to processed entity
     * @return {number}
     */
    get highestTimestamp() {
        return this._refreshTimeStamp + this._randomDelay + 10;
    }

    /**
     * Invalidate all post-processed results, does not update/draw viewport
     * @param {number} randomDelay - time in milliseconds, tile updates can randomly occur within randomDelay
     *   note: it is not guaranteed to be updated, e.g. if you need to have ALL finished after
     *   the time has elapsed, make sure to call draw() at the end (e.g. setTimer....)
     */
    invalidate(randomDelay=0) {
        // Raise tstamp to force redraw
        this._refreshTimeStamp = Date.now();
        this._randomDelay = Math.max(0, randomDelay);
    }

    /**
     * Clear the canvas - necessary for transparent items to render correctly
     *  - done automatically on shader render updates
     */
    clear() {
        this.openSD.drawer._clear();
        this.openSD.navigator.drawer._clear();
    }

    /**
     * Draw/update viewport, does not invalidate last post-processed results
     */
    draw() {
        //Necessary to clear if underlying image is hidden, todo: when refactoring, optimize this
        this.openSD.drawer._clear();
        this.openSD.navigator.drawer._clear();
        this.openSD.world.draw();
        this.openSD.navigator.world.draw();
    }

    /**
     * Redraw the scene to reflect the latest visualization changes.
     * @param {number} randomDelay - time in milliseconds, tile updates can randomly occur within randomDelay
     *   note: viewport canvas is not guaranteed to be updated, e.g. if you need to have ALL
     *   tiles updated after 'randomDelay', call draw() after the time has elapsed
     */
    redraw(randomDelay=0) {
        this.invalidate(randomDelay);
        this.draw();
    }

    /**
     * Get IDS of data sources to be fetched from the server at the time
     * @return {Array} array of keys from 'shaders' parameter of the current visualization goal
     */
    dataImageSources() {
        return this.webGLEngine.getSources();
    }

    /**
     * Get active shader index
     * @return {number}
     */
    activeShaderIndex() {
        return this.webGLEngine._program;
    }

    /**
     * Access to webGL context
     * @returns {WebGLRenderingContext|WebGLRenderingContext} context
     */
    GL() {
        return this.webGLEngine.gl;
    }

    /**
     * Initialize the bridge between OSD and WebGL rendering once 'open' event happens
     * unlike the WebGL's init() can (and should) be called immediately after preparation (loadShaders)
     * - awaits the OSD opening
     *
     * @param {function} layerLoaded callback on load
     * @param {number} withActiveIndex index of the visualization to load as first, default 0
     * @return {OpenSeadragon.BridgeGL}
     */
    initBeforeOpen(layerLoaded=()=>{}, withActiveIndex=0) {
        if (this.webGLEngine.isInitialized) return this;
        this._initSelf();

        let _this = this;
        let handler = function(e) {
            function init() {
                _this.webGLEngine.init();
                layerLoaded();
                _this.openSD.removeHandler('open', handler);
            }
            _this.loadShaders(withActiveIndex, init);
        };

        this.openSD.addHandler('open', handler);
        return this;
    }

    /**
     * Initialize the bridge between OSD and WebGL immediately, loadShaders(...) must be called manually in this case.
     * like the WebGL's init() must be called once WebGL.prepare() finished
     */
    initAfterOpen() {
        if (this.webGLEngine.isInitialized) return this;
        if (!this.webGLEngine.isPrepared) {
            throw "BridgeGL::loadShaders() must be called before using initAfterOpen!";
        }
        this._initSelf();
        this.webGLEngine.init();
        return this;
    }

    /**
     * Reset the WebGL module, so that different initialization can be performed
     */
    reset() {
        this.webGLEngine.reset();
    }

    //////////////////////////////////////////////////////////////////////////////
    ///////////// YOU PROBABLY DON'T WANT TO READ/CHANGE FUNCTIONS BELOW
    //////////////////////////////////////////////////////////////////////////////

    _initSelf() {
        //if not set manually
        if (!this.imagePixelSizeOnScreen) {
            //if we have OSD TOOLs to-be plugin (?), use it
            if (this.openSD.hasOwnProperty("tools")) {
                this.imagePixelSizeOnScreen =
                    this.openSD.scalebar.imagePixelSizeOnScreen.bind(this.openSD.scalebar);
            } else {
                //just some placeholder
                console.error("OpenSeadragon has no Tool extension with 'imagePixelSizeOnScreen' function and this " +
                    "function was not assigned to the bridge instance: pixel ratio difference will be always 1.");
                this.imagePixelSizeOnScreen = _ => 1;
            }
        }


        if (this._initBounded) return;
        this.loadShaders(0, this._initFinish.bind(this)); //just to be safe, should be already loaded at this time, consider throwing instead
    }

    _initFinish() {
        //This can be performed only once for now, mode of execution cannot be changed after init(...)
        this._initBounded = true;
        if (this.uid) { //not a cached version, uses events to evaluate on all tiles
            let tileLoaded = this._tileLoaded.bind(this);
            let tileDrawing = this._tileDrawing.bind(this);

            this.enable = function() {
                if (!this._disabled) return;
                this._disabled = false;
                this.openSD.addHandler('tile-drawing', tileDrawing);
                this.openSD.addHandler('tile-loaded', tileLoaded);
                this.openSD.navigator.addHandler('tile-drawing', tileDrawing);
                this.openSD.navigator.addHandler('tile-loaded', tileLoaded);
            };

            this.disable = function() {
                if (this._disabled) return;
                this._disabled = true;
                this.openSD.removeHandler('tile-drawing', tileDrawing);
                this.openSD.removeHandler('tile-loaded', tileLoaded);
                this.openSD.navigator.removeHandler('tile-drawing', tileDrawing);
                this.openSD.navigator.removeHandler('tile-loaded', tileLoaded);
            };
            this.enable();
        } else { //cached version, overrides the TileSource API to customize cache creation
            this.enable = function(index=-1) {
                if (!this._disabled) return;

                for (let idx in this._rendering) {
                    if (this._rendering[idx]) this._bindToTiledImage(idx);
                }
                this._disabled = false;
            };

            this.disable = function(index=-1) {
                if (this._disabled) return;

                for (let idx in this._rendering) {
                    if (this._rendering[idx]) this._unbindFromTiledSource(idx);
                }
                this._disabled = true;
            };
            this._disabled = false;
        }
    }

    /************** EVENT STRATEGY ******************/

    _tileLoaded(e) {
        if (! e.data) return;

        if (this.uid === e.tiledImage.source._bridgeId && !e.tile.webglId) {
            e.tile.webglId = this.uid;
            //will draw immediatelly
            e.tile.webglRefresh = 0;
            //we set context2D manually, the cache is NOT created
            e.tile.__data = e.data;
            //necessary, the tile is re-drawn upon re-zooming, store the output
            let canvas = document.createElement('canvas');
            canvas.width = e.tile.sourceBounds.width;
            canvas.height = e.tile.sourceBounds.height;
            e.tile.context2D = canvas.getContext('2d');
        }
    }

    _tileDrawing(e) {
        if (e.tile.webglId === this.uid && e.tile.webglRefresh <= this.timeStamp) {
            e.tile.webglRefresh = this.highestTimestamp;

            //noop if equal
            this.webGLEngine.setDimensions(e.tile.sourceBounds.width, e.tile.sourceBounds.height);
            let imageData = e.tile.__data;
            // Render a webGL canvas to an input canvas using cached version
            let output = this.webGLEngine.processImage(imageData, e.tile.sourceBounds,
                this.openSD.viewport.getZoom(), this.imagePixelSizeOnScreen());

            // Note: you can comment out clearing if you don't use transparency
            e.rendered.clearRect(0, 0, e.tile.sourceBounds.width, e.tile.sourceBounds.height);
            e.rendered.drawImage(output == null? imageData : output, 0, 0,
                e.tile.sourceBounds.width, e.tile.sourceBounds.height);
        }
    }

    /************** BINDING (CACHE) STRATEGY ******************/

    _bindToTiledImage(index) {
        let layer = this._rendering[index];
        const _context = this;
        let source = layer.source;

        //necessary to modify hash key so as to force the viewer download the image twice
        source.__cached_getTileHashKey = source.getTileHashKey;
        source.getTileHashKey = function(level, x, y, url, ajaxHeaders, postData) {
            return source.__cached_getTileHashKey(level, x, y, url, ajaxHeaders, postData) + "_webgl";
        };

        source.__cached_createTileCache = source.createTileCache;

        source.createTileCache = function(cache, data, tile) {
            cache._data = data;
            cache._dim = tile.sourceBounds;
            cache._dim.width = Math.max(cache._dim.width,1);
            cache._dim.height = Math.max(cache._dim.height,1);
        };

        source.__cached_destroyTileCache = source.destroyTileCache;
        source.destroyTileCache = function(cache) {
            delete cache._data;
            delete cache._dim;
            delete cache._renderedContext;
        };

        source.__cached_getTileCacheData = source.getTileCacheData;
        source.getTileCacheData = function(cache) {
            return cache._data;
        };

        source.__cached_hasTransparency = source.hasTransparency;
        source.hasTransparency = function(context2D, url, ajaxHeaders, post) {
            return true;
        };

        source.__cached_tileDataToIamge = source.getTileCacheDataAsImage;
        source.getTileCacheDataAsImage = function(cache) {
            throw "WebGL Postprocessing works only with canvasses for now!";
        };

        source.__cached_tileDataToRenderedContext = source.getTileCacheDataAsContext2D;
        source.getTileCacheDataAsContext2D = function(cache) {
            if (!cache._renderedContext) {
                cache.webglRefresh = 0;
                var canvas = document.createElement('canvas');
                canvas.width = cache._dim.width;
                canvas.height = cache._dim.height;
                cache._renderedContext = canvas.getContext('2d');
            }

            if (cache.webglRefresh <= _context.timeStamp) {
                cache.webglRefresh = _context.highestTimestamp;

                //noop if equal
                _context.webGLEngine.setDimensions(cache._dim.width, cache._dim.height);
                // Render a webGL canvas to an input canvas using cached version
                const output = _context.webGLEngine.processImage(cache._data, cache._dim,
                    _context.openSD.viewport.getZoom(), _context.imagePixelSizeOnScreen());

                // Note: you can comment out clearing if you don't use transparency
                cache._renderedContext.clearRect(0, 0, cache._dim.width, cache._dim.height);
                cache._renderedContext.drawImage(output == null ? cache._data : output, 0, 0,
                    cache._dim.width, cache._dim.height);
            }
            return cache._renderedContext;
        };
    }

    _unbindFromTiledSource(index) {
        let source = this._rendering[index];
        if (!source || !source.source) return;
        source = source.source;
        source.hasTransparency = source.__cached_hasTransparency;
        delete source.__cached_hasTransparency;
        source.createTileCache = source.__cached_createTileCache;
        delete source.__cached_createTileCache;
        source.destroyTileCache = source.__cached_destroyTileCache;
        delete source.__cached_destroyTileCache;
        source.getTileCacheData = source.__cached_getTileCacheData;
        delete source.__cached_getTileCacheData;
        source.getTileCacheDataAsImage = source.__cached_tileDataToIamge;
        delete source.__cached_tileDataToIamge;
        source.getTileCacheDataAsContext2D = source.__cached_tileDataToRenderedContext;
        delete source.__cached_tileDataToRenderedContext;
        source.getTileHashKey = source.__cached_getTileHashKey;
        delete source.__cached_getTileHashKey;
        return source;
    }
};

//
// window.OpenSeadragon.BridgeGLMultiplex = class {
//
//     constructor(openSeaDragonInstance, cachedMode=true) {
//         let _this  = this;
//         this.openSD = openSeaDragonInstance;
//
//         this._disabled = true; //so that first enable call is executed
//         this.webGLEngineList = [];
//         this._refreshTimeStampList = {};
//         this._randomDelayList = {};
//         this._visualizations = [];
//         this._activeIndex = 0;
//
//         if (!cachedMode) {
//             this.uid = OpenSeadragon.BridgeGL.getUniqueId();
//         }
//         this._rendering = new WeakMap();
//         this.imageData = undefined;
//     }
//
//     get webGLEngine() {
//         if (this.webGLEngineList.length < 1) return {};
//         return this.webGLEngineList[0];
//     }
//
//     get isMultiplex() {
//         return true;
//     }
//
//     /**
//      * Add OSD World Item index of a TiledImage that is being post-processed
//      * @param idx index
//      * @param webglOptions
//      */
//     addLayer(idx, webglOptions) {
//         let existing = this._rendering[idx];
//         let tiledImage = this.openSD.world.getItemAt(idx);
//         if (!tiledImage) throw "Invalid index: no Tiled Image with index " + idx;
//         if (existing) {
//             if (existing == tiledImage) return;
//             else this.removeLayer(idx);
//         }
//         this._rendering[idx] = tiledImage;
//
//         if (!this.webGLEngineList[idx]) {
//             this.webGLEngineList[idx] = new WebGLModule(webglOptions);
//         } else {
//             //todo bit dirty
//             $.extend(true, this.webGLEngineList[idx], webglOptions);
//             this.webGLEngineList[idx].reset();
//         }
//         this._refreshTimeStampList[idx] = Date.now();
//         this._randomDelayList[idx] = 0;
//
//         const engine = this.webGLEngineList[idx];
//         //add visualization shader
//         this._visualizations.forEach(vis => {
//             const newVis = {...vis, shaders: {}};
//             let entry = Object.entries(vis.shaders).find((e, i) => i === idx);
//             if (entry) newVis.shaders[entry[0]] = newVis.shaders[entry[1]];
//             engine.addVisualization(newVis);
//         });
//
//         if (!this.uid) {
//             //enable on the source by overriding its member functions
//             this._bindToTiledImage(idx);
//         } else {
//             tiledImage.source._bridgeId = this.uid;
//             tiledImage.source.__cached_hasTransparency = tiledImage.source.hasTransparency;
//             tiledImage.source.hasTransparency = function(context2D, url, ajaxHeaders, post) {
//                 return true; //we always render transparent
//             }
//             tiledImage.__bridgeIndex = idx;
//         }
//     }
//
//     /**
//      * Remove TiledImage by it's index
//      * @param idx index of the tiled image to remove
//      */
//     removeLayer(idx) {
//         if (!this.uid) {
//             const source = this._unbindFromTiledSource(idx);
//             if (!source) {
//                 console.warn("Could not properly remove bindings on TiledImage index", idx);
//             }
//         } else {
//             let source = this._rendering[idx];
//             if (!source) {
//                 console.warn("Could not properly remove bindings on TiledImage index", idx);
//             } else {
//                 delete tiledImage.source._bridgeId;
//                 source.hasTransparency = source.__cached_hasTransparency;
//                 delete source.__cached_hasTransparency;
//                 delete source.__bridgeIndex;
//             }
//         }
//         delete this._rendering[idx];
//         //do not delete other lists since we keep them for the next layer set session
//     }
//
//     /**
//      * Enable binding of the renderer to OSD
//      */
//     enable() {
//         //this is just a placeholder, set on init(...)
//     }
//
//     /**
//      * Disable binding of the renderer to OSD
//      */
//     disable() {
//         //this is just a placeholder, set on init(...)
//     }
//
//     /**
//      * Check whether rendering is running
//      * @return {boolean} true if binding is active
//      */
//     disabled() {
//         return this._disabled;
//     }
//
//     /**
//      * Get Tiled Image instance that is being bound to the post-processing engine
//      * @param osdItemWorldIndex index of the image, or nothing - in that case first tiled image is returned
//      * @return {undefined|OpenSeadragon.TiledImage} tiled image
//      *
//      * todo problematic, now we have multiple tile images --> what about for each tiled image
//      */
//     getTiledImage(osdItemWorldIndex=-1) {
//         if (osdItemWorldIndex < 0) {
//             for (let key in this._rendering) {
//                 if (this._rendering[key]) return this._rendering[key];
//             }
//             return undefined;
//         }
//         return this._rendering[osdItemWorldIndex];
//     }
//
//     getWorldIndex() {
//         for (let key in this._rendering) {
//             if (this._rendering[key]) return key;
//         }
//         return -1;
//     }
//
//     /**
//      * Runs a callback on each visualization goal
//      * @param {function} call callback to perform on each visualization goal (its object given as the only parameter)
//      */
//     foreachVisualization(call) {
//         for (let engine of this.webGLEngineList) {
//             if (engine?.isPrepared) {
//                 engine.foreachVisualization(call);
//             }
//         }
//     }
//
//     /**
//      * Get a visualizaiton goal object
//      * @returns {object} a visualizaiton goal object
//      */
//     visualization(index=undefined) {
//         return this._visualizations(index || this.webGLEngine.currentVisualizationIndex());
//     }
//
//     /**
//      * Get the current visualizaiton goal index
//      * @returns {number} current visualizaiton goal index
//      */
//     currentVisualizationIndex() {
//         //same for all engines
//         //topdo engines have just one
//         return this.webGLEngine.currentVisualizationIndex();
//     }
//
//     /**
//      * Set program shaders. Just forwards the call to webGLEngine, for easier access.
//      * @param {object} visualization - objects that define the visualization (see Readme)
//      * @return {boolean} true if loaded successfully
//      */
//     addVisualization(...visualization) {
//         if (this.webGLEngine) {
//             console.warn("Invalid action: visualizations can be attached before layers are added only.");
//             return false;
//         }
//         this._visualizations.push(...visualization);
//     }
//
//     /**
//      * Set program data.
//      * @param {string} data - objects that define the visualization (see Readme)
//      * @return {boolean} true if loaded successfully
//      */
//     setData(...data) {
//         if (this.webGLEngine.isPrepared) {
//             console.warn("Invalid action: visualizations have been already loaded.");
//             return false;
//         }
//         this.imageData = data.length === 0 ? undefined : data;
//         return true;
//     }
//
//     /**
//      * Change visualization in use
//      * @param {number} visIdx index of the visualization
//      */
//     switchVisualization(visIdx) {
//         //todo inactive renderers?
//
//         //todo need to re-change layers
//
//         // for (let key in this.webGLEngineList) {
//         //     this.webGLEngineList[key].switchVisualization(visIdx);
//         // }
//     }
//
//     /**
//      * Make ViaWebGL download and prepare visualizations,
//      * called inside init() if not called manually before
//      * (sometimes it is good to start ASAP - more time to load before OSD starts drawing)
//      */
//     loadShaders(activeVisualizationIdx=0, onPrepared=function(){}) {
//         if (this.webGLEngine.isPrepared) {
//             onPrepared();
//             return;
//         }
//         let guard = this.webGLEngineList.length;
//         for (let engine of this.webGLEngineList) {
//             engine.prepare(this.imageData, () => {
//                 guard--;
//                 if (guard < 1) onPrepared();
//             }, activeVisualizationIdx);
//         }
//     }
//
//
//
//
//
//
//
//
//
//     with(index) {
//         this._activeIndex = index;
//         return this;
//     }
//
//
//     /**
//      * Reorder shader: will re-generate current visualization from dynamic data obtained from webGLEngine.shaderGenerator
//      * @param {array} order array of strings that refer to ID's in the visualization
//      *   data (e.g. pyramidal tiff paths in our case), first is rendered last (top)
//      */
//     reorder(order=undefined) {
//         //todo support? reorder in viewer?
//         //not supported for now
//         // if (!Array.isArray(order)) {
//         //     this.webGLEngine.rebuildVisualization(null);
//         // } else {
//         //     //webGLEngine rendering is first in order: first drawn, last in order: last drawn (atop)
//         //     this.webGLEngine.rebuildVisualization(order.reverse());
//         // }
//         this.redraw();
//     }
//
//     /**
//      * Get current timestamp
//      * @return {number}
//      */
//     get timeStamp() {
//         if (this._randomDelayList[this._activeIndex] < 1) return this._refreshTimeStampList[this._activeIndex];
//         return Math.random() * this._randomDelayList[this._activeIndex] + this._refreshTimeStampList[this._activeIndex];
//     }
//
//     /**
//      * Get next timestamp which is guaranteed to be higher than or equal to any
//      * timestamp set to processed entity
//      * @return {number}
//      */
//     get highestTimestamp() {
//         return this._refreshTimeStampList[this._activeIndex] + this._randomDelayList[this._activeIndex] + 10;
//     }
//
//     /**
//      * Invalidate all post-processed results, does not update/draw viewport
//      * @param {number} randomDelay - time in milliseconds, tile updates can randomly occur within randomDelay
//      *   note: it is not guaranteed to be updated, e.g. if you need to have ALL finished after
//      *   the time has elapsed, make sure to call draw() at the end (e.g. setTimer....)
//      */
//     invalidate(randomDelay=0) {
//         // Raise tstamp to force redraw
//         this._refreshTimeStampList[this._activeIndex] = Date.now();
//         this._randomDelayList[this._activeIndex] = Math.max(0, randomDelay);
//     }
//
//     /**
//      * Clear the canvas - necessary for transparent items to render correctly
//      *  - done automatically on shader render updates
//      */
//     clear() {
//         this.openSD.drawer._clear();
//         this.openSD.navigator.drawer._clear();
//     }
//
//     /**
//      * Draw/update viewport, does not invalidate last post-processed results
//      */
//     draw() {
//         this.openSD.world.draw();
//         this.openSD.navigator.world.draw();
//     }
//
//     /**
//      * Redraw the scene to reflect the latest visualization changes.
//      * @param {number} randomDelay - time in milliseconds, tile updates can randomly occur within randomDelay
//      *   note: viewport canvas is not guaranteed to be updated, e.g. if you need to have ALL
//      *   tiles updated after 'randomDelay', call draw() after the time has elapsed
//      */
//     redraw(randomDelay=0, index=undefined) {
//         if (index === undefined) {
//             for (let i = 0; i < this.webGLEngineList.length; i++) {
//                 this.with(i).invalidate(randomDelay);
//             }
//         } else {
//             this.with(index).invalidate(randomDelay);
//         }
//         this.draw();
//     }
//
//     /**
//      * Get IDS of data sources to be fetched from the server at the time
//      * @return {Array} array of keys from 'shaders' parameter of the current visualization goal
//      */
//     dataImageSources() {
//         return this.webGLEngineList.reduce((acc, item) => {
//             acc.push(...item.getSources());
//         }, []);
//     }
//
//     /**
//      * Get active shader index
//      * @return {number}
//      */
//     activeShaderIndex() {
//         return this.webGLEngine._program;
//     }
//
//     /**
//      * Access to webGL context
//      * @returns {WebGLRenderingContext|WebGLRenderingContext} context
//      */
//     GL(index=undefined) {
//         if (index === undefined) return this.webGLEngine.gl;
//         return this.webGLEngineList[index].gl;
//     }
//
//     /**
//      * Initialize the bridge between OSD and WebGL rendering once 'open' event happens
//      * unlike the WebGL's init() can (and should) be called immediately after preparation (loadShaders)
//      * - awaits the OSD opening
//      *
//      * @param {function} layerLoaded callback on load
//      * @param {number} withActiveIndex index of the visualization to load as first, default 0
//      * @return {OpenSeadragon.BridgeGL}
//      */
//     initBeforeOpen(layerLoaded=()=>{}, withActiveIndex=0) {
//         if (this.webGLEngine.isInitialized) return this;
//         this._initSelf();
//
//         let _this = this;
//         let handler = function(e) {
//             function init() {
//                 _this.webGLEngineList.forEach(e => e.init());
//                 layerLoaded();
//                 _this.openSD.removeHandler('open', handler);
//             }
//             _this.loadShaders(withActiveIndex, init);
//         };
//
//         this.openSD.addHandler('open', handler);
//         return this;
//     }
//
//     /**
//      * Initialize the bridge between OSD and WebGL immediately, loadShaders(...) must be called manually in this case.
//      * like the WebGL's init() must be called once WebGL.prepare() finished
//      */
//     initAfterOpen() {
//         if (this.webGLEngine.isInitialized) return this;
//         if (!this.webGLEngine.isPrepared) {
//             throw "BridgeGL::loadShaders() must be called before using initAfterOpen!";
//         }
//         this._initSelf();
//         this.webGLEngineList.forEach(e => e.init());
//         return this;
//     }
//
//     /**
//      * Reset the WebGL module, so that different initialization can be performed
//      */
//     reset() {
//         this.webGLEngineList.forEach(e => e.reset());
//     }
//
//     //////////////////////////////////////////////////////////////////////////////
//     ///////////// YOU PROBABLY DON'T WANT TO READ/CHANGE FUNCTIONS BELOW
//     //////////////////////////////////////////////////////////////////////////////
//
//     _initSelf() {
//         //if not set manually
//         if (!this.imagePixelSizeOnScreen) {
//             //if we have OSD TOOLs to-be plugin (?), use it
//             if (this.openSD.hasOwnProperty("tools")) {
//                 this.imagePixelSizeOnScreen =
//                     this.openSD.scalebar.imagePixelSizeOnScreen.bind(this.openSD.scalebar);
//             } else {
//                 //just some placeholder
//                 console.error("OpenSeadragon has no Tool extension with 'imagePixelSizeOnScreen' function and this " +
//                     "function was not assigned to the bridge instance: pixel ratio difference will be always 1.");
//                 this.imagePixelSizeOnScreen = _ => 1;
//             }
//         }
//
//         if (this._initBounded) return;
//         this.loadShaders(0, this._initFinish.bind(this)); //just to be safe, should be already loaded at this time, consider throwing instead
//     }
//
//     _initFinish() {
//         //This can be performed only once for now, mode of execution cannot be changed after init(...)
//         this._initBounded = true;
//         if (this.uid) { //not a cached version, uses events to evaluate on all tiles
//             let tileLoaded = this._tileLoaded.bind(this);
//             let tileDrawing = this._tileDrawing.bind(this);
//
//             this.enable = function() {
//                 if (!this._disabled) return;
//                 this._disabled = false;
//                 this.openSD.addHandler('tile-drawing', tileDrawing);
//                 this.openSD.addHandler('tile-loaded', tileLoaded);
//                 this.openSD.navigator.addHandler('tile-drawing', tileDrawing);
//                 this.openSD.navigator.addHandler('tile-loaded', tileLoaded);
//             };
//
//             this.disable = function() {
//                 if (this._disabled) return;
//                 this._disabled = true;
//                 this.openSD.removeHandler('tile-drawing', tileDrawing);
//                 this.openSD.removeHandler('tile-loaded', tileLoaded);
//                 this.openSD.navigator.removeHandler('tile-drawing', tileDrawing);
//                 this.openSD.navigator.removeHandler('tile-loaded', tileLoaded);
//             };
//             this.enable();
//         } else { //cached version, overrides the TileSource API to customize cache creation
//             this.enable = function(index=-1) {
//                 if (!this._disabled) return;
//
//                 for (let idx in this._rendering) {
//                     if (this._rendering[idx]) this._bindToTiledImage(idx);
//                 }
//                 this._disabled = false;
//             };
//
//             this.disable = function(index=-1) {
//                 if (this._disabled) return;
//
//                 for (let idx in this._rendering) {
//                     if (this._rendering[idx]) this._unbindFromTiledSource(idx);
//                 }
//                 this._disabled = true;
//             };
//             this._disabled = false;
//         }
//     }
//
//     /************** EVENT STRATEGY ******************/
//
//     _tileLoaded(e) {
//         if (! e.data) return;
//
//         if (this.uid === e.tiledImage.source._bridgeId && !e.tile.webglId) {
//             e.tile.webglId = this.uid;
//             //will draw immediatelly
//             e.tile.webglRefresh = 0;
//             //we set context2D manually, the cache is NOT created
//             e.tile.__data = e.data;
//             //necessary, the tile is re-drawn upon re-zooming, store the output
//             let canvas = document.createElement('canvas');
//             canvas.width = e.tile.sourceBounds.width;
//             canvas.height = e.tile.sourceBounds.height;
//             e.tile.context2D = canvas.getContext('2d');
//         }
//     }
//
//     _tileDrawing(e) {
//         if (e.tile.webglId === this.uid && e.tile.webglRefresh <= this.with(index).timeStamp) {
//             e.tile.webglRefresh = this.with(index).highestTimestamp;
//
//             const idx = e.tiledImage.__bridgeIndex;
//             const engine = this.webGLEngineList[idx];
//
//             //noop if equal
//             engine.setDimensions(e.tile.sourceBounds.width, e.tile.sourceBounds.height);
//             let imageData = e.tile.__data;
//             // Render a webGL canvas to an input canvas using cached version
//             let output = engine.processImage(imageData, e.tile.sourceBounds,
//                 this.openSD.viewport.getZoom(), this.imagePixelSizeOnScreen());
//
//             // Note: you can comment out clearing if you don't use transparency
//             e.rendered.clearRect(0, 0, e.tile.sourceBounds.width, e.tile.sourceBounds.height);
//             e.rendered.drawImage(output == null? imageData : output, 0, 0,
//                 e.tile.sourceBounds.width, e.tile.sourceBounds.height);
//         }
//     }
//
//     /************** BINDING (CACHE) STRATEGY ******************/
//
//     _bindToTiledImage(index) {
//         const _context = this,
//             layer = this._rendering[index],
//             source = layer.source;
//
//         //necessary to modify hash key so as to force the viewer download the image twice
//         source.__cached_getTileHashKey = source.getTileHashKey;
//         source.getTileHashKey = function(level, x, y, url, ajaxHeaders, postData) {
//             return source.__cached_getTileHashKey(level, x, y, url, ajaxHeaders, postData) + "_webgl";
//         };
//
//         source.__cached_createTileCache = source.createTileCache;
//         source.createTileCache = function(cache, data, tile) {
//             cache._data = data;
//             cache._dim = tile.sourceBounds;
//             cache._dim.width = Math.max(cache._dim.width,1);
//             cache._dim.height = Math.max(cache._dim.height,1);
//             cache._engine = _context.webGLEngineList[index];
//         };
//
//         source.__cached_destroyTileCache = source.destroyTileCache;
//         source.destroyTileCache = function(cache) {
//             delete cache._data;
//             delete cache._engine;
//             delete cache._renderedContext;
//             delete cache._dim;
//         };
//
//         source.__cached_getTileCacheData = source.getTileCacheData;
//         source.getTileCacheData = function(cache) {
//             return cache._data;
//         };
//
//         source.__cached_hasTransparency = source.hasTransparency;
//         source.hasTransparency = function(context2D, url, ajaxHeaders, post) {
//             return true;
//         };
//
//         source.__cached_tileDataToIamge = source.getTileCacheDataAsImage;
//         source.getTileCacheDataAsImage = function(cache) {
//             throw "WebGL Postprocessing works only with canvasses for now!";
//         };
//
//         source.__cached_tileDataToRenderedContext = source.getTileCacheDataAsContext2D;
//         source.getTileCacheDataAsContext2D = function(cache) {
//             if (!cache._renderedContext) {
//                 cache.webglRefresh = 0;
//                 var canvas = document.createElement('canvas');
//                 canvas.width = cache._dim.width;
//                 canvas.height = cache._dim.height;
//                 cache._renderedContext = canvas.getContext('2d');
//             }
//
//             if (cache.webglRefresh <= _context.with(index).timeStamp) {
//                 cache.webglRefresh = _context.with(index).highestTimestamp;
//
//
//                 //noop if equal
//                 cache._engine.setDimensions(cache._dim.width, cache._dim.height);
//                 // Render a webGL canvas to an input canvas using cached version
//                 const output = cache._engine.processImage(cache._data, cache._dim,
//                     _context.openSD.viewport.getZoom(), _context.imagePixelSizeOnScreen());
//
//                 // Note: you can comment out clearing if you don't use transparency
//                 cache._renderedContext.clearRect(0, 0, cache._dim.width, cache._dim.height);
//                 cache._renderedContext.drawImage(output == null ? cache._data : output, 0, 0,
//                     cache._dim.width, cache._dim.height);
//             }
//             return cache._renderedContext;
//         };
//     }
//
//     _unbindFromTiledSource(index) {
//         let source = this._rendering[index];
//         if (!source || !source.source) return;
//         source = source.source;
//         source.hasTransparency = source.__cached_hasTransparency;
//         delete source.__cached_hasTransparency;
//         source.createTileCache = source.__cached_createTileCache;
//         delete source.__cached_createTileCache;
//         source.destroyTileCache = source.__cached_destroyTileCache;
//         delete source.__cached_destroyTileCache;
//         source.getTileCacheData = source.__cached_getTileCacheData;
//         delete source.__cached_getTileCacheData;
//         source.getTileCacheDataAsImage = source.__cached_tileDataToIamge;
//         delete source.__cached_tileDataToIamge;
//         source.getTileCacheDataAsContext2D = source.__cached_tileDataToRenderedContext;
//         delete source.__cached_tileDataToRenderedContext;
//         source.getTileHashKey = source.__cached_getTileHashKey;
//         delete source.__cached_getTileHashKey;
//         return source;
//     }
//
// }