quadTree/QuadTreeStrategy.tstypescript
import { EPSG3857 } from "../proj/EPSG3857";
import { Layer } from "../layer/Layer";
import { Vector } from "../layer/Vector";
import { Node } from "../quadTree/Node";
import { Planet } from "../scene/Planet";
import { Proj } from "../proj/Proj";
import { LonLat } from "../LonLat";
import { getTileCellExtent, getTileCellIndex, Segment, TILEGROUP_COMMON } from "../segment/Segment";
import { Extent } from "../Extent";
import { EntityCollectionsTreeStrategy } from "./EntityCollectionsTreeStrategy";
import { PlanetCamera } from "../camera";
import * as math from "../math";
import { createEvents, EventsHandler } from "../Events";

export type QuadTreeStrategyEventsList = ["rendercompleted", "terraincompleted"];

const CUR_LOD_SIZE = 256; //px
const MIN_LOD_SIZE = 512; //px
const MAX_LOD_SIZE = 256; //px

const HORIZON_TANGENT = 0.81;

export interface QuadTreeStrategyParams {
    planet: Planet;
    name?: string;
    proj?: Proj;
    maxEqualZoomAltitude?: number;
    minEqualZoomAltitude?: number;
    minEqualZoomCameraSlope?: number;
    transitionOpacityEnabled?: boolean;
}

export class QuadTreeStrategy {
    public events: EventsHandler<QuadTreeStrategyEventsList>;

    public maxZoomLimit: number = 50;

    public name: string;
    public projection: Proj;
    public readonly planet: Planet;

    public lodCamera: PlanetCamera | null = null;

    public disableNeighbors: boolean = false;

    /**
     * grid tree list.
     * @protected
     * @type {Node[]}
     */
    protected _quadTreeList: Node[];

    /**
     * Current visible mercator segments tree nodes array.
     * @public
     * @type {Node}
     */
    public _visibleNodes: Record<number, Node>;

    /**
     * Planet's segments collected for rendering frame.
     * @public
     * @type {Node}
     */
    public _renderedNodes: Node[];

    /**
     * Geocentric distance range of the terrain collected for the current rendering frame.     * @public
     * @type {number}
     */
    public minTerrainRadius: number = 0;
    public maxTerrainRadius: number = 0;

    public _minTerrainRadiusAcc: number = Infinity;
    public _maxTerrainRadiusAcc: number = -Infinity;

    public _renderedNodesInFrustum: Node[][];

    public _fadingNodes: Map<number, Node>;

    public _fadingOpaqueSegments: Segment[];

    public _transparentSegments: Segment[];

    /**
     * Current visible minimal zoom index planet segment.
     * @public
     * @type {number}
     */
    public minCurrZoom: number;

    /**
     * Current visible maximal zoom index planet segment.
     * @public
     * @type {number}
     */
    public maxCurrZoom: number;

    protected _transitionOpacityEnabled: boolean;

    public _viewExtent: Extent;

    /**
     * Level of details of visible segments.
     * @protected
     * @type {number}
     */
    protected _lodSize: number;
    protected _curLodSize: number;
    protected _minLodSize: number;
    public _maxLodSize: number;

    public maxEqualZoomAltitude: number;
    public minEqualZoomAltitude: number;
    public minEqualZoomCameraSlope: number;

    public _renderCompleted: boolean;
    public _renderCompletedActivated: boolean;

    public _terrainCompleted: boolean;
    public _terrainCompletedActivated: boolean;

    protected _skipPreRender: boolean = false;

    constructor(params: QuadTreeStrategyParams) {
        this.events = createEvents<QuadTreeStrategyEventsList>(QUADTREESTRATEGY_EVENTS);

        this.name = params.name || "";
        this.projection = params.proj || EPSG3857;
        this.planet = params.planet;
        this._quadTreeList = [];
        this._visibleNodes = {};

        this._renderedNodes = [];
        this._renderedNodesInFrustum = [];

        this._fadingNodes = new Map<number, Node>();
        this._fadingOpaqueSegments = [];
        this._transparentSegments = [];

        this.minCurrZoom = math.MAX;
        this.maxCurrZoom = math.MIN;

        this._viewExtent = new Extent(new LonLat(180, 180), new LonLat(-180, -180));

        this._lodSize = CUR_LOD_SIZE;
        this._curLodSize = CUR_LOD_SIZE;
        this._minLodSize = MIN_LOD_SIZE;
        this._maxLodSize = MAX_LOD_SIZE;

        this.maxEqualZoomAltitude = params.maxEqualZoomAltitude || 15000000.0;
        this.minEqualZoomAltitude = params.minEqualZoomAltitude || 10000.0;
        this.minEqualZoomCameraSlope = params.minEqualZoomCameraSlope || 0.8;

        this._renderCompleted = false;
        this._renderCompletedActivated = false;

        this._terrainCompleted = false;
        this._terrainCompletedActivated = false;

        this._transitionOpacityEnabled =
            params.transitionOpacityEnabled != undefined ? params.transitionOpacityEnabled : true;
    }

    public get lodSize(): number {
        return this._lodSize;
    }

    public setLodSize(currentLodSize: number, minLodSize?: number, maxLodSize?: number) {
        this._maxLodSize = maxLodSize || this._maxLodSize;
        this._minLodSize = minLodSize || this._minLodSize;
        this._curLodSize = currentLodSize;
        this._renderCompletedActivated = false;
        this._terrainCompletedActivated = false;
        this.planet.renderer?.requestRedraw();
    }

    public createEntityCollectionsTreeStrategy(layer: Vector, nodeCapacity: number): EntityCollectionsTreeStrategy {
        return new EntityCollectionsTreeStrategy(this, layer, nodeCapacity);
    }

    public destroyBranches() {
        this._renderCompletedActivated = false;
        this._terrainCompletedActivated = false;
        for (let i = 0, len = this._quadTreeList.length; i < len; i++) {
            this._quadTreeList[i].destroyBranches();
        }
        this.planet.renderer?.requestRedraw();
    }

    /**
     * clears layer material from the quad tree list.
     * @param layer
     * @param keepRendered if true, keeps materials that are currently rendered.
     */
    public clearLayerMaterial(layer: Layer, keepRendered: boolean = false) {
        let lid = layer.__id;
        for (let i = 0, len = this._quadTreeList.length; i < len; i++) {
            this._quadTreeList[i].traverseTree((node: Node) => {
                if (keepRendered && this._renderedNodes.includes(node)) {
                    return;
                }
                let mats = node.segment.materials;
                if (mats[lid]) {
                    mats[lid].clear();
                    //@ts-ignore
                    mats[lid] = null;
                    //delete mats[lid];
                }
            });
        }
    }

    public get terrainReady(): boolean {
        return this._terrainCompleted && this._terrainCompletedActivated;
    }

    /**
     * Returns true when every rendered segment has got its materials applied and
     * the completion has already been announced.
     * @public
     * @returns {boolean}
     */
    public get renderReady(): boolean {
        return this._renderCompleted && this._renderCompletedActivated;
    }

    protected _checkRendercompleted() {
        if (!this.renderReady || !this.terrainReady) {
            this.planet.renderer!.requestRedraw();
        }

        if (this._renderCompleted) {
            if (!this._renderCompletedActivated) {
                this._renderCompletedActivated = true;
                this.events.dispatch(this.events.rendercompleted, true);
            }
        } else {
            this._renderCompletedActivated = false;
        }
        this._renderCompleted = true;

        if (this._terrainCompleted) {
            if (!this._terrainCompletedActivated) {
                this._terrainCompletedActivated = true;
                this.events.dispatch(this.events.terraincompleted, true);
            }
        } else {
            this._terrainCompletedActivated = false;
        }

        this._terrainCompleted = true;
    }

    protected _initEvents() {
        this.planet.renderer!.events.on("resize", () => {
            this._renderCompletedActivated = false;
            this._terrainCompletedActivated = false;
        });

        this.planet.renderer!.events.on("postdraw", () => {
            this._checkRendercompleted();
        });
    }

    protected _updateRenderedNodesInFrustumArray(camera: PlanetCamera) {
        this._renderedNodesInFrustum = new Array(camera.frustums.length);
        for (let i = 0, len = this._renderedNodesInFrustum.length; i < len; i++) {
            this._renderedNodesInFrustum[i] = [];
        }
    }

    public init(camera: PlanetCamera) {
        this._initEvents();

        this._updateRenderedNodesInFrustumArray(camera);

        camera.events.on("frustumschanged", () => {
            this._updateRenderedNodesInFrustumArray(camera);
        });

        this.preRender();
        this.clearRenderedNodes();
        this.preLoad();
    }

    public clearRenderedNodes() {
        this._clearRenderedNodeList();
        this._clearRenderNodesInFrustum();
        this.planet.renderer?.requestRedraw();
    }

    protected _clearRenderedNodeList() {
        this._renderedNodes.length = 0;
        this._renderedNodes = [];
    }

    protected _clearRenderNodesInFrustum() {
        1;
        for (let i = 0, len = this._renderedNodesInFrustum.length; i < len; i++) {
            this._renderedNodesInFrustum[i].length = 0;
            this._renderedNodesInFrustum[i] = [];
        }
    }

    protected _collectRenderedNodesMaxZoom(cam: PlanetCamera) {
        if (
            cam.isOrthographic ||
            (cam.slope > this.minEqualZoomCameraSlope &&
                cam._lonLat.height < this.maxEqualZoomAltitude &&
                cam._lonLat.height > this.minEqualZoomAltitude)
        ) {
            this.minCurrZoom = this.maxCurrZoom;

            let temp = this._renderedNodes,
                rf = this._renderedNodesInFrustum,
                temp2 = [];

            this._clearRenderNodesInFrustum();
            this._renderedNodes = [];

            for (let i = 0, len = temp.length; i < len; i++) {
                let ri = temp[i];
                let ht = ri.segment.centerNormal.dot(cam.getBackward());
                if (ri.segment.tileZoom === this.maxCurrZoom || ht < HORIZON_TANGENT) {
                    this._renderedNodes.push(ri);
                    let k = 0,
                        inFrustum = ri.inFrustum;
                    while (inFrustum) {
                        if (inFrustum & 1) {
                            rf[k].push(ri);
                        }
                        k++;
                        inFrustum >>= 1;
                    }
                } else {
                    temp2.push(ri);
                }
            }

            for (let i = 0, len = temp2.length; i < len; i++) {
                temp2[i].renderTree(cam, this.maxCurrZoom, null, false, temp2[i]);
            }
        }
    }

    public set transitionOpacityEnabled(isEnabled: boolean) {
        this._transitionOpacityEnabled = isEnabled;
        //@todo: set render nodes transition opacity to one
    }

    public get transitionOpacityEnabled(): boolean {
        return this._transitionOpacityEnabled;
    }

    /**
     * Collects visible quad nodes.
     * @protected
     */
    public collectRenderNodes(cam: PlanetCamera) {
        if (this._skipPreRender) {
            this._lodSize = math.lerp(cam.slope < 0.0 ? 0.0 : cam.slope, this._curLodSize, this._minLodSize);
            cam._insideSegment = null;

            // clear first
            this._clearRenderedNodeList();
            this._clearRenderNodesInFrustum();

            this._viewExtent.southWest.set(180, 180);
            this._viewExtent.northEast.set(-180, -180);

            // todo: replace to camera
            this.minCurrZoom = math.MAX;
            this.maxCurrZoom = math.MIN;

            this._minTerrainRadiusAcc = Infinity;
            this._maxTerrainRadiusAcc = -Infinity;

            this._collectRenderNodes(cam);

            if (this._minTerrainRadiusAcc <= this._maxTerrainRadiusAcc) {
                this.minTerrainRadius = this._minTerrainRadiusAcc;
                this.maxTerrainRadius = this._maxTerrainRadiusAcc;
            }

            if (this.maxCurrZoom === math.MIN) {
                this.minCurrZoom = 0;
                this.maxCurrZoom = 0;
            }

            this._collectRenderedNodesMaxZoom(cam);

            // main camera effect
            this._fadingNodes.clear();

            if (this._transitionOpacityEnabled) {
                let opaqueNodes: Node[] = [];

                for (let i = 0; i < this._renderedNodes.length; i++) {
                    let ri = this._renderedNodes[i];
                    // it's not impossible to move the code into Node.addToRender, because
                    // we can't know actual state before _collectRenderedNodesMaxZoom pass
                    ri._collectFadingNodes();
                    ri._refreshTransitionOpacity();

                    if (ri.segment._transitionOpacity >= 1.0) {
                        ri.clearNeighbors();
                        ri.getRenderedNodesNeighbors(opaqueNodes);
                        opaqueNodes.push(ri);
                    } else {
                        for (let j = 0; j < ri._fadingNodes.length; j++) {
                            let rij = ri._fadingNodes[j];
                            if (rij.segment && rij.segment._transitionOpacity >= 1.0) {
                                rij.clearNeighbors();
                                rij.getRenderedNodesNeighbors(opaqueNodes);
                                opaqueNodes.push(rij);
                            }
                        }
                    }
                }
            }
        }

        this._skipPreRender = true;
    }

    public preRender() {
        this._skipPreRender = false;
        for (let i = 0; i < this._quadTreeList.length; i++) {
            let quadTree = this._quadTreeList[i];
            quadTree.createChildNodes();
            quadTree.segment.createPlainSegment();

            for (let j = 0; j < quadTree.nodes.length; j++) {
                quadTree.nodes[j].segment.createPlainSegment();
            }
        }
    }

    public preLoad() {
        for (let i = 0; i < this._quadTreeList.length; i++) {
            let quadTree = this._quadTreeList[i];
            quadTree.segment.passReady = true;
            quadTree.renderNode(1);
            this.planet.normalMapCreator.drawSingle(quadTree.segment);

            for (let j = 0; j < quadTree.nodes.length; j++) {
                quadTree.nodes[j].segment.passReady = true;
                quadTree.nodes[j].renderNode(1);
                this.planet._normalMapCreator.drawSingle(quadTree.nodes[j].segment);
            }
        }
    }

    protected _clearVisibleNodes() {
        this._visibleNodes = {};
    }

    protected _collectRenderNodes(camera: PlanetCamera) {
        this._clearVisibleNodes();
        for (let i = 0; i < this._quadTreeList.length; i++) {
            this._quadTreeList[i].renderTree(camera, 0, null);
        }
    }

    public clear() {
        for (let i = 0; i < this._quadTreeList.length; i++) {
            this._quadTreeList[i].clearTree();
        }
    }

    public get quadTreeList(): Node[] {
        return this._quadTreeList;
    }

    public getTileXY(lonLat: LonLat, zoom: number): [number, number, number, number] {
        let z = zoom,
            x: number,
            y: number,
            pz = 1 << z;

        x = getTileCellIndex(lonLat.lon, 360 / pz, -180);
        y = getTileCellIndex(lonLat.lat, 180 / pz, 90);

        return [x, y, z, TILEGROUP_COMMON];
    }

    public getLonLatTileOffset(lonLat: LonLat, x: number, y: number, z: number, gridSize: number): [number, number] {
        let extent: Extent;

        let worldExtent = Extent.createFromArray([-180, -90, 180, 90]);
        extent = getTileCellExtent(x, y, z, worldExtent);

        let sizeImgW = extent.getWidth() / (gridSize - 1),
            sizeImgH = extent.getHeight() / (gridSize - 1);

        let i = gridSize - Math.ceil((lonLat.lat - extent.southWest.lat) / sizeImgH) - 1,
            j = Math.floor((lonLat.lon - extent.southWest.lon) / sizeImgW);

        return [i, j];
    }

    public collectVisibleNode(node: Node) {
        this._visibleNodes[node.nodeId] = node;
    }
}

const QUADTREESTRATEGY_EVENTS: QuadTreeStrategyEventsList = ["rendercompleted", "terraincompleted"];