utils/GeoImageCreator.tstypescript
import * as utils from "../utils/shared";
import { BaseGeoImage } from "../layer/BaseGeoImage";
import { Framebuffer } from "../webgl/Framebuffer";
import { LonLat } from "../LonLat";
import { ShaderProgram } from "../webgl/ShaderProgram";
import { Planet } from "../scene/Planet";
import { doubleToTwoFloats2 } from "../math/doubleToTwoFloats";
import type { WebGLBufferExt, WebGLTextureExt } from "../webgl/Handler";
import { RADIANS } from "../math";
import { CUBE_FACES } from "../proj/cube";
import type { CubeProj } from "../proj/cube";

export class GeoImageCreator {
    public MAX_FRAMES: number;

    protected _gridSize: number;
    protected _planet: Planet;
    public _framebuffer: Framebuffer | null;
    protected _framebufferMercProj: Framebuffer | null;
    public _texCoordsBuffer: WebGLBufferExt | null;
    public _indexBuffer: WebGLBufferExt | null;
    protected _currentFrame: number;
    protected _queue: BaseGeoImage[];
    protected _animate: BaseGeoImage[];
    protected _quadTexCoordsBuffer: WebGLBufferExt | null;
    protected _quadVertexBuffer: WebGLTextureExt | null;

    constructor(planet: Planet, maxFrames: number = 5) {
        this.MAX_FRAMES = maxFrames;
        this._gridSize = 64;
        this._planet = planet;
        this._framebuffer = null;
        this._framebufferMercProj = null;
        this._texCoordsBuffer = null;
        this._indexBuffer = null;
        this._currentFrame = 0;
        this._queue = [];
        this._animate = [];
        this._quadTexCoordsBuffer = null;
        this._quadVertexBuffer = null;
    }

    public init() {
        this._initShaders();
        this._initBuffers();
    }

    /**
     * Creates geoImage corners coordinates grid buffer.
     * @public
     * @param{Array.<LonLat>} c - GeoImage corners coordinates.
     * @param{boolean} [toMerc=false] - Transform to web mercator.
     * @return{WebGLBuffer} Grid coordinates buffer.
     */
    public createGridBuffer(c: LonLat[], toMerc: boolean = false): [WebGLBufferExt, WebGLBufferExt] {
        let gs = this._gridSize;

        let v03 = new LonLat((c[3].lon - c[0].lon) / gs, (c[3].lat - c[0].lat) / gs),
            v12 = new LonLat((c[2].lon - c[1].lon) / gs, (c[2].lat - c[1].lat) / gs),
            v01 = new LonLat((c[1].lon - c[0].lon) / gs, (c[1].lat - c[0].lat) / gs),
            v32 = new LonLat((c[2].lon - c[3].lon) / gs, (c[2].lat - c[3].lat) / gs);

        const size = (gs + 1) * (gs + 1) * 2,
            halfSize = size / 2;

        let gridHigh = new Float32Array(size),
            gridLow = new Float32Array(size);

        let lonLatArr = new Array(halfSize);

        let kh = 0,
            kl = 0,
            k = 0;

        let tempArr = new Float32Array(2);

        for (let i = 0; i <= gs; i++) {
            let P03i = new LonLat(c[0].lon + i * v03.lon, c[0].lat + i * v03.lat),
                P12i = new LonLat(c[1].lon + i * v12.lon, c[1].lat + i * v12.lat);

            for (let j = 0; j <= gs; j++) {
                let P01j = new LonLat(c[0].lon + j * v01.lon, c[0].lat + j * v01.lat),
                    P32j = new LonLat(c[3].lon + j * v32.lon, c[3].lat + j * v32.lat);
                let xx = utils.getLinesIntersectionLonLat(P03i, P12i, P01j, P32j)!;
                doubleToTwoFloats2(xx.lon, tempArr);
                gridHigh[kh++] = tempArr[0];
                gridLow[kl++] = tempArr[1];
                doubleToTwoFloats2(xx.lat, tempArr);
                gridHigh[kh++] = tempArr[0];
                gridLow[kl++] = tempArr[1];

                lonLatArr[k++] = xx;
            }
        }

        if (toMerc) {
            for (let i = 0; i < halfSize; i++) {
                let c = lonLatArr[i].forwardMercator();
                doubleToTwoFloats2(c.lon, tempArr);
                gridHigh[i * 2] = tempArr[0];
                gridLow[i * 2] = tempArr[1];

                doubleToTwoFloats2(c.lat, tempArr);
                gridHigh[i * 2 + 1] = tempArr[0];
                gridLow[i * 2 + 1] = tempArr[1];
            }
        }

        return [
            this._planet.renderer!.handler.createArrayBuffer(gridHigh, 2, halfSize),
            this._planet.renderer!.handler.createArrayBuffer(gridLow, 2, halfSize)
        ];
    }

    /**
     * Returns the points of the geoImage grid, row by row from the first corner, as createGridBuffer makes them.
     * @public
     * @param{Array.<LonLat>} c - GeoImage corners coordinates.
     * @return{Array.<LonLat>} -
     */
    public createGridLonLat(c: LonLat[]): LonLat[] {
        const gs = this._gridSize;

        const v03 = new LonLat((c[3].lon - c[0].lon) / gs, (c[3].lat - c[0].lat) / gs),
            v12 = new LonLat((c[2].lon - c[1].lon) / gs, (c[2].lat - c[1].lat) / gs),
            v01 = new LonLat((c[1].lon - c[0].lon) / gs, (c[1].lat - c[0].lat) / gs),
            v32 = new LonLat((c[2].lon - c[3].lon) / gs, (c[2].lat - c[3].lat) / gs);

        const points: LonLat[] = [];

        for (let i = 0; i <= gs; i++) {
            const P03i = new LonLat(c[0].lon + i * v03.lon, c[0].lat + i * v03.lat),
                P12i = new LonLat(c[1].lon + i * v12.lon, c[1].lat + i * v12.lat);

            for (let j = 0; j <= gs; j++) {
                const P01j = new LonLat(c[0].lon + j * v01.lon, c[0].lat + j * v01.lat),
                    P32j = new LonLat(c[3].lon + j * v32.lon, c[3].lat + j * v32.lat);
                points.push(utils.getLinesIntersectionLonLat(P03i, P12i, P01j, P32j)!);
            }
        }

        return points;
    }

    /**
     * Creates the buffers of the grid point directions from the planet center, the surface normals in them, each
     * value in a float and the rest of it in another one.
     * @public
     * @param{Array.<LonLat>} points - Grid points.
     * @return{Array.<WebGLBuffer>} -
     */
    public createGridDirectionsBuffer(points: LonLat[]): [WebGLBufferExt, WebGLBufferExt] {
        const high = new Float32Array(points.length * 3),
            low = new Float32Array(points.length * 3);

        for (let i = 0; i < points.length; i++) {
            const lon = points[i].lon * RADIANS,
                lat = points[i].lat * RADIANS,
                d = [Math.cos(lat) * Math.cos(lon), Math.cos(lat) * Math.sin(lon), Math.sin(lat)];

            for (let k = 0; k < 3; k++) {
                high[i * 3 + k] = d[k];
                low[i * 3 + k] = d[k] - high[i * 3 + k];
            }
        }

        const h = this._planet.renderer!.handler;

        return [h.createArrayBuffer(high, 3, points.length), h.createArrayBuffer(low, 3, points.length)];
    }

    /**
     * Sets the uniforms of the geoImageTransformPlane program for a rectangle of positions on a cube face: the face
     * plane with the rectangle center on it.
     * @public
     */
    public setCubePlaneUniforms(
        shu: Record<string, any>,
        proj: CubeProj,
        face: number,
        s0: number,
        s1: number,
        t0: number,
        t1: number
    ) {
        const gl = this._planet.renderer!.handler.gl!,
            f = CUBE_FACES[face],
            u = proj.toPlane((s0 + s1) * 0.5),
            v = proj.toPlane((t0 + t1) * 0.5),
            x = f[0] + u * f[3] + v * f[6],
            y = f[1] + u * f[4] + v * f[7],
            z = f[2] + u * f[5] + v * f[8],
            l = Math.sqrt(x * x + y * y + z * z),
            high = new Float32Array([x / l, y / l, z / l]),
            low = new Float32Array([x / l - high[0], y / l - high[1], z / l - high[2]]),
            k = proj.equiangular ? 4.0 / Math.PI : 1.0;

        gl.uniform3fv(shu.centerHigh, high);
        gl.uniform3fv(shu.centerLow, low);
        gl.uniform3fv(shu.axisC, [f[0], f[1], f[2]]);
        gl.uniform3fv(shu.axisU, [f[3], f[4], f[5]]);
        gl.uniform3fv(shu.axisV, [f[6], f[7], f[8]]);
        gl.uniform2fv(shu.center, [u, v]);
        gl.uniform2fv(shu.scale, [k / (s1 - s0), k / (t1 - t0)]);
        gl.uniform1f(shu.equiangular, proj.equiangular ? 1 : 0);
    }

    /**
     * Returns true when there is nothing left to rasterize and no animated
     * geo images(i.e. GeoVideo) are being played.
     * @public
     * @returns {boolean}
     */
    public get isIdle(): boolean {
        if (this._queue.length !== 0) {
            return false;
        }
        for (let i = 0; i < this._animate.length; i++) {
            if (this._animate[i].getVisibility()) {
                return false;
            }
        }
        return true;
    }

    public frame() {
        let i = this.MAX_FRAMES;
        while (i-- && this._queue.length) {
            const q = this._queue.shift()!;
            q._isRendering = false;
            q.rendering();
            q.events.dispatch(q.events.loadend);
        }

        i = this._animate.length;
        while (i--) {
            let ai = this._animate[i];
            if (ai.getVisibility()) {
                ai.rendering();
            }
        }
    }

    public add(geoImage: BaseGeoImage) {
        if (!geoImage._isRendering) {
            geoImage._isRendering = true;
            if (geoImage._animate) {
                this._animate.push(geoImage);
            } else {
                this._queue.push(geoImage);
            }

            this._planet.renderer?.requestRedraw();
        }
    }

    public remove(geoImage: BaseGeoImage) {
        geoImage._creationProceeding = false;
        geoImage._isRendering = false;

        for (let i = 0; i < this._queue.length; i++) {
            if (this._queue[i].isEqual(geoImage)) {
                this._queue.splice(i, 1);
                break;
            }
        }

        for (let i = 0; i < this._animate.length; i++) {
            if (this._animate[i].isEqual(geoImage)) {
                this._animate.splice(i, 1);
                break;
            }
        }
    }

    protected _initBuffers() {
        let h = this._planet.renderer!.handler!;

        this._framebuffer = new Framebuffer(h, { width: 2, height: 2, useDepth: false });
        this._framebuffer.init();

        this._framebufferMercProj = new Framebuffer(h, { width: 2, height: 2, useDepth: false });
        this._framebufferMercProj.init();

        let gs = Math.log2(this._gridSize);

        this._texCoordsBuffer = this._planet._textureCoordsBufferCache[gs];

        this._indexBuffer = this._planet._indexesCache[gs][gs][gs][gs][gs].buffer;

        this._quadTexCoordsBuffer = h.createArrayBuffer(new Float32Array([0, 1, 1, 1, 0, 0, 1, 0]), 2, 4);
        this._quadVertexBuffer = h.createArrayBuffer(new Float32Array([-1, 1, 1, 1, -1, -1, 1, -1]), 2, 4);
    }

    protected _initShaders() {
        this._planet.renderer!.handler.addProgram(
            new ShaderProgram("geoImageTransformPlane", {
                uniforms: {
                    sourceTexture: "sampler2d",
                    isFullExtent: "bool",
                    decodeSourceSRGB: "bool",
                    centerHigh: "vec3",
                    centerLow: "vec3",
                    axisU: "vec3",
                    axisV: "vec3",
                    axisC: "vec3",
                    center: "vec2",
                    scale: "vec2",
                    equiangular: "float"
                },
                attributes: {
                    cornersHigh: "vec3",
                    cornersLow: "vec3",
                    texCoords: "vec2"
                },
                vertexShader: `attribute vec3 cornersHigh;
                    attribute vec3 cornersLow;
                    attribute vec2 texCoords;
                    uniform vec3 centerHigh;
                    uniform vec3 centerLow;
                    uniform vec3 axisU;
                    uniform vec3 axisV;
                    uniform vec3 axisC;
                    uniform vec2 center;
                    uniform vec2 scale;
                    uniform float equiangular;
                    varying vec2 v_texCoords;
                    void main() {
                        v_texCoords = texCoords;
                        vec3 q = (cornersHigh - centerHigh) + (cornersLow - centerLow);
                        float qc = dot(q, axisC);
                        float w = dot(centerHigh, axisC) + dot(centerLow, axisC) + qc;
                        if (abs(w) < 1e-7) {
                            w = -1e-7;
                        }
                        vec2 d = vec2(dot(q, axisU) - center.x * qc, dot(q, axisV) - center.y * qc) / w;
                        if (equiangular > 0.5 && w > 0.0) {
                            d = vec2(atan(d.x, 1.0 + center.x * (center.x + d.x)), atan(d.y, 1.0 + center.y * (center.y + d.y)));
                        }
                        gl_Position = vec4(d * scale * vec2(1.0, -1.0) * w, 0.0, w);
                    }`,
                fragmentShader: `precision highp float;
                    uniform sampler2D sourceTexture;
                    uniform bool isFullExtent;
                    uniform bool decodeSourceSRGB;
                    varying vec2 v_texCoords;
                    void main () {
                        if(!isFullExtent && (v_texCoords.x <= 0.001 || v_texCoords.x >= 0.999 ||
                            v_texCoords.y <= 0.001 || v_texCoords.y >= 0.999)) {
                            discard;
                        }
                        vec4 color = texture2D(sourceTexture, v_texCoords);
                        if (decodeSourceSRGB) {
                            color.rgb = pow(color.rgb, vec3(2.2));
                        }
                        gl_FragColor = color;
                    }`
            })
        );

        this._planet.renderer!.handler.addProgram(
            new ShaderProgram("geoImageTransform", {
                uniforms: {
                    sourceTexture: "sampler2d",
                    extentParamsHigh: "vec4",
                    extentParamsLow: "vec4",
                    isFullExtent: "bool",
                    decodeSourceSRGB: "bool"
                },
                attributes: {
                    cornersHigh: "vec2",
                    cornersLow: "vec2",
                    texCoords: "vec2"
                },
                vertexShader: `attribute vec2 cornersHigh;
                     attribute vec2 cornersLow;
                      attribute vec2 texCoords;
                      uniform vec4 extentParamsHigh;
                      uniform vec4 extentParamsLow;
                      varying vec2 v_texCoords;
                      void main() {
                          v_texCoords = texCoords;
                          vec2 highDiff = cornersHigh - extentParamsHigh.xy;
                          vec2 lowDiff = cornersLow - extentParamsLow.xy;
                          gl_Position = vec4((-1.0 + (highDiff * step(1.0, length(highDiff)) + lowDiff) * extentParamsHigh.zw) * vec2(1.0, -1.0), 0.0, 1.0);
                      }`,
                fragmentShader: `precision highp float;
                        uniform sampler2D sourceTexture;
                        uniform bool isFullExtent;
                        uniform bool decodeSourceSRGB;
                        varying vec2 v_texCoords;
                        void main () {
                            if(!isFullExtent && (v_texCoords.x <= 0.001 || v_texCoords.x >= 0.999 ||
                                v_texCoords.y <= 0.001 || v_texCoords.y >= 0.999)) {
                                discard;
                            }
                            vec4 color = texture2D(sourceTexture, v_texCoords);
                            if (decodeSourceSRGB) {
                                color.rgb = pow(color.rgb, vec3(2.2));
                            }
                            gl_FragColor = color;
                        }`
            })
        );
    }
}