terrain_RgbTerrain.ts

import {Extent} from "../Extent";
import {getTileExtent} from "../mercator";
import {GlobusTerrain, IGlobusTerrainParams} from "./GlobusTerrain";
import {isPowerOfTwo} from "../math";
import {Layer} from "../layer/Layer";
import {LonLat} from "../LonLat";
import {Segment} from "../segment/Segment";
import {binarySearchFast, TypedArray} from "../utils/shared";
import {IResponse} from "../utils/Loader";

export interface IRgbTerrainParams extends IGlobusTerrainParams {
    equalizeNormals?: boolean;
    key?: string;
    imageSize?: number;
    minHeight?: number;
    resolution?: number;
}

/**
 * @class
 * @extends {GlobusTerrain}
 * @param {string} [name=""] - Terrain provider name.
 * @param {IRgbTerrainParams} [options]:
 * @param {boolean} [equalizeNormals=true] - Make normal equalization on the edges of the tiles.
 * @param {string} [key=""] - API key.
 * @param {number} [imageSize=256] - Image size.
 * @param {number} [minHeight=-10000] - Minimal height for rgb to height converter.
 * @param {number} [resolution=0.1] - Height converter resolution.
 */
class RgbTerrain extends GlobusTerrain {

    protected _imageSize: number;

    protected _ctx: CanvasRenderingContext2D;

    protected _imageDataCache: Record<string, Uint8ClampedArray>;

    protected _minHeight: number;

    protected _resolution: number;

    constructor(name: string | null, options: IRgbTerrainParams = {}) {
        super(name || "RgbTerrain", {
            equalizeVertices: options.equalizeVertices != undefined ? options.equalizeVertices : true,
            maxZoom: options.maxZoom || 17,
            noDataValues: options.noDataValues || [options.minHeight != undefined ? options.minHeight : -10000],
            plainGridSize: options.plainGridSize || 128,
            url: options.url != undefined
                ? options.url
                : `//api.mapbox.com/v4/mapbox.terrain-rgb/{z}/{x}/{y}.pngraw?access_token=${options.key || "<key>"}`,
            gridSizeByZoom: options.gridSizeByZoom || [
                64, 32, 16, 8, 8, 8, 16, 16, 16, 32, 32, 32, 32, 32, 32, 64, 64, 64, 32, 32, 16, 8
            ],
            ...options
        });

        this.equalizeNormals = options.equalizeNormals || false;

        this._dataType = "imageBitmap";

        this._imageSize = options.imageSize || 256;

        this._ctx = this._createTemporalCanvas(this._imageSize);

        this._imageDataCache = {};

        this._minHeight = options.minHeight || -10000.0;
        this._resolution = options.resolution || 0.1;
    }

    static override checkNoDataValue(noDataValues: number[] | TypedArray, value: number): boolean {
        if (value > 50000) {
            return true;
        }
        return binarySearchFast(noDataValues, value) !== -1;
    }

    public rgb2Height(r: number, g: number, b: number): number {
        // Filter for "yellowish" pixels
        if (r === 255) {
            return this._minHeight;
        }
        return this._minHeight + this._resolution * (r * 256 * 256 + g * 256 + b);
    }

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

    protected _createTemporalCanvas(size: number): CanvasRenderingContext2D {
        let canvas = document.createElement("canvas");
        canvas.width = size;
        canvas.height = size;
        return canvas.getContext("2d", {
            willReadFrequently: true
        })!;
    }

    protected override _createHeights(data: HTMLImageElement | ImageBitmap, segment: Segment | null, tileGroup: number, tileX: number, tileY: number, tileZoom: number, extent: Extent, preventChildren: boolean): TypedArray | number[] {

        this._ctx.clearRect(0, 0, this._imageSize, this._imageSize);
        this._ctx.drawImage(data, 0, 0);
        let rgbaData = this._ctx.getImageData(0, 0, this._imageSize, this._imageSize).data;

        const SIZE = data.width;

        let availableParentTileX = 0,
            availableParentTileY = 0,
            availableParentTileZoom = 0,
            availableParentData: TypedArray | number[] | null = null,
            skipPositiveHeights = false;

        //
        // Getting parent segment terrain data, for zero and nodata values for the current segment
        //
        if (segment) {
            if (segment.tileZoom > this.maxNativeZoom) {
                let pn = segment.node;
                while (pn && !pn.segment.terrainExists) {
                    pn = pn.parentNode!;
                }
                if (pn) {
                    availableParentTileX = pn.segment.tileX;
                    availableParentTileY = pn.segment.tileY;
                    availableParentTileZoom = pn.segment.tileZoom;
                    availableParentData = pn.segment.elevationData;
                    // in this case maxNativeZoom means sea level
                    skipPositiveHeights = availableParentTileZoom <= 8;
                }
            }
        }

        //
        //Non-power of two images
        //
        if (!isPowerOfTwo(this._imageSize) && SIZE === this._imageSize) {
            let outCurrenElevations = new Float32Array(SIZE * SIZE);
            this.extractElevationTilesRgbNonPowerOfTwo(rgbaData, outCurrenElevations, this._heightFactor);
            return outCurrenElevations;
        }

        //
        // When image size equals grid size
        //
        if (this._imageSize === this.plainGridSize) {

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

            let [
                availableParentOffsetX,
                availableParentOffsetY,
                availableZoomDiff
            ] =
                segment ? getTileOffset(
                    segment.tileX, segment.tileY, segment.tileZoom,
                    availableParentTileX, availableParentTileY, availableParentTileZoom
                ) : [0, 0, 0];

            this.extractElevationSimple(
                rgbaData,
                this.noDataValues,
                availableParentData,
                availableParentOffsetX,
                availableParentOffsetY,
                availableZoomDiff,
                skipPositiveHeights,
                outCurrenElevations,
                this._heightFactor,
                this._imageSize
            );

            return outCurrenElevations;
        }

        //
        // Power of two images
        //
        let elevationsSize = (this.plainGridSize + 1) * (this.plainGridSize + 1);

        let d = SIZE / this.plainGridSize;

        let outCurrenElevations = new Float32Array(elevationsSize);
        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);
            }
        }

        if (!preventChildren) {
            this.extractElevationTilesRgb(
                rgbaData,
                this._heightFactor,
                this.noDataValues,
                availableParentData,
                availableParentTileX,
                availableParentTileY,
                availableParentTileZoom,
                segment ? segment.tileX : 0,
                segment ? segment.tileY : 0,
                segment ? segment.tileZoom : 0,
                skipPositiveHeights,
                outCurrenElevations,
                outChildrenElevations
            );

            // Save children data to cache
            for (let i = 0; i < d; i++) {
                for (let j = 0; j < d; j++) {
                    let [x, y, z] = getChildTileIndex(tileX, tileY, tileZoom, j, i);
                    let tileIndex = Layer.getTileIndex(x, y, z, tileGroup);
                    this.setElevationCache(tileIndex, {
                        heights: outChildrenElevations[i][j],
                        //
                        // @todo: must work for any grids
                        //
                        extent: getTileExtent(x, y, z)
                    });
                }
            }
        } else {
            this.extractElevationTilesRgbNoChildren(
                rgbaData,
                this._heightFactor,
                this.noDataValues,
                availableParentData,
                availableParentTileX,
                availableParentTileY,
                availableParentTileZoom,
                segment ? segment.tileX : 0,
                segment ? segment.tileY : 0,
                segment ? segment.tileZoom : 0,
                skipPositiveHeights,
                outCurrenElevations
            );
        }

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

        // Save current data to cache
        this.setElevationCache(tileIndex, {
            heights: outCurrenElevations,
            extent: extent
        });

        return outCurrenElevations;
    }

    public override getHeightAsync(lonLat: LonLat, callback: (h: number) => void, zoom?: number): boolean {

        zoom = zoom != undefined ? zoom : this.maxZoom;

        if (zoom === 0) {
            callback(0);
            return true;
        }

        const qts = this._planet!.quadTreeStrategy;
        const [x, y, z, tileGroup] = qts.getTileXY(lonLat, zoom);
        const [i, j] = qts.getLonLatTileOffset(lonLat, x, y, z, this._imageSize);
        const index = (i * this._imageSize + j) * 4;
        const tileIndex = Layer.getTileIndex(x, y, z, tileGroup);

        if (this._imageDataCache[tileIndex]) {
            let data = this._imageDataCache[tileIndex];
            let height = this._heightFactor * this.rgb2Height(data[index], data[index + 1], data[index + 2]);
            let isNoData = RgbTerrain.checkNoDataValue(this.noDataValues, height);
            if (isNoData) {
                return this.getHeightAsync(lonLat, callback, zoom - 1);
            } else {
                callback(this._heightFactor * this.rgb2Height(data[index], data[index + 1], data[index + 2]));
                return true;
            }
        }

        let def = this._fetchCache[tileIndex];
        if (!def) {
            def = this._loader.fetch({
                src: this._urlRewriteCallback && this._urlRewriteCallback(x, y, z, tileGroup) || this.buildURL(x, y, z, tileGroup),
                type: this._dataType
            });
            this._fetchCache[tileIndex] = def;
        }

        def!.then((response: IResponse) => {
            if (response.status === "ready") {
                this._ctx.clearRect(0, 0, this._imageSize, this._imageSize);
                this._ctx.drawImage(response.data, 0, 0);
                let data = this._ctx.getImageData(0, 0, 256, 256).data;
                this._imageDataCache[tileIndex] = data;

                let height = this._heightFactor * this.rgb2Height(data[index], data[index + 1], data[index + 2]);
                let isNoData = RgbTerrain.checkNoDataValue(this.noDataValues, height);
                if (isNoData) {
                    return this.getHeightAsync(lonLat, callback, zoom! - 1);
                } else {
                    callback(this._heightFactor * this.rgb2Height(data[index], data[index + 1], data[index + 2]));
                }
            } else if (response.status === "error") {
                return this.getHeightAsync(lonLat, callback, zoom! - 1);
            } else {
                //@ts-ignore
                this._fetchCache[tileIndex] = null;
                delete this._fetchCache[tileIndex];
            }
        });

        return false;
    }

    public extractElevationSimple(
        rgbaData: number[] | TypedArray,
        noDataValues: number[] | TypedArray,
        availableParentData: TypedArray | number[] | null = null,
        availableParentOffsetX: number,
        availableParentOffsetY: number,
        availableZoomDiff: number,
        skipPositiveHeights: boolean,
        outCurrenElevations: number[] | TypedArray,
        heightFactor: number = 1,
        imageSize: number
    ) {

        for (let k = 0, len = imageSize * imageSize; k < len; k++) {
            let j = k % imageSize,
                i = Math.floor(k / imageSize);
            let fromInd4 = k * 4;
            let height = heightFactor * this.rgb2Height(rgbaData[fromInd4], rgbaData[fromInd4 + 1], rgbaData[fromInd4 + 2]);
            let isNoData = RgbTerrain.checkNoDataValue(noDataValues, height);
            if ((isNoData || height === 0) && availableParentData) {
                height = getParentHeight(availableZoomDiff, availableParentOffsetX, availableParentOffsetY, availableParentData, i, j, skipPositiveHeights);
            }
            outCurrenElevations[i * (imageSize + 1) + j] = height;
        }

        for (let i = 0, len = imageSize; i < len; i++) {
            let j = imageSize - 1;
            let fromInd4 = (i * imageSize + j) * 4;
            let height = heightFactor * this.rgb2Height(rgbaData[fromInd4], rgbaData[fromInd4 + 1], rgbaData[fromInd4 + 2]);
            let isNoData = RgbTerrain.checkNoDataValue(noDataValues, height);
            if ((isNoData || height === 0) && availableParentData) {
                height = getParentHeight(availableZoomDiff, availableParentOffsetX, availableParentOffsetY, availableParentData, i, j, skipPositiveHeights);
            }
            outCurrenElevations[i * (imageSize + 1) + imageSize] = height;
        }

        for (let j = 0, len = imageSize; j < len; j++) {
            let i = imageSize - 1;
            let fromInd4 = (i * imageSize + j) * 4;
            let height = heightFactor * this.rgb2Height(rgbaData[fromInd4], rgbaData[fromInd4 + 1], rgbaData[fromInd4 + 2]);
            let isNoData = RgbTerrain.checkNoDataValue(noDataValues, height);
            if ((isNoData || height === 0) && availableParentData) {
                height = getParentHeight(availableZoomDiff, availableParentOffsetX, availableParentOffsetY, availableParentData, i, j, skipPositiveHeights);
            }
            outCurrenElevations[imageSize * (imageSize + 1) + j] = height;
        }

        let height = heightFactor * this.rgb2Height(rgbaData[rgbaData.length - 4], rgbaData[rgbaData.length - 3], rgbaData[rgbaData.length - 2]);
        let isNoData = RgbTerrain.checkNoDataValue(noDataValues, height);
        if ((isNoData || height === 0) && availableParentData) {
            height = getParentHeight(availableZoomDiff, availableParentOffsetX, availableParentOffsetY, availableParentData, imageSize - 1, imageSize - 1, skipPositiveHeights);
        }
        outCurrenElevations[outCurrenElevations.length - 1] = height;
    }

    public extractElevationTilesRgbNonPowerOfTwo(rgbaData: number[] | TypedArray, outCurrenElevations: number[] | TypedArray, heightFactor: number = 1) {
        for (let i = 0, len = outCurrenElevations.length; i < len; i++) {
            let i4 = i * 4;
            outCurrenElevations[i] = heightFactor * this.rgb2Height(rgbaData[i4], rgbaData[i4 + 1], rgbaData[i4 + 2]);
        }
    }

    public extractElevationTilesRgb(
        rgbaData: number[] | TypedArray,
        heightFactor: number,
        noDataValues: number[] | TypedArray,
        availableParentData: TypedArray | number[] | null = null,
        availableParentTileX: number,
        availableParentTileY: number,
        availableParentTileZoom: number,
        currentTileX: number,
        currentTileY: number,
        currentTileZoom: number,
        skipPositiveHeights: boolean,
        outCurrenElevations: number[] | TypedArray,
        outChildrenElevations: number[][][] | TypedArray[][]
    ) {
        let destSize = Math.sqrt(outCurrenElevations.length) - 1;
        let destSizeOne = destSize + 1;
        let sourceSize = Math.sqrt(rgbaData.length / 4);
        let dt = sourceSize / destSize;

        let rightHeight = 0,
            bottomHeight = 0,
            sourceSize4 = 0;

        let [availableParentOffsetX, availableParentOffsetY, availableZoomDiff] = getTileOffset(
            currentTileX, currentTileY, currentTileZoom,
            availableParentTileX, availableParentTileY, availableParentTileZoom
        );

        for (
            let k = 0, currIndex = 0, sourceDataLength = rgbaData.length / 4;
            k < sourceDataLength;
            k++
        ) {
            let k4 = k * 4;

            let height = heightFactor * this.rgb2Height(rgbaData[k4], rgbaData[k4 + 1], rgbaData[k4 + 2]);

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

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

            //
            // Try to get current height from the parent data
            if ((isNoDataCurrent || height === 0) && availableParentData) {
                height = getParentHeight(availableZoomDiff, availableParentOffsetX, availableParentOffsetY, availableParentData, Math.floor(i / dt), Math.floor(j / dt), skipPositiveHeights);
            }

            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 = heightFactor * this.rgb2Height(rgbaData[k4 + 4], rgbaData[k4 + 5], rgbaData[k4 + 6]);
                isNoDataRight = RgbTerrain.checkNoDataValue(noDataValues, rightHeight);

                //
                // Try to get right height from the parent data
                if ((isNoDataRight || rightHeight === 0) && availableParentData) {
                    rightHeight = getParentHeight(availableZoomDiff, availableParentOffsetX, availableParentOffsetY, availableParentData, Math.floor(i / dt), Math.floor((j + 1) / dt), skipPositiveHeights);
                }

                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
                sourceSize4 = sourceSize * 4;

                bottomHeight = heightFactor * this.rgb2Height(rgbaData[k4 + sourceSize4], rgbaData[k4 + sourceSize4 + 1], rgbaData[k4 + sourceSize4 + 2]);
                isNoDataBottom = RgbTerrain.checkNoDataValue(noDataValues, bottomHeight);

                //
                // Try to get bottom height from the parent data
                if ((isNoDataBottom || bottomHeight === 0) && availableParentData) {
                    bottomHeight = getParentHeight(availableZoomDiff, availableParentOffsetX, availableParentOffsetY, availableParentData, Math.floor((i + 1) / dt), Math.floor(j / dt), skipPositiveHeights);
                }

                let middleHeight = (height + bottomHeight) * 0.5;

                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 = heightFactor * this.rgb2Height(rgbaData[k4 + sourceSize4 + 4], rgbaData[k4 + sourceSize4 + 5], rgbaData[k4 + sourceSize4 + 6]);
                let isNoDataRightBottom = RgbTerrain.checkNoDataValue(noDataValues, rightBottomHeight);

                //
                // Try to get right bottom height from the parent data
                if ((isNoDataRightBottom || rightBottomHeight === 0) && availableParentData) {
                    rightBottomHeight = getParentHeight(availableZoomDiff, availableParentOffsetX, availableParentOffsetY, availableParentData, Math.floor((i + 1) / dt), Math.floor((j + 1) / dt), skipPositiveHeights);
                }

                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
                let bottomindex = destSize;
                outChildrenElevations[tileY + 1][tileX][bottomindex] = middleHeight;

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

    public extractElevationTilesRgbNoChildren(
        rgbaData: number[] | TypedArray,
        heightFactor: number,
        noDataValues: number[] | TypedArray,
        availableParentData: TypedArray | number[] | null = null,
        availableParentTileX: number,
        availableParentTileY: number,
        availableParentTileZoom: number,
        currentTileX: number,
        currentTileY: number,
        currentTileZoom: number,
        skipPositiveHeights: boolean,
        outCurrenElevations: number[] | TypedArray
    ) {
        let destSize = Math.sqrt(outCurrenElevations.length) - 1;
        let destSizeOne = destSize + 1;
        let sourceSize = Math.sqrt(rgbaData.length / 4);
        let dt = sourceSize / destSize;

        let rightHeight = 0,
            bottomHeight = 0,
            sourceSize4 = 0;

        let [availableParentOffsetX, availableParentOffsetY, availableZoomDiff] = getTileOffset(
            currentTileX, currentTileY, currentTileZoom,
            availableParentTileX, availableParentTileY, availableParentTileZoom
        );

        for (
            let k = 0, currIndex = 0, sourceDataLength = rgbaData.length / 4;
            k < sourceDataLength;
            k++
        ) {
            let k4 = k * 4;

            let height = heightFactor * this.rgb2Height(rgbaData[k4], rgbaData[k4 + 1], rgbaData[k4 + 2]);

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

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

            if ((isNoDataCurrent || height === 0) && availableParentData) {
                height = getParentHeight(availableZoomDiff, availableParentOffsetX, availableParentOffsetY, availableParentData, Math.floor(currIndex / destSizeOne), currIndex % destSizeOne, skipPositiveHeights);
            }

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

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

            if ((j + 1) % destSize === 0 && j !== sourceSize - 1) {
                //current tile
                rightHeight = heightFactor * this.rgb2Height(rgbaData[k4 + 4], rgbaData[k4 + 5], rgbaData[k4 + 6]);
                isNoDataRight = RgbTerrain.checkNoDataValue(noDataValues, rightHeight);

                if ((isNoDataRight || rightHeight === 0) && availableParentData) {
                    rightHeight = getParentHeight(availableZoomDiff, availableParentOffsetX, availableParentOffsetY, availableParentData, Math.floor(currIndex / destSizeOne), currIndex % destSizeOne, skipPositiveHeights);
                }

                let middleHeight = height;

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

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

            if ((i + 1) % destSize === 0 && i !== sourceSize - 1) {
                //current tile
                sourceSize4 = sourceSize * 4;

                bottomHeight = heightFactor * this.rgb2Height(rgbaData[k4 + sourceSize4], rgbaData[k4 + sourceSize4 + 1], rgbaData[k4 + sourceSize4 + 2]);
                isNoDataBottom = RgbTerrain.checkNoDataValue(noDataValues, bottomHeight);

                if ((isNoDataBottom || bottomHeight === 0) && availableParentData) {
                    bottomHeight = getParentHeight(availableZoomDiff, availableParentOffsetX, availableParentOffsetY, availableParentData, Math.floor(currIndex / destSizeOne), currIndex % destSizeOne, skipPositiveHeights);
                }

                let middleHeight = (height + bottomHeight) * 0.5;

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

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

            if (
                (j + 1) % destSize === 0 && j !== sourceSize - 1 &&
                (i + 1) % destSize === 0 && i !== sourceSize - 1
            ) {
                //current tile
                let rightBottomHeight = heightFactor * this.rgb2Height(rgbaData[k4 + sourceSize4 + 4], rgbaData[k4 + sourceSize4 + 5], rgbaData[k4 + sourceSize4 + 6]);
                let isNoDataRightBottom = RgbTerrain.checkNoDataValue(noDataValues, rightBottomHeight);

                if ((isNoDataRightBottom || rightBottomHeight === 0) && availableParentData) {
                    rightBottomHeight = getParentHeight(availableZoomDiff, availableParentOffsetX, availableParentOffsetY, availableParentData, Math.floor(currIndex / destSizeOne), currIndex % destSizeOne, skipPositiveHeights);
                }

                let middleHeight = height;

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

                outCurrenElevations[currIndex++] = middleHeight;
            }
        }
    }
}

function getTileOffset(
    currentTileX: number,
    currentTileY: number,
    currentTileZoom: number,
    parentTileX: number,
    parentTileY: number,
    parentTileZoom: number
): [number, number, number] {
    let dz2 = 2 << (currentTileZoom - parentTileZoom - 1);
    return [currentTileX - dz2 * parentTileX, currentTileY - dz2 * parentTileY, 1.0 / dz2];
}

function getChildTileIndex(
    currentParentTileX: number,
    currentParentTileY: number,
    currentParentTileZoom: number,
    childOffsetX: number,
    childOffsetY: number
): [number, number, number] {
    return [currentParentTileX * 2 + childOffsetX, currentParentTileY * 2 + childOffsetY, currentParentTileZoom + 1];
}

function getParentHeight(oneByDz2: number, offsetX: number, offsetY: number, heights: TypedArray | number[], i: number, j: number, skipPositiveHeights?: boolean) {
    let parentGridSize = Math.sqrt(heights.length);
    let pi = Math.floor(offsetY * oneByDz2 * parentGridSize + i * oneByDz2),
        pj = Math.floor(offsetX * oneByDz2 * parentGridSize + j * oneByDz2);
    let h = heights[pi * parentGridSize + pj];
    return skipPositiveHeights ? (h > 0 ? 0 : h) : h;
}

export {RgbTerrain};