quadTree/QuadTreeStrategy.tstypescript
import { EPSG3857 } from "../proj/EPSG3857";
import { Layer } from "../layer/Layer";
import { Vector } from "../layer/Vector";
import { Node } from "../quadTree/Node";
import { MAX_RENDERED_NODES, NE, NW, SE, SW } from "../quadTree/quadTree";
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 type { Camera } from "../camera/Camera";
import type { DepthCamera } from "../renderer/depthCamera/DepthCamera";
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;

const __emptyArray__: readonly Node[] = [];

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 disableNeighbors: boolean = false;

    /**
     * Width of a root tile in degrees of longitude, a tile of the zoom is 2^zoom times narrower. The native zoom of
     * a raster source is found by it.
     * @public
     * @type {number}
     */
    public rootTileWidth: number = 360;

    /**
     * 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[][];

    /**
     * Squared distance from the camera to the horizon of the highest terrain point.
     * @public
     * @type {number}
     */
    public _horizonDist2: number = 0;

    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;

    /**
     * Opaque nodes collected for the renderer depth cameras, [depth camera index][frustumIndex].
     * @public
     */
    public _viewCamerasRenderedNodes: Node[][][] = [];

    /**
     * View camera collection counter, a node gone through by the last collection is marked with it.
     * @public
     */
    public _viewWalkFrame: number = 0;

    /**
     * Neighbor search counter, an opaque node of the current frame is marked with it.
     * @public
     */
    public _neighborFrame: number = 0;

    /**
     * True in a frame the segment corners can have changed in, see Segment._equalizeCorner.
     * @public
     */
    public _cornersChanged: boolean = true;

    /**
     * Set by a segment that has moved its corner, the others see it in the next frame.
     * @public
     */
    public _cornerMoved: boolean = false;

    protected _opaqueNodesIds: number[] = [];

    /**
     * Normal map creator version the normal map edges were compared at, see Segment._checkNormalMapEdges.
     */
    protected _normalMapEdgesVersion: number = -1;

    /**
     * False while a node only the view cameras render waits for its terrain. Keeps the frames coming in the
     * idle mode, apart from _terrainCompleted, which stays about the main camera view.
     * @public
     */
    public _viewCamerasTerrainCompleted: boolean = true;

    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._viewCamerasTerrainCompleted) {
            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();
        for (let i = 0; i < this._viewCamerasRenderedNodes.length; i++) {
            this._clearViewCameraRenderedNodes(i);
        }
        this.planet.renderer?.requestRedraw();
    }

    /**
     * Nodes of a renderer depth camera: where it sees the main camera nodes it gets the same ones, elsewhere
     * nodes chosen with the main camera LOD.
     * @public
     * @param {number} slot - Depth camera index in the renderer depth camera handler.
     * @param {number} [frustumIndex=0] - Depth camera frustum index.
     */
    public getViewCameraRenderedNodes(slot: number, frustumIndex: number = 0): readonly Node[] {
        const rn = this._viewCamerasRenderedNodes[slot];
        return (rn && rn[frustumIndex]) || __emptyArray__;
    }

    protected _clearViewCameraRenderedNodes(slot: number) {
        const rn = this._viewCamerasRenderedNodes[slot];
        if (!rn) return;
        for (let i = 0; i < rn.length; i++) {
            rn[i].length = 0;
        }
    }

    public _addViewCameraNode(slot: number, node: Node, inFrustum: number) {
        const rf = this._viewCamerasRenderedNodes[slot];
        let k = 0;
        while (inFrustum) {
            if (inFrustum & 1) {
                rf[k].push(node);
            }
            k++;
            inFrustum >>= 1;
        }
    }

    /**
     * Mask of the view camera frustums whose node lists are not full yet.
     * @public
     */
    public _getViewCameraOpenFrustums(slot: number): number {
        const rf = this._viewCamerasRenderedNodes[slot];
        let mask = 0;
        for (let i = 0; i < rf.length; i++) {
            if (rf[i].length < MAX_RENDERED_NODES) {
                mask |= 1 << i;
            }
        }
        return mask;
    }

    protected _addViewCameraOpaqueNode(slot: number, camera: Camera, frustumMask: number, node: Node) {
        const inFrustum = node.getFrustumMask(camera) & frustumMask;
        if (inFrustum) {
            this._addViewCameraNode(slot, node, inFrustum);
        }
    }

    /**
     * Runs after the main collection is complete. Takes the opaque main nodes the view camera sees, then walks
     * the tree for the parts the main camera has not rendered, never going under a main rendered node.
     * @protected
     */
    protected _collectViewCamerasRenderNodes(mainCamera: PlanetCamera) {
        const planet = this.planet;

        const depthCameras: DepthCamera[] =
            planet.renderer && !planet.disableDepthCamera && planet.quadTreeStrategy === this
                ? planet.renderer.depthCameraHandler._depthCameras
                : [];

        const lists = this._viewCamerasRenderedNodes;

        lists.length = depthCameras.length;

        this._viewWalkFrame++;
        this._viewCamerasTerrainCompleted = true;

        const mainState = this._renderedNodes.length < MAX_RENDERED_NODES;

        for (let slot = 0; slot < depthCameras.length; slot++) {
            const dc = depthCameras[slot];
            const rf = lists[slot] || (lists[slot] = []);

            if (!dc.enabled || !dc.initialized) {
                rf.length = 0;
                continue;
            }

            const camera = dc.camera;
            const frustumMask = dc.frustumMask;

            for (let i = rf.length; i < camera.frustums.length; i++) {
                rf[i] = [];
            }

            rf.length = camera.frustums.length;
            this._clearViewCameraRenderedNodes(slot);

            if (mainState) {
                const rn = this._renderedNodes;

                for (let i = 0; i < rn.length; i++) {
                    const ri = rn[i];
                    if (!this._transitionOpacityEnabled || ri.segment._transitionOpacity >= 1.0) {
                        this._addViewCameraOpaqueNode(slot, camera, frustumMask, ri);
                    }
                }

                // The opaque nodes shown instead of the fading in ones, each of them once. Exactly 1 is the frame the
                // fading in node has become opaque itself and is taken above.
                for (const fj of this._fadingNodes.values()) {
                    if (fj.segment && fj.segment._transitionOpacity > 1.0) {
                        this._addViewCameraOpaqueNode(slot, camera, frustumMask, fj);
                    }
                }
            }

            this._collectViewRenderNodes(slot, camera, frustumMask, mainCamera, mainState);
        }
    }

    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._updateHorizonDist(cam);

            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();

            const opaqueNodes: Node[] = [];

            this._neighborFrame++;

            if (this._transitionOpacityEnabled) {
                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) {
                        this._addOpaqueNode(opaqueNodes, ri);
                    } else {
                        for (let j = 0; j < ri._fadingNodes.length; j++) {
                            let rij = ri._fadingNodes[j];
                            if (rij.segment && rij.segment._transitionOpacity >= 1.0) {
                                this._addOpaqueNode(opaqueNodes, rij);
                            }
                        }
                    }
                }
            } else {
                for (let i = 0; i < this._renderedNodes.length; i++) {
                    this._addOpaqueNode(opaqueNodes, this._renderedNodes[i]);
                }
            }

            this._linkOpaqueNodes(opaqueNodes);

            this._collectViewCamerasRenderNodes(cam);
        }

        this._skipPreRender = true;
    }

    /**
     * Takes the node into the opaque nodes of the frame, the neighbors are searched among them. A parent fading
     * under its children comes here from each of them, it is taken once.
     * @protected
     */
    protected _addOpaqueNode(opaqueNodes: Node[], node: Node) {
        if (node._neighborFrame !== this._neighborFrame) {
            node._neighborFrame = this._neighborFrame;
            node.clearNeighbors();
            opaqueNodes.push(node);
        }
    }

    protected _linkOpaqueNodes(opaqueNodes: Node[]) {
        if (this.disableNeighbors) return;

        const ids = this._opaqueNodesIds;

        let changed = this._cornerMoved || ids.length !== opaqueNodes.length;

        for (let i = 0; i < opaqueNodes.length; i++) {
            const ni = opaqueNodes[i];
            ni.getRenderedNodesNeighbors(this._neighborFrame);
            changed = changed || ids[i] !== ni.__id || ni.segment.readyToEngage;
            ids[i] = ni.__id;
        }

        ids.length = opaqueNodes.length;

        // The edges change only with the nodes on the screen or with a new normal map
        const terrain = this.planet && this.planet.terrain,
            nmc = this.planet && this.planet._normalMapCreator;
        if (terrain && terrain.equalizeNormals && nmc && (changed || nmc.__version__ !== this._normalMapEdgesVersion)) {
            this._normalMapEdgesVersion = nmc.__version__;
            for (let i = 0; i < opaqueNodes.length; i++) {
                opaqueNodes[i].segment._checkNormalMapEdges();
            }
        }

        this._cornersChanged = changed;
        this._cornerMoved = false;
    }

    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);
            }
        }
    }

    /**
     * Calculates the distance from the camera to the horizon of the highest terrain point: a point farther than it
     * is under the horizon. Used in renderTree to skip rendering of segments that are under the horizon.
     * @protected
     */
    protected _updateHorizonDist(cam: PlanetCamera) {
        this._horizonDist2 = this._getHorizonDist2(cam);
    }

    protected _getHorizonDist2(cam: Camera): number {
        let planet = this.planet,
            r = planet.ellipsoid.polarSize,
            h = (planet.terrain ? planet.terrain.getMaxHeight() : 0) * planet._heightFactor,
            eyeHorizon2 = cam.eye.length2() - r * r,
            d = Math.sqrt(eyeHorizon2 > 0 ? eyeHorizon2 : 0) + Math.sqrt(2 * r * h + h * h);
        return d * d;
    }

    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);
        }
    }

    /**
     * The tree traversal of a view camera, it runs after the main camera one.
     * @protected
     */
    protected _collectViewRenderNodes(
        slot: number,
        camera: Camera,
        frustumMask: number,
        mainCamera: PlanetCamera,
        mainState: boolean
    ) {
        const horizonDist2 = camera.isOrthographic ? Infinity : this._getHorizonDist2(camera);

        for (let i = 0; i < this._quadTreeList.length; i++) {
            this._quadTreeList[i].renderViewCameraTree(
                slot,
                camera,
                frustumMask,
                mainCamera,
                horizonDist2,
                mainState,
                null
            );
        }
    }

    /**
     * Destroys the branches the main camera does not render.
     * @public
     * @param {boolean} [keepViewCameraBranches=false] - Keeps the branches leading to the nodes the depth cameras
     * render now. They are under the nodes the main camera has not gone into, and would be made and loaded again.
     */
    public clear(keepViewCameraBranches: boolean = false) {
        for (let i = 0; i < this._quadTreeList.length; i++) {
            this._quadTreeList[i].clearTree(keepViewCameraBranches);
        }
    }

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

    /**
     * Returns the most detailed node made under the point, not deeper than the zoom.
     * @public
     * @param {LonLat} lonLat - Point.
     * @param {number} [zoom] - Maximal node zoom.
     * @returns {Node | null} -
     */
    public getNodeByLonLat(lonLat: LonLat, zoom: number = Infinity): Node | null {
        let node: Node | undefined = this._quadTreeList.find((root) => root.segment.isLonLatInside(lonLat));

        if (!node) {
            return null;
        }

        let p = node.segment.projectNative(lonLat);

        while (node.nodes.length && node.segment.tileZoom < zoom) {
            let e: Extent = node.segment._extent,
                isEast = p.lon >= (e.southWest.lon + e.northEast.lon) * 0.5,
                isSouth = p.lat < (e.southWest.lat + e.northEast.lat) * 0.5;
            node = node.nodes[isSouth ? (isEast ? SE : SW) : isEast ? NE : NW];
        }

        return node;
    }

    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"];