terrain/rgbTerrainGrid.tstypescript
import type { TypedArray } from "../utils/shared";

export const RGB_ENCODING_CUSTOM = -1;
export const RGB_ENCODING_RGB = 0;
export const RGB_ENCODING_TERRARIUM = 1;

/** Writes a row of pixel heights, NaN for no data pixels. */
export type DecodeHeightsRowFunc = (row: number, outHeights: Float32Array) => void;

export interface IRgbDecodeParams {
    encoding: number;
    minHeight: number;
    resolution: number;
    heightFactor: number;
    noDataValues: number[] | TypedArray;
    parentData: number[] | TypedArray | null;
    parentOffsetX: number;
    parentOffsetY: number;
    parentZoomDiff: number;
    skipPositiveHeights: boolean;
}

export interface IRgbHeightsParams {
    decode: IRgbDecodeParams;
    size: number;
    gridSize: number;
    levels: number;
    noDataHeight: number;
}

const PARAMS_ID = 0,
    PARAMS_SIZE = 1,
    PARAMS_GRID_SIZE = 2,
    PARAMS_LEVELS = 3,
    PARAMS_ENCODING = 4,
    PARAMS_MIN_HEIGHT = 5,
    PARAMS_RESOLUTION = 6,
    PARAMS_HEIGHT_FACTOR = 7,
    PARAMS_PARENT_OFFSET_X = 8,
    PARAMS_PARENT_OFFSET_Y = 9,
    PARAMS_PARENT_ZOOM_DIFF = 10,
    PARAMS_SKIP_POSITIVE_HEIGHTS = 11,
    PARAMS_NO_DATA_HEIGHT = 12,
    PARAMS_NO_DATA_VALUES = 13;

/**
 * Packs heights parameters to the buffer for the worker message.
 * @param {number} id - Worker task id.
 * @param {IRgbHeightsParams} p - Heights parameters, parent data is sent separately.
 * @returns {Float64Array} -
 */
export function packRgbHeightsParams(id: number, p: IRgbHeightsParams): Float64Array {
    let d = p.decode,
        ndv = d.noDataValues;
    let params = new Float64Array(PARAMS_NO_DATA_VALUES + ndv.length);

    params[PARAMS_ID] = id;
    params[PARAMS_SIZE] = p.size;
    params[PARAMS_GRID_SIZE] = p.gridSize;
    params[PARAMS_LEVELS] = p.levels;
    params[PARAMS_ENCODING] = d.encoding;
    params[PARAMS_MIN_HEIGHT] = d.minHeight;
    params[PARAMS_RESOLUTION] = d.resolution;
    params[PARAMS_HEIGHT_FACTOR] = d.heightFactor;
    params[PARAMS_PARENT_OFFSET_X] = d.parentOffsetX;
    params[PARAMS_PARENT_OFFSET_Y] = d.parentOffsetY;
    params[PARAMS_PARENT_ZOOM_DIFF] = d.parentZoomDiff;
    params[PARAMS_SKIP_POSITIVE_HEIGHTS] = d.skipPositiveHeights ? 1 : 0;
    params[PARAMS_NO_DATA_HEIGHT] = p.noDataHeight;

    for (let i = 0; i < ndv.length; i++) {
        params[PARAMS_NO_DATA_VALUES + i] = ndv[i];
    }
    return params;
}

/**
 * Unpacks heights parameters from the worker message buffer.
 * @param {Float64Array} params - Packed parameters.
 * @param {Float32Array | null} parentData - Parent segment elevations.
 * @returns {Array} - Task id and parameters (exactly 2 entries).
 */
export function unpackRgbHeightsParams(
    params: Float64Array,
    parentData: Float32Array | null
): [number, IRgbHeightsParams] {
    return [
        params[PARAMS_ID],
        {
            decode: {
                encoding: params[PARAMS_ENCODING],
                minHeight: params[PARAMS_MIN_HEIGHT],
                resolution: params[PARAMS_RESOLUTION],
                heightFactor: params[PARAMS_HEIGHT_FACTOR],
                noDataValues: params.subarray(PARAMS_NO_DATA_VALUES),
                parentData: parentData,
                parentOffsetX: params[PARAMS_PARENT_OFFSET_X],
                parentOffsetY: params[PARAMS_PARENT_OFFSET_Y],
                parentZoomDiff: params[PARAMS_PARENT_ZOOM_DIFF],
                skipPositiveHeights: params[PARAMS_SKIP_POSITIVE_HEIGHTS] === 1
            },
            size: params[PARAMS_SIZE],
            gridSize: params[PARAMS_GRID_SIZE],
            levels: params[PARAMS_LEVELS],
            noDataHeight: params[PARAMS_NO_DATA_HEIGHT]
        }
    ];
}

/**
 * Returns vertex grid cells for the image size, 2^n and (2^n + 1) sizes only.
 * @param {number} size - Image size.
 * @returns {number} -
 */
export function getGridCells(size: number): number {
    return (size & (size - 1)) === 0 ? size : size - 1;
}

function isNoDataValue(noDataValues: number[] | TypedArray, ndMin: number, ndMax: number, h: number): boolean {
    if (h > 50000) {
        return true;
    }
    if (h <= ndMin || h >= ndMax) {
        return false;
    }
    let start = 0,
        end = noDataValues.length - 1;
    while (start <= end) {
        let k = (start + end) >> 1;
        if (Math.abs(noDataValues[k] - h) < 1e-3) {
            return true;
        } else if (noDataValues[k] < h) {
            start = k + 1;
        } else {
            end = k - 1;
        }
    }
    return false;
}

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

/**
 * Decodes "rgb" or "terrarium" encoded pixel row heights.
 * @param {number[] | TypedArray} rgbaData - Image RGBA data.
 * @param {number} row - Pixel row.
 * @param {number} size - Image size.
 * @param {number} parentScale - Parent grid cells by image grid cells.
 * @param {IRgbDecodeParams} p - Decode parameters.
 * @param {Float32Array} outHeights - Row heights.
 * @param {number} noDataHeight - Height for no data pixels.
 */
export function decodeRgbHeightsRow(
    rgbaData: number[] | TypedArray,
    row: number,
    size: number,
    parentScale: number,
    p: IRgbDecodeParams,
    outHeights: Float32Array,
    noDataHeight: number
) {
    let ndv = p.noDataValues,
        ndMin = ndv.length ? ndv[0] - 1e-3 : 0,
        ndMax = ndv.length ? ndv[ndv.length - 1] + 1e-3 : 0;

    let isTerrarium = p.encoding === RGB_ENCODING_TERRARIUM,
        minHeight = p.minHeight,
        resolution = p.resolution,
        heightFactor = p.heightFactor,
        parentData = p.parentData;

    let pi = Math.floor(row * parentScale);

    for (let j = 0, k4 = row * size * 4; j < size; j++, k4 += 4) {
        let r = rgbaData[k4],
            g = rgbaData[k4 + 1],
            b = rgbaData[k4 + 2];

        let height = isTerrarium
            ? r * 256 + g + b / 256 - 32768
            : r === 255
              ? minHeight
              : minHeight + resolution * (r * 65536 + g * 256 + b);

        height *= heightFactor;

        let isNoData = isNoDataValue(ndv, ndMin, ndMax, height);

        if ((isNoData || height === 0) && parentData) {
            height = getParentHeight(
                p.parentZoomDiff,
                p.parentOffsetX,
                p.parentOffsetY,
                parentData,
                pi,
                Math.floor(j * parentScale),
                p.skipPositiveHeights
            );
            isNoData = isNoDataValue(ndv, ndMin, ndMax, height);
        }

        outHeights[j] = isNoData ? noDataHeight : height;
    }
}

/**
 * Converts size x size pixel heights to (size + 1) x (size + 1) vertex grid in pixel corners.
 * @param {number} size - Image size.
 * @param {DecodeHeightsRowFunc} decodeRow - Decodes pixel row heights, NaN for no data.
 * @param {number} noDataHeight - Height for vertices without data around.
 * @returns {Float32Array} -
 */
export function createVertexGrid(size: number, decodeRow: DecodeHeightsRowFunc, noDataHeight: number): Float32Array {
    let gs = size + 1;

    // Per-axis taps for vertex v: two pixel indices with interpolation weights, and the nearest pixels.
    let i0 = new Int32Array(gs),
        i1 = new Int32Array(gs),
        w0 = new Float32Array(gs),
        w1 = new Float32Array(gs),
        n0 = new Int32Array(gs),
        n1 = new Int32Array(gs);

    for (let v = 0; v < gs; v++) {
        if (v === 0) {
            i0[v] = 0;
            i1[v] = 1;
            w0[v] = 1.5;
            w1[v] = -0.5;
            n0[v] = n1[v] = 0;
        } else if (v === size) {
            i0[v] = size - 1;
            i1[v] = size - 2;
            w0[v] = 1.5;
            w1[v] = -0.5;
            n0[v] = n1[v] = size - 1;
        } else {
            i0[v] = n0[v] = v - 1;
            i1[v] = n1[v] = v;
            w0[v] = w1[v] = 0.5;
        }
    }

    // Pixel row r is kept at (r & 1) * size
    let heights = new Float32Array(size * 2),
        rowHeights = [heights.subarray(0, size), heights.subarray(size)],
        lastRow = -1;

    let out = new Float32Array(gs * gs);

    for (let vi = 0; vi < gs; vi++) {
        let needRow = vi === 0 ? 1 : Math.min(vi, size - 1);
        while (lastRow < needRow) {
            lastRow++;
            decodeRow(lastRow, rowHeights[lastRow & 1]);
        }

        let r0 = (i0[vi] & 1) * size,
            r1 = (i1[vi] & 1) * size,
            wr0 = w0[vi],
            wr1 = w1[vi],
            nr0 = (n0[vi] & 1) * size,
            nr1 = (n1[vi] & 1) * size;

        for (let vj = 0; vj < gs; vj++) {
            let c0 = i0[vj],
                c1 = i1[vj];

            let h =
                wr0 * (w0[vj] * heights[r0 + c0] + w1[vj] * heights[r0 + c1]) +
                wr1 * (w0[vj] * heights[r1 + c0] + w1[vj] * heights[r1 + c1]);

            // NaN means at least one tap has no data
            if (h === h) {
                out[vi * gs + vj] = h;
            } else {
                let h00 = heights[nr0 + n0[vj]],
                    h01 = heights[nr0 + n1[vj]],
                    h10 = heights[nr1 + n0[vj]],
                    h11 = heights[nr1 + n1[vj]];

                let sum = 0,
                    count = 0;

                if (h00 === h00) {
                    sum += h00;
                    count++;
                }
                if (h01 === h01) {
                    sum += h01;
                    count++;
                }
                if (h10 === h10) {
                    sum += h10;
                    count++;
                }
                if (h11 === h11) {
                    sum += h11;
                    count++;
                }

                out[vi * gs + vj] = count ? sum / count : noDataHeight;
            }
        }
    }

    return out;
}

/**
 * Takes (gridSize + 1) x (gridSize + 1) vertices of the cells x cells square starting at
 * the row0, col0 vertex of the (size + 1) x (size + 1) grid. Cells must be gridSize multiplied by a power of two.
 */
export function sampleSubGrid(
    grid: Float32Array,
    size: number,
    row0: number,
    col0: number,
    cells: number,
    gridSize: number,
    out: Float32Array = new Float32Array((gridSize + 1) * (gridSize + 1)),
    outOffset: number = 0
): Float32Array {
    let gs = size + 1,
        outGs = gridSize + 1,
        step = cells / gridSize;

    for (let i = 0; i < outGs; i++) {
        let src = (row0 + i * step) * gs + col0,
            dst = outOffset + i * outGs;
        for (let j = 0; j < outGs; j++) {
            out[dst + j] = grid[src + j * step];
        }
    }

    return out;
}

/**
 * Creates the tile heights grid and its subdivision levels, each tile is (gridSize + 1) x (gridSize + 1).
 * Output order is the tile itself, then 2x2 tiles of the first level, 4x4 of the second and so on, row by row.
 * @param {number} size - Image size, 2^n or (2^n + 1).
 * @param {number} gridSize - Tile grid size.
 * @param {number} levels - Subdivision levels.
 * @param {DecodeHeightsRowFunc} decodeRow - Decodes pixel row heights, NaN for no data.
 * @param {number} noDataHeight - Height for vertices without data.
 * @returns {Float32Array} -
 */
export function createHeightsLevels(
    size: number,
    gridSize: number,
    levels: number,
    decodeRow: DecodeHeightsRowFunc,
    noDataHeight: number
): Float32Array {
    let cells = getGridCells(size);

    let grid: Float32Array;

    if (cells === size) {
        grid = createVertexGrid(size, decodeRow, noDataHeight);
    } else {
        grid = new Float32Array(size * size);
        for (let row = 0; row < size; row++) {
            let rowHeights = grid.subarray(row * size, (row + 1) * size);
            decodeRow(row, rowHeights);
            for (let j = 0; j < size; j++) {
                if (rowHeights[j] !== rowHeights[j]) {
                    rowHeights[j] = noDataHeight;
                }
            }
        }
    }

    let tileLength = (gridSize + 1) * (gridSize + 1),
        tilesCount = 0;

    for (let k = 0; k <= levels; k++) {
        tilesCount += 1 << (2 * k);
    }

    let out = new Float32Array(tilesCount * tileLength);

    for (let k = 0, c = cells, offset = 0; k <= levels; k++, c >>= 1) {
        let d = 1 << k;
        for (let i = 0; i < d; i++) {
            for (let j = 0; j < d; j++) {
                sampleSubGrid(grid, cells, i * c, j * c, c, gridSize, out, offset);
                offset += tileLength;
            }
        }
    }

    return out;
}