# Source: mercator-gl.js

    ///////////////////////////////////////////////////////////////////////////////////
    // The MIT License (MIT)
    //
    // Copyright (c) 2019 Tarek Sherif
    //
    // Permission is hereby granted, free of charge, to any person obtaining a copy of
    // this software and associated documentation files (the "Software"), to deal in
    // the Software without restriction, including without limitation the rights to
    // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
    // the Software, and to permit persons to whom the Software is furnished to do so,
    // subject to the following conditions:
    //
    // The above copyright notice and this permission notice shall be included in all
    // copies or substantial portions of the Software.
    //
    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
    // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
    // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
    // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
    // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    ///////////////////////////////////////////////////////////////////////////////////

    /**
        @module JavaScript
    */

    // GLSL projection code from deck.gl https://github.com/uber/deck.gl
    // Used under MIT licence

    // transformMat4 from gl-matrix https://github.com/toji/gl-matrix/blob/master/src/vec4.js
    // Used under MIT licence

    import {PROJECTION_GLSL} from "./projection-glsl.js";

    const PI = Math.PI;
    const PI_4 = PI / 4;
    const DEGREES_TO_RADIANS = PI / 180;
    const TILE_SIZE = 512;
    const EARTH_CIRCUMFERENCE = 40.03e6;

    // High-precision for intermediate calculations
    let tempCenter64 = new Float64Array(4);

    // Low-precision for uniforms and to avoid instability
    let lngLat32 = new Float32Array(2);

    /**
        Create a Float32Array containing the low-part of 64-bit floats. The
        low parts can be passed to GLSL projection functions to improve numerical
        stability.

        @function
        @param {Float32Array} lnglat Array containing longitude latitude data (assumed to be in adjacent elements).
        @param {number} offset Offset of the first longitude value from the start of the array.
        @param {number} stride Number of elements between each pair of longitude/latitude values.
        @return {Float32Array} Array containing the low parts of input longitude/latitude data.
    */
    export function highPrecisionLngLat(lngLat, offset = 0, stride = 2) {
        let numElements = Math.ceil((lngLat.length - offset) / stride);
        let precisionData = new Float32Array(numElements * 2);
        for (let i = 0; i < numElements; ++i) {
            let lli = offset + i * stride;
            let pi = i * 2;

            precisionData[pi]     = lngLat[lli]     - Math.fround(lngLat[lli]);
            precisionData[pi + 1] = lngLat[lli + 1] - Math.fround(lngLat[lli + 1]);
        }

        return precisionData;
    }

    /**
        Inject MercatorGL projection functions in to GLSL vertex shader source string.

        @function
        @param {string} vsSource Vertex shader source code. Can be GLSL 1 or 3.
        @return {string} Source code with the projection functions injected.
    */
    export function injectMercatorGLSL(vsSource) {
        let versionMatch = vsSource.match(/#version \d+(\s+es)?\s*\n/);
        let versionLine = versionMatch ? versionMatch[0] : "";

        return vsSource.replace(versionLine, versionLine + PROJECTION_GLSL);
    }

    /**
        Allocate arrays to store projection uniform values. Will add uniforms
        to existing object, if provided.

        @function
        @param {object} [uniforms] Optional object containing existing uniforms.
        @return {object} Object containing projection uniforms.
    */
    export function allocateMercatorUniforms(uniforms = {}) {
        uniforms.mercator_gl_lngLatCenter = new Float32Array(2);
        uniforms.mercator_gl_angleDerivatives = new Float32Array(3);
        uniforms.mercator_gl_clipCenter = new Float32Array(4);
        uniforms.mercator_gl_viewProjectionMatrix = new Float32Array(16);
        uniforms.mercator_gl_scale = 0;

        return uniforms;
    }

    /**
        Update projection uniforms based on current mercator settings and provided
        view-projection matrix. The view projection matrix is expected to project from
        a 512x512x<elevation meters> mercator space to clip space.

        @function
        @param {object} uniforms Object containing existing uniforms.
        @param {array} lngLat Current camera center as a 2-element longitude/latitude array.
        @param {number} zoom Current mercator zoom level.
        @param {array} viewProjectionMatrix 4x4 matrix to transform from mercator space to clip space.
        @return {object} Object containing updated uniforms.
    */
    export function updateMercatorUniforms(uniforms, lngLat, zoom, viewProjectionMatrix) {
        let longitude = lngLat[0];
        let latitude = lngLat[1];

        uniforms.mercator_gl_scale = Math.pow(2, zoom);

        uniforms.mercator_gl_lngLatCenter[0] = longitude;
        uniforms.mercator_gl_lngLatCenter[1] = latitude;

        angleDerivatives(uniforms.mercator_gl_angleDerivatives, latitude, zoom);

        lngLat32[0] = longitude;
        lngLat32[1] = latitude;
        lngLatToClip(uniforms.mercator_gl_clipCenter, lngLat32, zoom, viewProjectionMatrix);

        uniforms.mercator_gl_viewProjectionMatrix.set(viewProjectionMatrix);

        return uniforms;
    }

    /**
        Mercator pixels/meter at the current latitude and zoom level (using 512x512 tile size).

        @function
        @param {number} latitude Current latitude of the camera center.
        @param {number} zoom Current mercator zoom level.
        @return {number} Number of pixels per meter.
    */
    export function pixelsPerMeter(latitude, zoom) {
        let scale = Math.pow(2, zoom);
        let latCosine = Math.cos(latitude * DEGREES_TO_RADIANS);

        // Number of pixels occupied by one meter around current lat/lon
        return scale * TILE_SIZE / EARTH_CIRCUMFERENCE / latCosine;
    }

    /**
        Mercator pixels/degree of longitude and latitude at the current latitude and zoom level (using 512x512 tile size).

        @function
        @param {array} out Two-element array into which the results will be written.
        @param {number} latitude Current latitude of the camera center.
        @param {number} zoom Current mercator zoom level.
        @return {array} The "out" array provided, containing the results.
    */
    export function pixelsPerDegree(out, latitude, zoom) {
        let scale = Math.pow(2, zoom);
        let latCosine = Math.cos(latitude * DEGREES_TO_RADIANS);

        // Number of pixels occupied by one degree around current lat/lon
        out[0] = scale * TILE_SIZE / 360;
        out[1] = out[0] / latCosine;

        return out;
    }

    /**
        Transform a longitude/latitude point to a 4d homegeneous coordinate in Mercator space.

        @function
        @param {array} out 4-element array into which the results will be written.
        @param {array} lngLat Longitude/latitude coordinate.
        @param {number} zoom Current mercator zoom level.
        @return {array} The "out" array provided, containing the results.
    */
    export function lngLatToMercator(out, lngLat, zoom) {
        let longitude = lngLat[0];
        let latitude = lngLat[1];
        let scale = Math.pow(2, zoom);

        let lambda2 = longitude * DEGREES_TO_RADIANS;
        let phi2 = latitude * DEGREES_TO_RADIANS;
        let x = scale * TILE_SIZE * (lambda2 + PI) / (2 * PI);
        let y = scale * TILE_SIZE * (PI - Math.log(Math.tan(PI_4 + phi2 * 0.5))) / (2 * PI);

        out[0] = x;
        out[1] = y;
        out[2] = 0;
        out[3] = 1;

        return out;
    }

    /**
        Transform a homegeneous 4d representation of a mercator point on the plane
        (x, y, 0, 1) to clip space.

        @function
        @param {array} out 4-element array into which the results will be written.
        @param {array} mercatorPosition Homegeneous 4d coordinate on the mercator plane.
        @param {array} viewProjectionMatrix 4x4 matrix to transform from mercator space to clip space.
        @return {array} The "out" array provided, containing the results.
    */
    export function mercatorToClip(out, mercatorPosition, viewProjectionMatrix) {
        transformMat4(out, mercatorPosition, viewProjectionMatrix);

        return out;
    }

    /**
        Transform a 2d longitude/latitude point to clip space to clip space.

        @function
        @param {array} out 4-element array into which the results will be written.
        @param {array} lngLat Longitude/latitude coordinate.
        @param {array} viewProjectionMatrix 4x4 matrix to transform from mercator space to clip space.
        @return {array} The "out" array provided, containing the results.
    */
    export function lngLatToClip(out, lngLat, zoom, viewProjectionMatrix) {
        lngLatToMercator(tempCenter64, lngLat, zoom);
        mercatorToClip(out, tempCenter64, viewProjectionMatrix);

        return out;
    }

    function angleDerivatives(out, latitude, zoom, latCosine2) {
        let latCosine = Math.cos(latitude * DEGREES_TO_RADIANS);
        let latCosine2 = DEGREES_TO_RADIANS * Math.tan(latitude * DEGREES_TO_RADIANS) / latCosine;

        pixelsPerDegree(out, latitude, zoom);
        out[2] = out[0] * latCosine2 / 2;
    }

    function transformMat4(out, a, m) {
        let x = a[0];
        let y = a[1];
        let z = a[2];
        let w = a[3];
        out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w;
        out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w;
        out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w;
        out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w;

        return out;
    }