terrain/BilTerrain.tstypescript
import { getTileExtent } from "../mercator";
import type { IGlobusTerrainParams } from "./GlobusTerrain";
import { GlobusTerrain } from "./GlobusTerrain";
import { Layer } from "../layer/Layer";
import { WMS } from "../layer/WMS";
import { isPowerOfTwo, nextHighestPowerOfTwo } from "../math";
import { Segment } from "../segment/Segment";
import { Extent } from "../Extent";
import type { TypedArray } from "../utils/shared";

interface IBilTerrainParams extends IGlobusTerrainParams {
    layers?: string;
    imageSize?: number;
}

class BilTerrain extends GlobusTerrain {
    protected _format: string;

    protected _layers: string;

    protected _imageSize: number;

    constructor(options: IBilTerrainParams = {}) {
        super("BilTerrain", options);

        this.equalizeVertices = true;

        this.equalizeNormals = true;

        this.minZoom = options.minZoom || 2;

        this.maxZoom = options.maxZoom || 14;

        this.noDataValues = options.noDataValues || [-9999, 32767];

        this.url = options.url || "";

        this._format = "application/bil16";

        this._layers = options.layers || "";

        this._imageSize = options.imageSize || 256;

        this.plainGridSize =
            options.plainGridSize != undefined
                ? options.plainGridSize
                : isPowerOfTwo(this._imageSize)
                  ? this._imageSize / 2
                  : nextHighestPowerOfTwo(this._imageSize) / 2;

        this._dataType = "arrayBuffer";
    }

    public override isBlur(segment: Segment): boolean {
        return segment.tileZoom >= 18;
    }

    protected override _createUrl(segment: Segment): string {
        return WMS.createRequestUrl(
            this.url,
            this._layers,
            this._format,
            "1.1.1",
            "GetMap",
            segment._projection.code,
            WMS.get_bbox_v1_1_1(segment.getExtent()),
            this._imageSize,
            this._imageSize
        );
    }

    protected override _createHeights(
        data: number[],
        segment: Segment | null,
        tileGroup: number,
        tileX: number,
        tileY: number,
        tileZoom: number,
        extent: Extent,
        preventChildren: boolean
    ): TypedArray | number[] {
        let bil16 = new Int16Array(data);

        //
        //Non-power of two images
        //
        if (!isPowerOfTwo(this._imageSize)) {
            let outCurrenElevations = new Float32Array(bil16.length);
            //TODO: optimize
            extractElevationTilesNonPowerOfTwo(bil16, outCurrenElevations);
            return outCurrenElevations;
        }

        let elevationsSize = (this.plainGridSize + 1) * (this.plainGridSize + 1);

        let d = 4;

        let outChildrenElevations = new Array(d);

        for (let i = 0; i < d; i++) {
            outChildrenElevations[i] = [];
            for (let j = 0; j < d; j++) {
                outChildrenElevations[i][j] = new Float32Array(elevationsSize);
            }
        }

        let outCurrenElevations = new Float32Array(elevationsSize);

        extractElevationTiles(bil16, this.noDataValues, outCurrenElevations, outChildrenElevations);

        let tileIndex = Layer.getTileIndex(tileX, tileY, tileZoom, tileGroup);

        this.setElevationCache(tileIndex, {
            heights: outCurrenElevations,
            extent: extent
        });

        let dd = this._imageSize / this.plainGridSize;

        for (let i = 0; i < dd; i++) {
            for (let j = 0; j < dd; j++) {
                let x = tileX * 2 + j,
                    y = tileY * 2 + i,
                    z = tileZoom + 1;
                let tileIndex = Layer.getTileIndex(x, y, z, tileGroup);
                this.setElevationCache(tileIndex, {
                    heights: outChildrenElevations[i][j],
                    extent: getTileExtent(x, y, z)
                });
            }
        }

        return outCurrenElevations;
    }
}

function extractElevationTilesNonPowerOfTwo(data: number[] | TypedArray, outCurrenElevations: number[] | TypedArray) {
    for (let i = 0, len = outCurrenElevations.length; i < len; i++) {
        outCurrenElevations[i] = data[i];
    }
}

function extractElevationTiles(
    data: number[] | TypedArray,
    noDataValues: number[] | TypedArray,
    outCurrenElevations: number[] | TypedArray,
    outChildrenElevations: number[][][] | TypedArray[][]
) {
    let destSize = Math.sqrt(outCurrenElevations.length) - 1;
    let destSizeOne = destSize + 1;
    let sourceSize = Math.sqrt(data.length);
    let dt = sourceSize / destSize;

    let rightHeight = 0,
        bottomHeight = 0;

    for (let k = 0, currIndex = 0, sourceDataLength = data.length; k < sourceDataLength; k++) {
        let height = data[k];

        let isNoDataCurrent = BilTerrain.checkNoDataValue(noDataValues, height),
            isNoDataRight = false,
            isNoDataBottom = false;

        let i = Math.floor(k / sourceSize),
            j = k % sourceSize;

        let tileX = Math.floor(j / destSize),
            tileY = Math.floor(i / destSize);

        let destArr = outChildrenElevations[tileY][tileX];

        let ii = i % destSize,
            jj = j % destSize;

        let destIndex = (ii + tileY) * destSizeOne + jj + tileX;

        destArr[destIndex] = height;

        if ((i + tileY) % dt === 0 && (j + tileX) % dt === 0) {
            outCurrenElevations[currIndex++] = height;
        }

        if ((j + 1) % destSize === 0 && j !== sourceSize - 1) {
            //current tile
            rightHeight = data[k];

            isNoDataRight = BilTerrain.checkNoDataValue(noDataValues, rightHeight);

            let middleHeight = height;
            if (!(isNoDataCurrent || isNoDataRight)) {
                middleHeight = (height + rightHeight) * 0.5;
            }

            destIndex = (ii + tileY) * destSizeOne + jj + 1;
            destArr[destIndex] = middleHeight;

            if ((i + tileY) % dt === 0) {
                outCurrenElevations[currIndex++] = middleHeight;
            }

            //next right tile
            let rightindex = (ii + tileY) * destSizeOne + ((jj + 1) % destSize);
            outChildrenElevations[tileY][tileX + 1][rightindex] = middleHeight;
        }

        if ((i + 1) % destSize === 0 && i !== sourceSize - 1) {
            //current tile
            bottomHeight = data[k + sourceSize];

            isNoDataBottom = BilTerrain.checkNoDataValue(noDataValues, bottomHeight);

            let middleHeight = height;

            if (!(isNoDataCurrent || isNoDataBottom)) {
                middleHeight = (height + bottomHeight) * 0.5;
            }

            destIndex = (ii + 1) * destSizeOne + jj + tileX;
            destArr[destIndex] = middleHeight;

            if ((j + tileX) % dt === 0) {
                outCurrenElevations[currIndex++] = middleHeight;
            }

            //next bottom tile
            let bottomindex = ((ii + 1) % destSize) * destSizeOne + jj + tileX;
            outChildrenElevations[tileY + 1][tileX][bottomindex] = middleHeight;
        }

        if ((j + 1) % destSize === 0 && j !== sourceSize - 1 && (i + 1) % destSize === 0 && i !== sourceSize - 1) {
            //current tile
            let rightBottomHeight = data[k + sourceSize + 1];

            let isNoDataRightBottom = BilTerrain.checkNoDataValue(noDataValues, rightBottomHeight);

            let middleHeight = height;

            if (!(isNoDataCurrent || isNoDataRight || isNoDataBottom || isNoDataRightBottom)) {
                middleHeight = (height + rightHeight + bottomHeight + rightBottomHeight) * 0.25;
            }

            destIndex = (ii + 1) * destSizeOne + (jj + 1);
            destArr[destIndex] = middleHeight;

            outCurrenElevations[currIndex++] = middleHeight;

            //next right tile
            let rightindex = (ii + 1) * destSizeOne;
            outChildrenElevations[tileY][tileX + 1][rightindex] = middleHeight;

            //next bottom tile
            outChildrenElevations[tileY + 1][tileX][destSize] = middleHeight;

            //next right bottom tile
            let rightBottomindex = 0;
            outChildrenElevations[tileY + 1][tileX + 1][rightBottomindex] = middleHeight;
        }
    }
}

export { BilTerrain };