Source: ui/color/Utils.js

import iro from '@jaames/iro';

/**
 * A collection of color scale helper tools.
 */

/**
 * Checks if a given string is a six digit hexadecimal color code
 * 
 * @author rhess <robin.hess@awi.de>
 * @memberof vef.ui.color
 * 
 * @param {string} color hex color code
 * @returns {boolean} true if color is valid
 */
export function isHexColor(color) {
    return /^#(?:[0-9a-fA-F]{6})$/.test(color);
}

/**
 * Copy a color item to remove original pointers to instances
 * 
 * @author rhess <robin.hess@awi.de>
 * @memberof vef.ui.color
 * 
 * @param {object} item color item
 * @returns {object} copied color item
 */
export function copyColorItem(item) {
    return {
        opacity: item.opacity,
        value: item.value,
        color: item.color
    };
}

/**
 * Copy a color scale to remove original pointers to instances
 * 
 * @author rhess <robin.hess@awi.de>
 * @memberof vef.ui.color
 * 
 * @param {object[]} scale color scale
 * @returns {object[]} copied color scale
 */
export function copyColorScale(scale) {
    const newScale = [];
    for (let i = 0; i < scale.length; ++i) {
        newScale.push(copyColorItem(scale[i]));
    }
    return newScale;
}

/**
 * Validates a color item and throws error if it
 * has invalid properties.
 * 
 * exampleItem = {
 *   "color": "#000000",
 *   "value": 1
 *   "opacity": 1
 * }
 * 
 * @author rhess <robin.hess@awi.de>
 * @memberof vef.ui.color
 * 
 * @param {object} item color item
 * @returns {object} valid color item
 */
export function validateColorItem(item) {
    if (typeof item != "object") throw new Error("invalid color object");
    if (!Number.isFinite(item.opacity)) throw new Error("invalid opacity");
    if (!Number.isFinite(item.value)) throw new Error("invalid value");
    if (!isHexColor(item.color)) throw new Error("invalid color string");

    return item;
}

/**
 * Validates a color scale and throws error if it
 * has invalid items.
 * 
 * @author rhess <robin.hess@awi.de>
 * @memberof vef.ui.color
 * 
 * @param {object[]} colorScale color scale
 * @returns {object[]} valid color scale
 */
export function validateColorScale(colorScale) {
    for (let i = 0; i < colorScale.length; ++i) {
        validateColorItem(colorScale[i]);
    }

    return colorScale;
}

/**
 * Set the color scale based on the existing WMS query string
 * parameters of the O2A template styles.
 * 
 * @author rhess <robin.hess@awi.de>
 * @memberof vef.ui.color
 * 
 * @returns {object} color scale
 */
export function wmsParamsToColorScale(env) {
    const items = [];
    let nv = null;

    env = env.trim();
    if (env.length > 0) {
        const groups = env.split(";");
        for (let i = 0; i < groups.length; ++i) {
            groups[i] = groups[i].split(":");

            let name = groups[i][0];
            let value = groups[i][1];

            if (name.startsWith("col")) {
                name = "color";
            } else if (name.startsWith("val")) {
                name = "value";
                value = Number.parseFloat(value);
            } else if (name.startsWith("opa")) {
                name = "opacity";
                value = Number.parseFloat(value);
            } else {
                continue;
            }

            let index = Number.parseInt(groups[i][0].substring(3));
            if (Number.isNaN(index)) {
                if (groups[i][0].substring(3) == "nv") {
                    nv = nv || {};
                    nv[name] = value;
                } else {
                    throw "invalid variable name: " + name;
                }
            } else {
                if (!items[index - 1]) items[index - 1] = {};
                items[index - 1][name] = value;
            }
        }
    }

    return {
        scale: items,
        noValue: nv
    }
}

/**
 * Get the WMS query parameters for the the O2A color template styles
 * from the given color scale
 * 
 * @author rhess <robin.hess@awi.de>
 * @memberof vef.ui.color
 * 
 * @param {object[]} scale color scale array
 * @param {object} noValue
 * 
 * @returns {object} {env, styles}
 * 
 */
export function colorScaleToWmsParams(scale, noValue) {
    validateColorScale(scale);
    const length = parseInt(scale.length);

    // set style name
    let styles = "";
    if (length == 0) {
        styles = "";
    } else if (length <= 2) {
        styles = "template_2col";
    } else {
        styles = `template_${length}col`;
    }

    // set style configuration
    let env = "";
    for (let i = 0; i < scale.length; ++i) {
        const varIndex = parseInt((scale.length == 1) ? 2 : i + 1);
        const item = scale[i];

        if (item.color !== false) env += `col${varIndex}:${item.color};`;
        if (item.value !== false) env += `val${varIndex}:${item.value};`;
        if (item.opacity !== false) env += `opa${varIndex}:${item.opacity};`;
    }

    if (noValue) {
        validateColorItem(noValue);
        if (noValue.color !== false) env += `colnv:${noValue.color};`;
        if (noValue.value !== false) env += `valnv:${noValue.value};`;
        if (noValue.opacity !== false) env += `opanv:${noValue.opacity};`;
    }

    return {
        styles: styles,
        env: env
    }
}

/**
 * Stretch the color scale values linear with equal distance
 * 
 * @author rhess <robin.hess@awi.de>
 * @memberof vef.ui.color
 * 
 * @param {object[]} scale color scale array
 * @param {number} min 
 * @param {number} max
 */
export function stretchColorScale(scale, min, max) {
    validateColorScale(scale);
    if (!Number.isFinite(min) || !Number.isFinite(max)) throw new Error("invalid min/max values");

    const range = Math.abs(max - min);
    const count = scale.length - 1;

    for (let i = 0; i < scale.length; ++i) {
        scale[i].value = min + ((i / count) * range);
    }

    return scale;
}

/**
 * Invert the color scale value order
 * 
 * @author rhess <robin.hess@awi.de>
 * @memberof vef.ui.color
 * 
 * @param {object[]} scale color scale array
 */
export function invertColorScale(scale) {
    validateColorScale(scale);

    const halfLength = Math.floor(scale.length / 2);
    for (let i = 0; i < halfLength; ++i) {
        const j = scale.length - 1 - i;
        const temp = scale[i].value;
        scale[i].value = scale[j].value;
        scale[j].value = temp;

    }

    return scale;
}

/**
 * Generate a CSS-gradient based on the
 * given color scale
 * 
 * @author rhess <robin.hess@awi.de>
 * @memberof vef.ui.color
 * 
 * @param {object[]} scale 
 * @param {number} min custom minimum value (optional)
 * @param {number} max custom maximum value (optional)
 */
export function generateCssGradient(scale, min, max) {
    let gradient = "";
    max = (Number.isFinite(max)) ? max : scale[scale.length - 1].value;
    min = (Number.isFinite(min)) ? min : scale[0].value;
    for (let j = 0; j < scale.length; ++j) {
        const percentage = parseInt((scale[j].value - min) / (max - min) * 100);
        gradient += `,${scale[j].color} ${percentage}%`;
    }
    return `linear-gradient(to right ${gradient}`;
}

/**
 * Get the matching color from a given scale for a value with
 * linear interpolation
 * 
 * @author rhess <robin.hess@awi.de>
 * @memberof vef.ui.color
 * 
 * @param {object[]} scale 
 * @param {number} value 
 * @param {string} format "hexstring", "rgb", (default "hexstring")
 */
export function getColorFromScale(scale, value, format) {
    let min = 0;
    let max = scale.length - 1;
    for (let i = 0; i < scale.length; ++i) {
        if (scale[i].value <= value) min = i;
        if (scale[i].value >= value) max = i;
    }

    if (value <= scale[min].value) {
        const color = new iro.Color(scale[min].color);
        return {
            color: (format == "rgb") ? color.rgb : color.hexString,
            opacity: scale[min].opacity,
        };
    } else if (value >= scale[max].value) {
        const color = new iro.Color(scale[max].color);
        return {
            color: (format == "rgb") ? color.rgb : color.hexString,
            opacity: scale[max].opacity,
        };
    } else {
        const range = scale[max].value - scale[min].value;
        const ratio = (value - scale[min].value) / range;

        const opacityRange = scale[max].opacity - scale[min].opacity;
        const opacity = scale[min].opacity + (opacityRange * ratio);

        const colorMin = new iro.Color(scale[min].color).rgb;
        const colorMax = new iro.Color(scale[max].color).rgb;
        for (let i in colorMin) {
            const channelRange = colorMax[i] - colorMin[i];
            colorMin[i] = Math.floor(colorMin[i] + (channelRange * ratio));
        }

        const color = new iro.Color(colorMin);

        return {
            color: (format == "rgb") ? color.rgb : color.hexString,
            opacity: opacity
        }
    }

}