packages/layergl/lib/layergl.js

/*
 * @class GridLayer.GL
 * @inherits GridLayer
 *
 * This `GridLayer` runs some WebGL code on each grid cell, and puts an image
 * with the result back in place.
 *
 * The contents of each cell can be purely synthetic (based only on the cell
 * coordinates), or be based on some remote tiles (used as textures in the WebGL
 * shaders).
 *
 * The fragment shader is assumed to receive two `vec2` attributes, with the CRS
 * coordinates and the texture coordinates: `aCRSCoords` and `aTextureCoords`.
 * If textures are used, they are accesed through the uniforms `uTexture0` through `uTexture7`
 * There will always be four vertices forming two triangles (a quad).
 *
 */

/**
 * @class LayerGl
 * @extends L.GridLayer
 * @description
 * WebGL for tile layers.
 * runs each tile layer cell image data or other form of tile-based data through WebGL shaders
 */
L.TileLayer.LayerGl = L.GridLayer.extend( {
    /**
     * options
     * @description
     * layer options
     * @type {Object}
     *
     * @property {object} uniforms
     * a key-value list of names and initial values for shader uniforms. values must be `number` or `Array[1-4]`
     *
     * @property {object} tileLayers
     * a key-value list of tile layers where the key is the texture uniform referenced in the shader
     * ```
     * { u_texture0: L.tileLayer.wms( ... ) }
     * // u_texture0 will be linked to the shader and can be referenced
     * ```
     *
     * @property {string} vertexShader
     * string representing the GLSL vertex shader to be run.
     * vertex shaders handle the processing of individual vertices.
     * vertex shader must include:
     * - **attributes** (attribute is supported in vertex shader only, attributes passed in from js program links)
     *      - `attribute vec2 a_VertexCoords;`
     *          - `vec2` attribute declaration defining the vertex coordinates in webgl space (where in the canvas)
     *      - `attribute vec2 a_TextureCoords;`
     *          - `vec2` attribute declaration defining texture coordinate in texture space (where in the texture)
     *      - `attribute vec2 a_CRSCoords;`
     *          - `vec2` attribute declaration defining texture coordinate in projection space
     *      - `attribute vec2 a_LatLngCoords;`
     *          - `vec2` attribute declaration defining texture coordinate in world space
     * - **varying** (varying values passed to fragment shader)
     *      - `varying vec2 v_TextureCoords;`
     *          - `vec2` texture coordinates passed to the fragment
     *      - `varying vec2 v_CRSCoords;`
     *          - `vec2` projection coordinates passed to the fragment
     *      - `varying vec2 v_LatLngCoords;`
     *          - `vec2` lat/lng coordinates passed to the fragment
     * - `void main( void ) { ... }`
     *  - main function to set varying declarations passed to the fragment
     * ```
     * attribute vec2 a_VertexCoords;
     * attribute vec2 a_TextureCoords;
     * attribute vec2 a_CRSCoords;
     * attribute vec2 a_LatLngCoords;
     *
     * varying vec2 v_TextureCoords;
     * varying vec2 v_CRSCoords;
     * varying vec2 v_LatLngCoords;
     *
     * void main( void ) {
     *      v_TextureCoords = a_TextureCoords;
     *      v_CRSCoords     = a_CRSCoords;
     *      v_LatLngCoords  = a_LatLngCoords;
     *      gl_Position = vec4( a_VertexCoords, 1.0, 1.0 );
     * }
     * ```
     *
     * @property {string} fragmentShader
     * string representing the GLSL fragment shader to be run.
     * fragment shaders handle the processing of individual vertex fragments
     * fragments shader must include:
     * - **precision** declaration for float types
     *      - `precision <precision> float;`
     *          - `precision highp float;` - recommend using `highp` float precision for best results
     * - **uniform** (uniform passed in from js program links)
     *      - `uniform float u_Now;`
     *          - `float` to reference the performance timing
     *      - `uniform sampler2D u_texture0;`
     *          - `sampler2D` passed in from the {@link LayerGl#tileLayers}
     *          - references the image sample passed to each fragment
     * - **varying** (varying values passed from vertex shader)
     *      - `varying vec2 v_TextureCoords;`
     *          - `vec2` varying declaration defining fragment coordinates in texture space
     *      - `varying vec2 v_CRSCoords;`
     *          - `vec2` varying declaration defining fragment coordinates in projection space
     *      - `varying vec2 v_LatLngCoords;`
     *          - `vec2` varying declaration defining fragment coordinates in lat/lng space
     * - `void main( void ) { ... }`
     *  - main function to set varying declarations. results of the fragment are passed back to canvas by setting
     *  `gl_FragColor`
     * - **glabals**
     *      - `gl_FragColor` is the result of the fragment color and must be set to carry results to the end of the
     *      program
     *
     * ```
     * precision highp float;
     *
     * uniform float u_Now;
     * uniform sampler2D u_texture0;
     *
     * varying vec2 v_TextureCoords;
     * varying vec2 v_CRSCoords;
     * varying vec2 v_LatLngCoords;
     *
     * void main( void ) {
     *      vec4 texelColor = texture2D( u_texture0, v_TextureCoords );
     *      gl_FragColor = texelColor;
     * }
     * ```
     *
     * @property {array} extensions
     * extensions to load into the WebGL program.
     * the default program requires linear filtering for floating-point textures so
     * [`OES_texture_float_linear`](https://www.khronos.org/registry/OpenGL/extensions/OES/OES_texture_float_linear.txt)
     * extension is hardcoded in {@link LayerGl#initialize}
     *
     * @alias LayerGl#options
     * @memberOf LayerGl#
     */
    options: {
        uniforms: {},
        tileLayers: {},
        vertexShader: '',
        fragmentShader: '',
        extensions: []
    },

    /**
     * initialize
     * @description
     * leaflet initialize method
     * instantiating the layer will initialize all the GL context and upload shaders and vertex buffers to the GPU
     * (the vertices will stay the same for all tiles).
     * @param {object} [options={}] - layer options
     * @alias initialize
     * @memberOf LayerGl#
     */
    initialize( options = {} ) {
        options = L.setOptions( this, options );

        this._renderer = L.DomUtil.create( 'canvas' );

        this._renderer.width = this._renderer.height = options.tileSize;

        this._gl = (
            this._renderer.getContext( 'webgl', { premultipliedAlpha: false } ) ||
            this._renderer.getContext( 'experimental-webgl', { premultipliedAlpha: false } )
        );

        this._gl.viewportWidth = this._gl.viewportHeight = options.tileSize;

        this._glError = false;

        // init textures
        this._textures             = [];
        this._tileLayerUniformKeys = Object.keys( options.tileLayers );

        this._loadGLProgram();

        this._tileLayers = this._tileLayerUniformKeys.map(
            ( key, i ) => {
                this._textures[ i ] = this._gl.createTexture();
                this._gl.uniform1i( this._gl.getUniformLocation( this._glProgram, key ), i );

                return options.tileLayers[ key ];
            }
        );

        this._gl.getExtension( 'OES_texture_float_linear' );

        for ( let i = 0; i < options.extensions.length; i++ ) {
            this._gl.getExtension( options.extensions[ i ] );
        }
    },

    /**
     * getGlError
     * @memberOf LayerGl~
     * @description
     * if any compiling/linking errors occur in the shaders, returns a string with information about that error
     * @return {string|undefined} - glError if exists
     */
    getGlError() {
        return this._glError;
    },

    /**
     * _linkShader
     * @memberOf LayerGl~
     * @description
     * shader type being linked
     * @param {string} shaderCode
     * shader code to compile and link
     * @param {('VERTEX_SHADER'|'FRAGMENT_SHADER')} type
     * `VERTEX_SHADER` or `FRAGMENT_SHADER` type being linked
     * @return {WebGLShader}
     * returns `WebGLShader` if link succeeds
     */
    _linkShader( shaderCode, type ) {
        if ( !shaderCode || typeof shaderCode !== 'string' ) {
            throw new Error( '"shaderCode" is required and must be type "string"' );
        }
        else if ( type !== this._gl.VERTEX_SHADER && type !== this._gl.FRAGMENT_SHADER ) {
            throw new Error( '"type" must be VERTEX_SHADER or FRAGMENT_SHADER' );
        }

        const shader = this._gl.createShader( type );

        this._gl.shaderSource( shader, shaderCode );
        this._gl.compileShader( shader );

        // @event shaderError
        // Fired when there was an error creating the shaders.
        if ( !this._gl.getShaderParameter( shader, this._gl.COMPILE_STATUS ) ) {
            this._glError = this._gl.getShaderInfoLog( shader );
            console.error( this._glError );
            throw new Error( this._glError );
        }

        this._gl.attachShader( this._glProgram, shader );

        return shader;
    },

    /**
     * _loadGLProgram
     * @memberOf LayerGl~
     * @description
     * create, compile, link, and use the primary webgl program
     *
     * once loaded and uniforms initialized, create three data buffer with 8 elements each
     * - `_CRSBuffer` - the `easting, northing` CRS coords
     * - `_LatLngBuffer` - the `easting, northing` lat/lng coords
     * - the `s, t` (also referred to as `u, v`) texture coords and the viewport coords for each of the 4 vertices
     *
     * data for the texel and viewport coords is static, and needs to be declared only once
     */
    _loadGLProgram() {
        this._getUniformSizes();

        /**
         * _glProgram
         * @alias LayerGl._glProgram
         * @description
         * primary `WebGLProgram` created
         * @type {WebGLProgram}
         */
        this._glProgram = this._gl.createProgram();

        const
            vertexShader   = this._linkShader( this.options.vertexShader, this._gl.VERTEX_SHADER ),
            fragmentShader = this._linkShader( this.options.fragmentShader, this._gl.FRAGMENT_SHADER );

        if ( !vertexShader || !fragmentShader ) {
            throw new Error( 'shaders failed to link' );
        }

        this._gl.linkProgram( this._glProgram );
        this._gl.useProgram( this._glProgram );

        // There will be two vec2 vertex attributes per vertex - aCRSCoords and aTextureCoords
        this._aVertexPosition = this._gl.getAttribLocation( this._glProgram, 'a_VertexCoords' );
        this._aTexPosition    = this._gl.getAttribLocation( this._glProgram, 'a_TextureCoords' );
        this._aCRSPosition    = this._gl.getAttribLocation( this._glProgram, 'a_CRSCoords' );
        this._aLatLngPosition = this._gl.getAttribLocation( this._glProgram, 'a_LatLngCoords' );

        this._initUniforms( this._glProgram );

        // if the shader is time-dependent (animated in any way), or has custom uniforms, init the texture cache
        if ( this._isReRenderable ) {
            this._fetchedTextures = {};
            this._2dContexts      = {};
        }

        /**
         * _CRSBuffer
         * @alias LayerGl._CRSBuffer
         * @description
         * the `easting, northing` CRS coords buffer
         * @type {WebGLBuffer}
         */
        this._CRSBuffer = this._gl.createBuffer();
        this._gl.bindBuffer( this._gl.ARRAY_BUFFER, this._CRSBuffer );
        this._gl.bufferData( this._gl.ARRAY_BUFFER, new Float32Array( 8 ), this._gl.STATIC_DRAW );

        if ( this._aCRSPosition !== -1 ) {
            this._gl.enableVertexAttribArray( this._aCRSPosition );
            this._gl.vertexAttribPointer( this._aCRSPosition, 2, this._gl.FLOAT, false, 8, 0 );
        }

        /**
         * _LatLngBuffer
         * @alias LayerGl._LatLngBuffer
         * @description
         * the `easting, northing` lat/lng coords buffer
         * @type {WebGLBuffer}
         */
        this._LatLngBuffer = this._gl.createBuffer();
        this._gl.bindBuffer( this._gl.ARRAY_BUFFER, this._LatLngBuffer );
        this._gl.bufferData( this._gl.ARRAY_BUFFER, new Float32Array( 8 ), this._gl.STATIC_DRAW );

        if ( this._aLatLngPosition !== -1 ) {
            this._gl.enableVertexAttribArray( this._aLatLngPosition );
            this._gl.vertexAttribPointer( this._aLatLngPosition, 2, this._gl.FLOAT, false, 8, 0 );
        }

        /**
         * _TexCoordsBuffer
         * @alias LayerGl._TexCoordsBuffer
         * @description
         * the texel coords buffer
         * @type {WebGLBuffer}
         */
        this._TexCoordsBuffer = this._gl.createBuffer();
        this._gl.bindBuffer( this._gl.ARRAY_BUFFER, this._TexCoordsBuffer );
        this._gl.bufferData( this._gl.ARRAY_BUFFER, new Float32Array( [
            1.0, 0.0,
            0.0, 0.0,
            1.0, 1.0,
            0.0, 1.0
        ] ), this._gl.STATIC_DRAW );

        if ( this._aTexPosition !== -1 ) {
            this._gl.enableVertexAttribArray( this._aTexPosition );
            this._gl.vertexAttribPointer( this._aTexPosition, 2, this._gl.FLOAT, false, 8, 0 );
        }

        /**
         * _VertexCoordsBuffer
         * @alias LayerGl._VertexCoordsBuffer
         * @description
         * the vertex coords buffer
         * @type {WebGLBuffer}
         */
        this._VertexCoordsBuffer = this._gl.createBuffer();
        this._gl.bindBuffer( this._gl.ARRAY_BUFFER, this._VertexCoordsBuffer );
        this._gl.bufferData( this._gl.ARRAY_BUFFER, new Float32Array( [
            1.0, 1.0,
            -1.0, 1.0,
            1.0, -1.0,
            -1.0, -1.0
        ] ), this._gl.STATIC_DRAW );

        if ( this._aVertexPosition !== -1 ) {
            this._gl.enableVertexAttribArray( this._aVertexPosition );
            this._gl.vertexAttribPointer( this._aVertexPosition, 2, this._gl.FLOAT, false, 8, 0 );
        }
    },

    /**
     * _getUniformSizes
     * @memberOf LayerGl~
     * @description
     * determines the size of the default values given for the uniforms.
     * loads a string value for defining the uniforms in the shader header into `_uniformSizes`
     */
    _getUniformSizes() {
        /**
         * _uniformSizes
         * @alias LayerGl._uniformSizes
         * @description
         * determines the size of the default values given for the uniforms and loads the uniform name from the
         * shader header
         * @type {Object}
         */
        this._uniformSizes = {};

        for ( const uniformKey in this.options.uniforms ) {
            const defaultValue = this.options.uniforms[ uniformKey ];

            if ( typeof defaultValue === 'number' ) {
                this._uniformSizes[ uniformKey ] = 0;
            }
            else if ( Array.isArray( defaultValue ) ) {
                if ( defaultValue.length > 4 ) {
                    throw new Error( 'Max size for uniform value is 4 elements' );
                }

                this._uniformSizes[ uniformKey ] = defaultValue.length;
            }
            else {
                throw new Error(
                    'Default value for uniforms must be either number or array of numbers'
                );
            }
        }
    },

    /**
     * _initUniforms
     * @memberOf LayerGl~
     * @description
     * initializes the `u_Now` uniform, and user-provided uniforms from the current GL program.
     * sets the `_isReRenderable` property if there are any set uniforms.
     * @param {WebGLProgram} program - WebGlProgram (using as param to support multiple programs in the future)
     * {@link LayerGl._glProgram} created - must be linked to extract uniform locations
     */
    _initUniforms( program ) {
        this._uNowPosition   = this._gl.getUniformLocation( program, 'u_Now' );
        this._isReRenderable = false;

        if ( this._uNowPosition ) {
            this._gl.uniform1f( this._uNowPosition, performance.now() );
            this._isReRenderable = true;
        }

        this._uniformLocations = {};

        for ( const uniformKey in this.options.uniforms ) {
            this._uniformLocations[ uniformKey ] = this._gl.getUniformLocation( program, uniformKey );
            this.setUniform( uniformKey, this.options.uniforms[ uniformKey ] );
            this._isReRenderable = true;
        }
    },

    /**
     * _render
     * @memberOf LayerGl~
     * @description
     * called once per tile - uses the layer's GL context to render a tile, passing the complex space coordinates to
     * the GPU, and asking to render the vertexes (as triangles) again.
     * it is not necessary to clear the WebGL context, because the shader will overwrite all the pixel values.
     * @param {{x: number, y: number, z: number}} coords
     * coords passed in from {@link LayerGl~createTile}
     */
    _render( coords ) {
        this._gl.viewport( 0, 0, this._gl.drawingBufferWidth, this._gl.drawingBufferHeight );

        this._gl.enable( this._gl.BLEND );

        const
            tileBounds = this._tileCoordsToBounds( coords ),
            west       = tileBounds.getWest(),
            east       = tileBounds.getEast(),
            north      = tileBounds.getNorth(),
            south      = tileBounds.getSouth(),
            // create data array for LatLng buffer
            latLngData = [
                // Vertex 0
                east, north,

                // Vertex 1
                west, north,

                // Vertex 2
                east, south,

                // Vertex 3
                west, south
            ];

        // upload buffer to GPU
        this._gl.bindBuffer( this._gl.ARRAY_BUFFER, this._LatLngBuffer );
        this._gl.bufferData( this._gl.ARRAY_BUFFER, new Float32Array( latLngData ), this._gl.STATIC_DRAW );

        // create data array for CRS buffer
        const
            crs     = this._map.options.crs,
            min     = crs.project( L.latLng( south, west ) ),
            max     = crs.project( L.latLng( north, east ) ),
            crsData = [
                // Vertex 0
                max.x, max.y,

                // Vertex 1
                min.x, max.y,

                // Vertex 2
                max.x, min.y,

                // Vertex 3
                min.x, min.y
            ];

        // upload buffer to GPU
        this._gl.bindBuffer( this._gl.ARRAY_BUFFER, this._CRSBuffer );
        this._gl.bufferData( this._gl.ARRAY_BUFFER, new Float32Array( crsData ), this._gl.STATIC_DRAW );

        // draw arrays
        this._gl.drawArrays( this._gl.TRIANGLE_STRIP, 0, 4 );
    },

    /**
     * _bindTexture
     * @memberOf LayerGl~
     * @description
     * binds a `ImageData` (`HTMLImageElement`, `HTMLCanvasElement` or `ImageBitmap`) to a texture, given its index
     * (0 to 7). the image data is assumed to be in 8-bit RGBA format. TEXTURE0 corresponds to the
     * WebGL pointer 0x84C0 - add index integer to specify texture channel pointer
     * @param {number} index - index of texture uniform
     * @param {TexImageSource} imageData - image data from tile data ref: {@link LayerGl~createTile}
     */
    _bindTexture( index, imageData ) {
        this._gl.activeTexture( this._gl.TEXTURE0 + index );
        this._gl.bindTexture( this._gl.TEXTURE_2D, this._textures[ index ] );
        this._gl.texImage2D(
            this._gl.TEXTURE_2D,
            0,
            this._gl.RGBA,
            this._gl.RGBA,
            this._gl.UNSIGNED_BYTE,
            imageData
        );

        this._gl.texParameteri( this._gl.TEXTURE_2D, this._gl.TEXTURE_MIN_FILTER, this._gl.LINEAR_MIPMAP_NEAREST );
        this._gl.texParameteri( this._gl.TEXTURE_2D, this._gl.TEXTURE_MAG_FILTER, this._gl.LINEAR );
        this._gl.texParameteri( this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_S, this._gl.CLAMP_TO_EDGE );
        this._gl.texParameteri( this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_T, this._gl.CLAMP_TO_EDGE );
        this._gl.generateMipmap( this._gl.TEXTURE_2D );
    },

    /**
     * _bindTextureArrays
     * @memberOf LayerGl~
     * @description
     * binds an array of `TypedArrays` (array containing `Float32Array`) , given its index (0 to 7).
     * the image data is assumed to **not** be in 8-bit RGBA format.
     * type is inferred from the type of the typed array.
     * @param {number} index - index of texture uniform
     * @param {ArrayBufferView} arrays - image data from tile data ref: {@link LayerGl~createTile}
     */
    _bindTextureArrays( index, arrays ) {
        this._gl.activeTexture( this._gl.TEXTURE0 + index );
        this._gl.bindTexture( this._gl.TEXTURE_2D_ARRAY, this._textures[ index ] );
        this._gl.texParameteri( this._gl.TEXTURE_2D_ARRAY, this._gl.TEXTURE_MIN_FILTER, this._gl.NEAREST );
        this._gl.texParameteri( this._gl.TEXTURE_2D_ARRAY, this._gl.TEXTURE_MAG_FILTER, this._gl.NEAREST );

        this._gl.texImage3D(
            this._gl.TEXTURE_2D_ARRAY,
            0,
            this._gl.R32F,
            arrays.width,
            arrays.height,
            arrays.length,
            0,
            this._gl.RED,
            this._gl.FLOAT,
            Float32Array.from( arrays[ 0 ] ),
            0
        );

        this._gl.texParameteri( this._gl.TEXTURE_2D_ARRAY, this._gl.TEXTURE_WRAP_S, this._gl.CLAMP_TO_EDGE );
        this._gl.texParameteri( this._gl.TEXTURE_2D_ARRAY, this._gl.TEXTURE_WRAP_T, this._gl.CLAMP_TO_EDGE );
        // this._gl.generateMipmap( this._gl.TEXTURE_2D );
    },

    /**
     * createTile
     * @memberOf LayerGl~
     * @description
     * reference leaflet docs
     * @param {Object} coords - tile coordinates
     * @param {function} done - callback
     * @return {*} - tile
     */
    createTile( coords, done ) {
        const tile         = L.DomUtil.create( 'canvas', 'leaflet-tile' );
        tile.width         = tile.height = this.options.tileSize;
        tile.onselectstart = tile.onmousemove = L.Util.falseFn;

        const
            ctx          = tile.getContext( '2d' ),
            unwrappedKey = this._tileCoordsToKey( coords ),
            texFetches   = [];

        for ( let i = 0; i < this._tileLayers.length && i < 8; i++ ) {
            texFetches.push( this._getNthTile( i, coords ) );
        }

        Promise.all( texFetches )
            .then( ( textureImages ) => {
                if ( this._isReRenderable ) {
                    this._fetchedTextures[ unwrappedKey ] = textureImages;
                    this._2dContexts[ unwrappedKey ]      = ctx;
                }

                for ( let i = 0; i < this._tileLayers.length && i < 8; i++ ) {
                    this._bindTexture( i, textureImages[ i ] );
                }

                this._render( coords );
                ctx.drawImage( this._renderer, 0, 0 );
                done();
            } )
            .catch( ( err ) => {
                L.TileLayer.prototype._tileOnError.call( this, done, tile, err );
            } );

        return tile;
    },

    _removeTile( key ) {
        if ( this._isReRenderable ) {
            delete this._fetchedTextures[ key ];
            delete this._2dContexts[ key ];
        }

        L.TileLayer.prototype._removeTile.call( this, key );
    },

    onAdd() {
        // if the shader is time-dependent (i.e. animated), start an animation loop.
        if ( this._uNowPosition ) {
            L.Util.cancelAnimFrame( this._animFrame );
            this._animFrame = L.Util.requestAnimFrame( this._onFrame, this );
        }

        L.TileLayer.prototype.onAdd.call( this );
    },

    onRemove( map ) {
        // stop the animation loop, if any.
        L.Util.cancelAnimFrame( this._animFrame );
        L.TileLayer.prototype.onRemove.call( this, map );
    },

    _onFrame() {
        if ( this._uNowPosition && this._map ) {
            this.reRender();
            this._animFrame = L.Util.requestAnimFrame( this._onFrame, this, false );
        }
    },

    fastReRender() {
        this._gl.clear( this._gl.COLOR_BUFFER_BIT );
    },

    // runs shader again on all tiles
    reRender() {
        if ( !this._isReRenderable ) {
            return;
        }

        this._gl.uniform1f( this._uNowPosition, performance.now() );

        for ( const key in this._tiles ) {
            if ( !Object.prototype.hasOwnProperty.call( this._tiles, key ) ) {
                continue;
            }

            const
                tile       = this._tiles[ key ],
                coords     = this._keyToTileCoords( key ),
                wrappedKey = this._tileCoordsToKey( this._wrapCoords( coords ) );

            if ( !tile.current || !tile.loaded || !this._fetchedTextures[ wrappedKey ] ) {
                continue;
            }

            for ( let i = 0; i < this._tileLayers.length && i < 8; i++ ) {
                this._bindTexture( i, this._fetchedTextures[ wrappedKey ][ i ] );
            }

            this._render( coords );

            this._2dContexts[ key ].drawImage( this._renderer, 0, 0 );
        }
    },

    // sets value(s) for a uniform
    setUniform( name, value ) {
        // TODO: circle back to transferring boolean uniforms for edge detection and other boolean vertex shaders
        switch ( this._uniformSizes[ name ] ) {
            case 0:
                this._gl.uniform1f( this._uniformLocations[ name ], value );
                break;
            case 1:
                this._gl.uniform1fv( this._uniformLocations[ name ], value );
                break;
            case 2:
                this._gl.uniform2fv( this._uniformLocations[ name ], value );
                break;
            case 3:
                this._gl.uniform3fv( this._uniformLocations[ name ], value );
                break;
            case 4:
                this._gl.uniform4fv( this._uniformLocations[ name ], value );
                break;
        }
    },

    // gets the tile for the Nth `TileLayer` in `this._tileLayers`,
    // for the given tile coords, returns a promise to the tile.
    _getNthTile( n, coords ) {
        const layer = this._tileLayers[ n ];

        layer._tileZoom        = this._tileZoom;
        layer._map             = this._map;
        layer._crs             = this._map.options.crs;
        layer._globalTileRange = this._globalTileRange;

        return new Promise(
            ( res, rej ) => {
                const tile       = document.createElement( 'img' );
                tile.crossOrigin = '';
                tile.src         = layer.getTileUrl( coords );
                L.DomEvent.on( tile, 'load', res.bind( this, tile ) );
                L.DomEvent.on( tile, 'error', rej.bind( this, tile ) );
            }
        );
    }
} );

L.tileLayer.layerGl = function( opts ) {
    return new L.TileLayer.LayerGl( opts );
};