"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Symbology
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Gradient = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const core_geometry_1 = require("@itwin/core-geometry");
const ColorDef_1 = require("./ColorDef");
const Image_1 = require("./Image");
const ThematicDisplay_1 = require("./ThematicDisplay");
/** Namespace containing types for defining a color gradient, often used for filled planar regions.
 * @see [[GeometryParams]]
 * @see [[GraphicParams]]
 * @public
 */
var Gradient;
(function (Gradient) {
    /** Flags applied to a [[Gradient.Symb]]. */
    let Flags;
    (function (Flags) {
        /** No flags. */
        Flags[Flags["None"] = 0] = "None";
        /** Reverse the order of the gradient keys. */
        Flags[Flags["Invert"] = 1] = "Invert";
        /** Draw an outline around the surface to which the gradient is applied. */
        Flags[Flags["Outline"] = 2] = "Outline";
    })(Flags = Gradient.Flags || (Gradient.Flags = {}));
    /** Enumerates the modes by which a [[Gradient.Symb]]'s keys are applied to create an image. */
    let Mode;
    (function (Mode) {
        Mode[Mode["None"] = 0] = "None";
        Mode[Mode["Linear"] = 1] = "Linear";
        Mode[Mode["Curved"] = 2] = "Curved";
        Mode[Mode["Cylindrical"] = 3] = "Cylindrical";
        Mode[Mode["Spherical"] = 4] = "Spherical";
        Mode[Mode["Hemispherical"] = 5] = "Hemispherical";
        /** For a gradient created based for [[ThematicDisplay]]. */
        Mode[Mode["Thematic"] = 6] = "Thematic";
    })(Mode = Gradient.Mode || (Gradient.Mode = {}));
    /** Gradient fraction value to [[ColorDef]] pair
     * @see [[Gradient.KeyColorProps]]
     */
    class KeyColor {
        constructor(json) {
            this.value = json.value;
            this.color = ColorDef_1.ColorDef.fromJSON(json.color);
        }
    }
    Gradient.KeyColor = KeyColor;
    /** Compare two KeyColor objects for equality. Returns true if equal. */
    function keyColorEquals(a, b) {
        return (a.value === b.value) && a.color.equals(b.color);
    }
    Gradient.keyColorEquals = keyColorEquals;
    /** Multi-color area fill defined by a range of colors that vary by position.
     * Gradient fill can be applied to planar regions.
     * @see [[Gradient.SymbProps]]
     */
    class Symb {
        constructor() {
            this.mode = Mode.None;
            this.flags = Flags.None;
            this.shift = 0;
            this.keys = [];
        }
        /** create a GradientSymb from a json object. */
        static fromJSON(json) {
            const result = new Symb();
            if (!json)
                return result;
            result.mode = json.mode;
            result.flags = (json.flags === undefined) ? Flags.None : json.flags;
            result.angle = json.angle ? core_geometry_1.Angle.fromJSON(json.angle) : undefined;
            result.tint = json.tint;
            result.shift = json.shift ? json.shift : 0;
            json.keys.forEach((key) => result.keys.push(new KeyColor(key)));
            result.thematicSettings = (json.thematicSettings === undefined) ? undefined : ThematicDisplay_1.ThematicGradientSettings.fromJSON(json.thematicSettings);
            return result;
        }
        /** Create for [[ThematicDisplay]]. */
        static createThematic(settings) {
            const result = new Symb();
            result.mode = Mode.Thematic;
            result.thematicSettings = settings;
            if (settings.colorScheme < ThematicDisplay_1.ThematicGradientColorScheme.Custom) {
                for (const keyValue of Gradient.Symb._fixedSchemeKeys[settings.colorScheme])
                    result.keys.push(new KeyColor({ value: keyValue[0], color: ColorDef_1.ColorDef.computeTbgrFromComponents(keyValue[1], keyValue[3], keyValue[2]) }));
            }
            else { // custom color scheme; must use custom keys
                (0, core_bentley_1.assert)(settings.customKeys.length > 1, "Custom thematic mode requires at least two keys to be defined");
                if (settings.customKeys.length > 1) {
                    settings.customKeys.forEach((keyColor) => result.keys.push(keyColor));
                }
                else { // if custom color keys are not specified properly, revert to some basic key scheme and assert
                    for (const keyValue of Gradient.Symb._fixedCustomKeys)
                        result.keys.push(new KeyColor({ value: keyValue[0], color: ColorDef_1.ColorDef.from(keyValue[1], keyValue[3], keyValue[2]).toJSON() }));
                }
            }
            return result;
        }
        toJSON() {
            return {
                ...this,
                thematicSettings: this.thematicSettings?.toJSON(),
                keys: this.keys.map((key) => ({ value: key.value, color: key.color.toJSON() })),
            };
        }
        clone() {
            return Symb.fromJSON(this.toJSON());
        }
        /** Returns true if this symbology is equal to another, false otherwise. */
        equals(other) {
            return Symb.compareSymb(this, other) === 0;
        }
        /** Compares two gradient symbologies. Used for ordering Gradient.Symb objects.
         * @param lhs First gradient to compare
         * @param rhs Second gradient to compare
         * @returns 0 if lhs is equivalent to rhs, a negative number if lhs compares less than rhs, or a positive number if lhs compares greater than rhs.
         */
        static compareSymb(lhs, rhs) {
            if (lhs === rhs)
                return 0; // Same pointer
            if (lhs.mode !== rhs.mode)
                return lhs.mode - rhs.mode;
            if (lhs.flags !== rhs.flags)
                if (lhs.flags === undefined)
                    return -1;
                else if (rhs.flags === undefined)
                    return 1;
                else
                    return lhs.flags - rhs.flags;
            if (lhs.tint !== rhs.tint)
                if (lhs.tint === undefined)
                    return -1;
                else if (rhs.tint === undefined)
                    return 1;
                else
                    return lhs.tint - rhs.tint;
            if (lhs.shift !== rhs.shift)
                if (lhs.shift === undefined)
                    return -1;
                else if (rhs.shift === undefined)
                    return 1;
                else
                    return lhs.shift - rhs.shift;
            if ((lhs.angle === undefined) !== (rhs.angle === undefined))
                if (lhs.angle === undefined)
                    return -1;
                else
                    return 1;
            if (lhs.angle && !lhs.angle.isAlmostEqualNoPeriodShift(rhs.angle))
                return lhs.angle.radians - rhs.angle.radians;
            if (lhs.keys.length !== rhs.keys.length)
                return lhs.keys.length - rhs.keys.length;
            for (let i = 0; i < lhs.keys.length; i++) {
                if (lhs.keys[i].value !== rhs.keys[i].value)
                    return lhs.keys[i].value - rhs.keys[i].value;
                if (!lhs.keys[i].color.equals(rhs.keys[i].color))
                    return lhs.keys[i].color.tbgr - rhs.keys[i].color.tbgr;
            }
            if (lhs.thematicSettings !== rhs.thematicSettings)
                if (undefined === lhs.thematicSettings)
                    return -1;
                else if (undefined === rhs.thematicSettings)
                    return 1;
                else {
                    const thematicCompareResult = ThematicDisplay_1.ThematicGradientSettings.compare(lhs.thematicSettings, rhs.thematicSettings);
                    if (0 !== thematicCompareResult)
                        return thematicCompareResult;
                }
            return 0;
        }
        /** Compare this symbology to another.
         * @see [[Gradient.Symb.compareSymb]]
         */
        compare(other) {
            return Gradient.Symb.compareSymb(this, other);
        }
        /**
         * Ensure the value given is within the range of 0 to 255,
         * and truncate the value to only the 8 least significant bits.
         */
        roundToByte(num) {
            return Math.min(num + .5, 255.0) & 0xFF;
        }
        /** Maps a value to an RGBA value adjusted from a color present in this symbology's array. */
        mapColor(value) {
            if (value < 0)
                value = 0;
            else if (value > 1)
                value = 1;
            if ((this.flags & Flags.Invert) !== 0)
                value = 1 - value;
            let idx = 0;
            let d;
            let w0;
            let w1;
            if (this.keys.length <= 2) {
                w0 = 1.0 - value;
                w1 = value;
            }
            else { // locate value in map, blend corresponding colors
                while (idx < (this.keys.length - 2) && value > this.keys[idx + 1].value)
                    idx++;
                d = this.keys[idx + 1].value - this.keys[idx].value;
                w1 = d < 0.0001 ? 0.0 : (value - this.keys[idx].value) / d;
                w0 = 1.0 - w1;
            }
            const color0 = this.keys[idx].color;
            const color1 = this.keys[idx + 1].color;
            const colors0 = color0.colors;
            const colors1 = color1.colors;
            const red = w0 * colors0.r + w1 * colors1.r;
            const green = w0 * colors0.g + w1 * colors1.g;
            const blue = w0 * colors0.b + w1 * colors1.b;
            const transparency = w0 * colors0.t + w1 * colors1.t;
            return ColorDef_1.ColorDef.from(this.roundToByte(red), this.roundToByte(green), this.roundToByte(blue), this.roundToByte(transparency));
        }
        get hasTranslucency() {
            for (const key of this.keys) {
                if (!key.color.isOpaque)
                    return true;
            }
            return false;
        }
        /** Returns true if the [[Gradient.Flags.Outline]] flag is set. */
        get isOutlined() { return 0 !== (this.flags & Flags.Outline); }
        /** This function (for internal use only) provides the WebGL renderer with a thematic image that its shaders
         * can use properly with various thematic rendering techniques.
         * If you want a regular gradient image, use the method [[Gradient.Symb.getImage]].
         * @internal
         */
        getThematicImageForRenderer(maxDimension) {
            (0, core_bentley_1.assert)(Mode.Thematic === this.mode, "getThematicImageForRenderer only is used for thematic display.");
            let settings = this.thematicSettings;
            if (settings === undefined) {
                settings = ThematicDisplay_1.ThematicGradientSettings.defaults;
            }
            const stepCount = Math.min(settings.stepCount, maxDimension);
            const dimension = (ThematicDisplay_1.ThematicGradientMode.Smooth === settings.mode) ? maxDimension : stepCount;
            const image = new Uint8Array(1 * dimension * 4);
            let currentIdx = image.length - 1;
            function addColor(color) {
                image[currentIdx--] = color.getAlpha();
                image[currentIdx--] = color.colors.b;
                image[currentIdx--] = color.colors.g;
                image[currentIdx--] = color.colors.r;
            }
            switch (settings.mode) {
                case ThematicDisplay_1.ThematicGradientMode.Smooth: {
                    for (let j = 0; j < dimension; j++) {
                        const f = (1 - j / (dimension));
                        addColor(this.mapColor(f));
                    }
                    break;
                }
                case ThematicDisplay_1.ThematicGradientMode.SteppedWithDelimiter:
                case ThematicDisplay_1.ThematicGradientMode.IsoLines:
                // The work to generate the delimiter lines and isolines is done completely in the shader.
                // Therefore, we just fall through here and use a regular stepped gradient.
                case ThematicDisplay_1.ThematicGradientMode.Stepped: {
                    (0, core_bentley_1.assert)(settings.stepCount > 1, "Step count must be at least two to generate renderer gradient for thematic display");
                    for (let j = 0; j < dimension; j++) {
                        // If we use Smooth's approach to generate the gradient...
                        // We would get these values for stepCount five: 0   .2   .4   .6   .8
                        //                  We really want these values: 0   .25  .5   .75   1
                        // This preserves an exact color mapping of a n-step gradient when stepCount also equals n.
                        // stepCount must be at least two for this.  The thematic API enforces stepCount of at least 2.
                        const f = (1 - j / (dimension - 1));
                        addColor(this.mapColor(f));
                    }
                    break;
                }
            }
            (0, core_bentley_1.assert)(-1 === currentIdx);
            const imageBuffer = Image_1.ImageBuffer.create(image, Image_1.ImageBufferFormat.Rgba, 1);
            (0, core_bentley_1.assert)(undefined !== imageBuffer);
            return imageBuffer;
        }
        /** Produces a bitmap image from this gradient.
         * @param width Width of the image
         * @param height Height of the image
         * @note If this gradient uses [[Gradient.Mode.Thematic]], then the width of the image will be 1 and the margin color will be included in the top and bottom rows.
         * @see [[produceImage]] for more customization.
         */
        getImage(width, height) {
            if (this.mode === Mode.Thematic)
                width = 1;
            return this.produceImage({ width, height, includeThematicMargin: true });
        }
        /** Produces a bitmap image from this gradient. */
        produceImage(args) {
            const { width, height, includeThematicMargin } = { ...args };
            const thisAngle = (this.angle === undefined) ? 0 : this.angle.radians;
            const cosA = Math.cos(thisAngle);
            const sinA = Math.sin(thisAngle);
            const image = new Uint8Array(width * height * 4);
            let currentIdx = image.length - 1;
            const shift = Math.min(1.0, Math.abs(this.shift));
            switch (this.mode) {
                case Mode.Linear:
                case Mode.Cylindrical: {
                    const xs = 0.5 - 0.25 * shift * cosA;
                    const ys = 0.5 - 0.25 * shift * sinA;
                    let dMax;
                    let dMin = dMax = 0.0;
                    let d;
                    for (let j = 0; j < 2; j++) {
                        for (let i = 0; i < 2; i++) {
                            d = (i - xs) * cosA + (j - ys) * sinA;
                            if (d < dMin)
                                dMin = d;
                            if (d > dMax)
                                dMax = d;
                        }
                    }
                    for (let j = 0; j < height; j++) {
                        const y = j / height - ys;
                        for (let i = 0; i < width; i++) {
                            const x = i / width - xs;
                            d = x * cosA + y * sinA;
                            let f;
                            if (this.mode === Mode.Linear) {
                                if (d > 0)
                                    f = 0.5 + 0.5 * d / dMax;
                                else
                                    f = 0.5 - 0.5 * d / dMin;
                            }
                            else {
                                if (d > 0)
                                    f = Math.sin(Math.PI / 2 * (1.0 - d / dMax));
                                else
                                    f = Math.sin(Math.PI / 2 * (1.0 - d / dMin));
                            }
                            const color = this.mapColor(f);
                            image[currentIdx--] = color.getAlpha();
                            image[currentIdx--] = color.colors.b;
                            image[currentIdx--] = color.colors.g;
                            image[currentIdx--] = color.colors.r;
                        }
                    }
                    break;
                }
                case Mode.Curved: {
                    const xs = 0.5 + 0.5 * sinA - 0.25 * shift * cosA;
                    const ys = 0.5 - 0.5 * cosA - 0.25 * shift * sinA;
                    for (let j = 0; j < height; j++) {
                        const y = j / height - ys;
                        for (let i = 0; i < width; i++) {
                            const x = i / width - xs;
                            const xr = 0.8 * (x * cosA + y * sinA);
                            const yr = y * cosA - x * sinA;
                            const f = Math.sin(Math.PI / 2 * (1 - Math.sqrt(xr * xr + yr * yr)));
                            const color = this.mapColor(f);
                            image[currentIdx--] = color.getAlpha();
                            image[currentIdx--] = color.colors.b;
                            image[currentIdx--] = color.colors.g;
                            image[currentIdx--] = color.colors.r;
                        }
                    }
                    break;
                }
                case Mode.Spherical: {
                    const r = 0.5 + 0.125 * Math.sin(2.0 * thisAngle);
                    const xs = 0.5 * shift * (cosA + sinA) * r;
                    const ys = 0.5 * shift * (sinA - cosA) * r;
                    for (let j = 0; j < height; j++) {
                        const y = ys + j / height - 0.5;
                        for (let i = 0; i < width; i++) {
                            const x = xs + i / width - 0.5;
                            const f = Math.sin(Math.PI / 2 * (1.0 - Math.sqrt(x * x + y * y) / r));
                            const color = this.mapColor(f);
                            image[currentIdx--] = color.getAlpha();
                            image[currentIdx--] = color.colors.b;
                            image[currentIdx--] = color.colors.g;
                            image[currentIdx--] = color.colors.r;
                        }
                    }
                    break;
                }
                case Mode.Hemispherical: {
                    const xs = 0.5 + 0.5 * sinA - 0.5 * shift * cosA;
                    const ys = 0.5 - 0.5 * cosA - 0.5 * shift * sinA;
                    for (let j = 0; j < height; j++) {
                        const y = j / height - ys;
                        for (let i = 0; i < width; i++) {
                            const x = i / width - xs;
                            const f = Math.sin(Math.PI / 2 * (1.0 - Math.sqrt(x * x + y * y)));
                            const color = this.mapColor(f);
                            image[currentIdx--] = color.getAlpha();
                            image[currentIdx--] = color.colors.b;
                            image[currentIdx--] = color.colors.g;
                            image[currentIdx--] = color.colors.r;
                        }
                    }
                    break;
                }
                case Mode.Thematic: {
                    const settings = this.thematicSettings ?? ThematicDisplay_1.ThematicGradientSettings.defaults;
                    for (let j = 0; j < height; j++) {
                        let f = 1 - j / height;
                        let color;
                        if (includeThematicMargin && (f < ThematicDisplay_1.ThematicGradientSettings.margin || f > ThematicDisplay_1.ThematicGradientSettings.contentMax)) {
                            color = settings.marginColor;
                        }
                        else {
                            f = (f - ThematicDisplay_1.ThematicGradientSettings.margin) / (ThematicDisplay_1.ThematicGradientSettings.contentRange);
                            switch (settings.mode) {
                                case ThematicDisplay_1.ThematicGradientMode.SteppedWithDelimiter:
                                case ThematicDisplay_1.ThematicGradientMode.IsoLines:
                                case ThematicDisplay_1.ThematicGradientMode.Stepped: {
                                    if (settings.stepCount > 1) {
                                        const fStep = Math.floor(f * settings.stepCount - 0.00001) / (settings.stepCount - 1);
                                        color = this.mapColor(fStep);
                                    }
                                    break;
                                }
                                case ThematicDisplay_1.ThematicGradientMode.Smooth:
                                    color = this.mapColor(f);
                                    break;
                            }
                        }
                        for (let i = 0; i < width; i++) {
                            image[currentIdx--] = color.getAlpha();
                            image[currentIdx--] = color.colors.b;
                            image[currentIdx--] = color.colors.g;
                            image[currentIdx--] = color.colors.r;
                        }
                    }
                }
            }
            (0, core_bentley_1.assert)(-1 === currentIdx);
            const imageBuffer = Image_1.ImageBuffer.create(image, Image_1.ImageBufferFormat.Rgba, width);
            (0, core_bentley_1.assert)(undefined !== imageBuffer);
            return imageBuffer;
        }
    }
    Symb._fixedSchemeKeys = [
        // NB: these color values are ordered as rbg.  Note how the components are applied below.
        [[0.0, 0, 255, 0], [0.25, 0, 255, 255], [0.5, 0, 0, 255], [0.75, 255, 0, 255], [1.0, 255, 0, 0]], // Blue Red.
        [[0.0, 255, 0, 0], [0.25, 255, 0, 255], [0.5, 0, 0, 255], [0.75, 0, 255, 255], [1.0, 0, 255, 0]], // Red blue.
        [[0.0, 0, 0, 0], [1.0, 255, 255, 255]], // Monochrome.
        [[0.0, 152, 148, 188], [0.5, 204, 160, 204], [1.0, 152, 72, 128]], // Based off of the topographic gradients in Point Clouds.
        [[0.0, 0, 255, 0], [0.2, 72, 96, 160], [0.4, 152, 96, 160], [0.6, 128, 32, 104], [0.7, 148, 180, 128], [1.0, 240, 240, 240]], // Based off of the sea-mountain gradient in Point Clouds.
    ];
    Symb._fixedCustomKeys = [[0.0, 255, 0, 0], [1.0, 0, 255, 0]];
    Gradient.Symb = Symb;
})(Gradient || (exports.Gradient = Gradient = {}));
//# sourceMappingURL=Gradient.js.map