Source: /modules/webgldataLoader.js

/**
 * Data loading strategies for different WebGL versions.
 * Should you have your own data format, change/re-define these
 * to correctly load the textures to GPU, based on the WebGL version used.
 *
 * The processing accepts arrays of images to feed to the shader built from configuration.
 * This implementation supports data as Image or Canvas objects. We will refer to them as <image*>
 *
 * Implemented texture loaders support
 *  - working with <image*> object - image data chunks are vertically concatenated
 *  - working with [<image*>] object - images are in array
 *
 * @namespace WebGLModule.DataLoader
 */
WebGLModule.DataLoader = {
    /**
     * In case the system is fed by anything but 'Image' (or the like) data object,
     * implement here conversion so that debug mode can draw it.
     * @param {*} data
     * @return {HTMLElement} Dom Element
     */
    dataAsHtmlElement: function(data) {
        return {
            "[object HTMLImageElement]": () => data,
            "[object HTMLCanvasElement]": () => data,
            //Image objects in Array, we assume image objects only
            "[object Array]": function() {
                const node = document.createElement("div");
                for (let image of data) {
                    node.append(image);
                }
                return node;
            }
        }[toString.apply(data)]();
    },

    /**
     * Data loader for WebGL 1.0. Must load the data based on dataIndexMapping:
     *  for (first) texture at index 0, obtain its global index at dataIndexMapping[0]
     *  use the global index to localize the texture chunk in the data
     *  use the local index to get the texture name the chunk must be loaded to.
     *
     * Note that loading with single texture and computing indices is not possible
     * since some access surrounding pixels -> requirement of MIRRORED_REPEAT not implementable
     *
     * For details, please, see the implementation.
     * @class WebGLModule.DataLoader.V1_0
     */
    V1_0: class /**@lends WebGLModule.DataLoader.V1_0 */ {
        /**
         * Creation
         * @param {WebGLRenderingContext} gl
         * @memberOf WebGLModule.DataLoader.V1_0
         */
        constructor(gl) {
            this._units = [];

            //todo more elegant
            this.loadersByType["[object HTMLCanvasElement]"] = this.loadersByType["[object HTMLImageElement]"];
        }

        /**
         * Called when the program is being loaded (set as active)
         * @param {WebGLModule} context
         * @param {WebGLRenderingContext} gl WebGL context
         * @param {WebGLProgram} program
         * @param {GLint} wrap required texture GL wrap value
         * @param {GLint} filter required texture GL filter value
         * @param {object} visualization reference to the visualization object
         * @instance
         * @memberOf WebGLModule.DataLoader.V1_0
         */
        toBuffers(context, gl, program, wrap, filter, visualization) {
            this.wrap = wrap;
            this.filter = filter;

            //The resizing in border tiles is done when the GL canvas is rendered to the output canvas
            gl.uniform2f(gl.getUniformLocation(program, 'sampler_size'), gl.canvas.width, gl.canvas.height);
        }

        /**
         * Called when tile is processed
         * @param {WebGLModule} context
         * @param {array} dataIndexMapping mapping of array indices to data indices, e.g. texture 0 for
         *   this shader corresponds to index dataIndexMapping[0] in the data array, -1 value used for textures not loaded
         * @param {object} visualization reference to the current active visualization object
         * @param {*} data data object, must contain all the data listed in WebGLModule.prototype.getSources() in
         *   the respective order, dataIndexMapping then points with index to this data; by default an Image object
         * @param {object} tileBounds tile size in pixels
         * @param {number} tileBounds.width tile width
         * @param {number} tileBounds.height tile height
         * @param {WebGLProgram} program current WebGLProgram
         * @param {WebGLRenderingContext} gl
         * @memberOf WebGLModule.DataLoader.V1_0
         * @instance
         */
        toCanvas(context, dataIndexMapping, visualization, data, tileBounds, program, gl) {
            (this.loadersByType[toString.apply(data)] || (() => {throw "WebGL 1.0 Renderer cannot load data as texture: " + toString.apply(data)}))(
                this, context, dataIndexMapping, visualization, data, tileBounds, program, gl
            );
        }

        /**
         * Todo docs
         * Texture not necessary to re-initialize since we can reuse it straight away
         * @param context
         * @param dataIndexMapping
         * @param visualization
         * @param data
         * @param tileBounds
         * @param program
         * @param gl
         * @memberOf WebGLModule.DataLoader.V1_0
         * @instance
         */
        toCanvasFinish(context, dataIndexMapping, visualization, data, tileBounds, program, gl) {

        }

        /**
         * Measure texture size
         * @param {number} index index of the texture
         * @return {string} GLSL expression (unterminated) to obtain texture size - vec2
         * @memberOf WebGLModule.DataLoader.V1_0
         * @instance
         */
        measure(index) {
            return 'sampler_size';
        }

        /**
         * Sample texture
         * @param {number|string} index texture index, must respect index re-mapping (see declare())
         * @param {string} vec2coords GLSL expression that evaluates to vec2
         * @return {string} GLSL expression (unterminated) that evaluates to vec4
         * @memberOf WebGLModule.DataLoader.V1_0
         * @instance
         */
        sample(index, vec2coords) {
            return `texture2D(vis_data_sampler_${index}, ${vec2coords})`;
        }

        /**
         * Declare elements in shader
         * @param {number[]} indicesOfImages mapping of shader to data index, i.e. if shader requests
         *  texture i, the texture is located in the data at indicesOfImages[i] index
         * @return {string} GLSL declaration (terminated with semicolon) of necessary elements for textures
         * @memberOf WebGLModule.DataLoader.V1_0
         * @instance
         */
        declare(indicesOfImages) {
            let samplers = 'uniform vec2 sampler_size;';
            for (let i = 0; i < indicesOfImages.length; i++) {
                if (indicesOfImages[i] === -1) continue;
                samplers += `uniform sampler2D vis_data_sampler_${i};`;
            }
            return samplers;
        }

        /**
         * Loader strategy based on toString result, extend with your type if necessary.
         * If your type cannot use the given version strategy (TEXTURE UNITS), you have
         * to re-define the whole API.
         */
        loadersByType = {
            "[object HTMLImageElement]": function(self, webglModule, dataIndexMapping, visualization, data, tileBounds, program, gl) {
                if (!self._canvas) {
                    self._canvas = document.createElement('canvas');
                    self._canvasReader = self._canvas.getContext('2d', {willReadFrequently: true});
                    self._canvasConverter = document.createElement('canvas');
                    self._canvasConverterReader = self._canvasConverter.getContext('2d', {willReadFrequently: true});
                }

                let index = 0;
                tileBounds.width = Math.round(tileBounds.width);
                tileBounds.height = Math.round(tileBounds.height);

                //we read from here
                self._canvas.width = data.width;
                self._canvas.height = data.height;
                self._canvasReader.drawImage(data, 0, 0);

                const NUM_IMAGES = Math.round(data.height / tileBounds.height);
                //Allowed texture size dimension only 256+ and power of two...

                //it worked for arbitrary size until we begun with image arrays... is it necessary?
                const IMAGE_SIZE = data.width < 256 ? 256 : Math.pow(2, Math.ceil(Math.log2(data.width)));
                self._canvasConverter.width = IMAGE_SIZE;
                self._canvasConverter.height = IMAGE_SIZE;

                //just load all images and let shaders reference them...
                for (let i = 0; i < dataIndexMapping.length; i++) {
                    if (dataIndexMapping[i] < 0) {
                        continue;
                    }
                    if (index >= NUM_IMAGES) {
                        console.warn("The visualization contains less data than layers. Skipping layers ...");
                        return;
                    }

                    //create textures
                    while (index >= self._units.length) {
                        self._units.push(gl.createTexture());
                    }
                    let bindConst = `TEXTURE${index}`;
                    gl.activeTexture(gl[bindConst]);
                    let location = gl.getUniformLocation(program, `vis_data_sampler_${i}`);
                    gl.uniform1i(location, index);

                    gl.bindTexture(gl.TEXTURE_2D, self._units[index]);
                    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, self.wrap);
                    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, self.wrap);
                    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, self.filter);
                    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, self.filter);
                    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);

                    let pixels;
                    if (tileBounds.width !== IMAGE_SIZE || tileBounds.height !== IMAGE_SIZE)  {
                        self._canvasConverterReader.drawImage(self._canvas, 0, dataIndexMapping[i]*tileBounds.height,
                            tileBounds.width, tileBounds.height, 0, 0, IMAGE_SIZE, IMAGE_SIZE);

                        pixels = self._canvasConverterReader.getImageData(0, 0, IMAGE_SIZE, IMAGE_SIZE);
                    } else {
                        //load data
                        pixels = self._canvasReader.getImageData(0,
                            dataIndexMapping[i]*tileBounds.height, tileBounds.width, tileBounds.height);
                    }

                    gl.texImage2D(gl.TEXTURE_2D,
                        0,
                        gl.RGBA,
                        gl.RGBA,
                        gl.UNSIGNED_BYTE,
                        pixels);
                    index++;
                }
            },
            //Image objects in Array, we assume image objects only
            "[object Array]": function(self, webglModule, dataIndexMapping, visualization, data, tileBounds, program, gl) {
                let index = 0;
                const NUM_IMAGES = data.length;
                //just load all images and let shaders reference them...
                for (let i = 0; i < dataIndexMapping.length; i++) {
                    if (dataIndexMapping[i] < 0) {
                        continue;
                    }
                    if (index >= NUM_IMAGES) {
                        console.warn("The visualization contains less data than layers. Skipping layers ...");
                        return;
                    }

                    //create textures
                    while (index >= self._units.length) {
                        self._units.push(gl.createTexture());
                    }
                    let bindConst = `TEXTURE${index}`;
                    gl.activeTexture(gl[bindConst]);
                    let location = gl.getUniformLocation(program, `vis_data_sampler_${i}`);
                    gl.uniform1i(location, index);

                    gl.bindTexture(gl.TEXTURE_2D, self._units[index]);
                    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, self.wrap);
                    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, self.wrap);
                    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, self.filter);
                    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, self.filter);
                    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
                    //do not check the image size, we render what wwe
                    gl.texImage2D(gl.TEXTURE_2D,
                        0,
                        gl.RGBA,
                        gl.RGBA,
                        gl.UNSIGNED_BYTE,
                        data[index++]
                    );
                }
            }
        };
    },

    /**
     * Data loader for WebGL 2.0. Must load the data to a Texture2DArray.
     * The name of the texture is a constant. The order od the textures in
     * the z-stacking is defined in dataIndexMapping.
     *
     * For details, please, see the implementation.
     * @class WebGLModule.DataLoader.V2_0
     */
    V2_0: class /**@lends WebGLModule.DataLoader.V2_0 */ {
        /**
         * Creation
         * @param {WebGL2RenderingContext} gl
         * @param {string} textureName texture name, must load the data as Texture2DArray
         * @memberOf WebGLModule.DataLoader.V2_0
         * */
        constructor(gl, textureName) {
            this.textureName = textureName;
            this.textureId = gl.createTexture();

            //todo more elegant:
            this.loadersByType["[object HTMLCanvasElement]"] = this.loadersByType["[object HTMLImageElement]"];
        }

        /**
         * Called when the program is being loaded (set as active)
         * @param {WebGLModule} context
         * @param {WebGL2RenderingContext} gl WebGL context
         * @param {GLint} wrap required texture GL wrap value
         * @param {GLint} filter required texture GL filter value
         * @param {object} visualization reference to the visualization object
         * @memberOf WebGLModule.DataLoader.V2_0
         */
        toBuffers(context, gl, program, wrap, filter, visualization) {
            this.wrap = wrap;
            this.filter = filter;
        }

        /**
         * Called when tile is processed
         * @param {WebGLModule} context context renderer reference
         * @param {array} dataIndexMapping mapping of array indices to data indices, e.g. texture 0 for
         *   this shader corresponds to index dataIndexMapping[0] in the data array, -1 value used for textures not loaded
         * @param {object} visualization reference to the current active visualization object
         * @param {*} data data object, must contain all the data listed in WebGLModule.prototype.getSources() in
         *   the respective order, dataIndexMapping then points with index to this data; by default an Image object
         * @param {object} tileBounds tile size in pixels
         * @param {number} tileBounds.width tile width
         * @param {number} tileBounds.height tile height
         * @param {WebGLProgram} program current WebGLProgram
         * @param {WebGL2RenderingContext} gl
         * @memberOf WebGLModule.DataLoader.V2_0
         * @instance
         */
        toCanvas(context, dataIndexMapping, visualization, data, tileBounds, program, gl) {
            (this.loadersByType[toString.apply(data)] || (() => {throw "WebGL 2.0 Renderer cannot load data as texture: " + toString.apply(data)}))(
                this, context, dataIndexMapping, visualization, data, tileBounds, program, gl
            );
        }

        /**
         *
         * @param context
         * @param dataIndexMapping
         * @param visualization
         * @param data
         * @param tileBounds
         * @param program
         * @param gl
         * @memberOf WebGLModule.DataLoader.V2_0
         * @instance
         */
        toCanvasFinish(context, dataIndexMapping, visualization, data, tileBounds, program, gl) {
            if (Array.isArray(data)) {
                //need to re-initialize the texture
                gl.deleteTexture(this.textureId);
                this.textureId = gl.createTexture();
            }
        }


        /**
         * Measure texture size
         * @param {number} index index of the texture
         * @return {string} GLSL expression (unterminated) to obtain texture size - vec2
         * @memberOf WebGLModule.DataLoader.V2_0
         * @instance
         */
        measure(index) {
            return 'vec2(textureSize(vis_data_sampler_array))';
        }

        /**
         * Sample texture
         * @param {number|string} index texture index, must respect index re-mapping (see declare())
         * @param {string} vec2coords GLSL expression that evaluates to vec2
         * @return {string} GLSL expression (unterminated) that evaluates to vec4
         * @memberOf WebGLModule.DataLoader.V2_0
         * @instance
         */
        sample(index, vec2coords) {
            return `texture(vis_data_sampler_array, vec3(${vec2coords}, _vis_data_sampler_array_indices[${index}]))`;
        }

        /**
         * Declare elements in shader
         * @param {number[]} indicesOfImages mapping of shader to data index, i.e. if shader requests
         *  texture i, the texture is located in the data at indicesOfImages[i] index
         * @return {string} GLSL declaration (terminated with semicolon) of necessary elements for textures
         * @memberOf WebGLModule.DataLoader.V2_0
         * @instance
         */
        declare(indicesOfImages) {
            return `uniform sampler2DArray vis_data_sampler_array;
int _vis_data_sampler_array_indices[${indicesOfImages.length}] = int[${indicesOfImages.length}](
  ${indicesOfImages.join(",")}
);`;
        }

        /**
         * Loader strategy based on toString result, extend with your type if necessary.
         * If your type cannot use the given version strategy (TEXTURE_2D_ARRAY UNIT), you have
         * to re-define the whole API.
         */
        loadersByType = {
            //Vertically Concatenated Images
            "[object HTMLImageElement]": function(self, webglModule, dataIndexMapping, visualization, data, tileBounds, program, gl) {
                const NUM_IMAGES = Math.round(data.height / tileBounds.height);

                // Texture checking disabled due to performance reasons
                // if (NUM_IMAGES < dataIndexMapping.reduce((sum, val, _i, _a) => sum + (val >= 0 ? 1 : 0), 0).length) {
                //     console.warn("Incoming data does not contain necessary number of images!", NUM_IMAGES, dataIndexMapping);
                // }

                //Just load the texture since it comes as an Image element concatenated below each other
                //in the correct order --> directly to GPU
                gl.activeTexture(gl.TEXTURE0);
                gl.bindTexture(gl.TEXTURE_2D_ARRAY, self.textureId);
                gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, self.filter);
                gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, self.filter);
                gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, self.wrap);
                gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, self.wrap);

                gl.texImage3D(
                    gl.TEXTURE_2D_ARRAY,
                    0,
                    gl.RGBA,
                    tileBounds.width,
                    tileBounds.height,
                    NUM_IMAGES,
                    0,
                    gl.RGBA,
                    gl.UNSIGNED_BYTE,
                    data
                );
            },
            //Image objects in Array, we assume image objects only
            "[object Array]": function(self, webglModule, dataIndexMapping, visualization, data, tileBounds, program, gl) {
                gl.activeTexture(gl.TEXTURE0);
                gl.bindTexture(gl.TEXTURE_2D_ARRAY, self.textureId);
                gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.RGBA8, data[0].width, data[0].height, data.length+1);
                gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAX_LEVEL, 0);
                gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, self.filter);
                gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, self.filter);
                gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, self.wrap);
                gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, self.wrap);

                let index = 0;
                for (let image of data) {
                    gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, index++, image.width, image.height,
                        1, gl.RGBA, gl.UNSIGNED_BYTE, image);
                }
            }
        };
    }
};