Source: ui/color/ColorScalePicker.js

import iro from '@jaames/iro';

import * as Utils from "./Utils.js";
import { UiElement } from "../UiElement.js";
import { ColorScaleDropdown } from "./ColorScaleDropdown.js";
import { ColorSlider } from "./ColorSlider.js";
import { roundNumber } from "../../utils/utils.js";
import "./ColorScalePicker.css";

export { ColorScalePicker };

/**
 * Create custom color scales in the Ui using a color picker
 * 
 * @author rhess <robin.hess@awi.de>
 * @memberof vef.ui.color
 */
class ColorScalePicker extends UiElement {

    /**
     * options = {
     *   maxItems: number (default: 5)
     *   min: number (default: 0),
     *   max: number (default: 1)
     * }
     *
     * @param {string/HTMLElement} target
     * @param {object} options
     */
    constructor(target, options) {
        super(target, {
            "apply": []
        });

        const defaultOptions = {
            maxItems: 5,
            min: 0,
            max: 1
        };

        this.options_ = { ...defaultOptions, ...options };
        this.items_ = [];

        this.presetDropdown_ = null;
        this.presetSelected_ = false;

        this.initElement_();
    }

    /**
     * Add a color item to the scale
     * 
     * @param {object} item color item
     */
    addColor(item) {
        if (this.presetDropdown_) this.presetDropdown_.deselect();
        if (this.colorSlider_.getLength() >= this.options_.maxItems) return null;

        item = (item) ? Utils.validateColorItem(item) : {};
        const handle = this.colorSlider_.addHandle(item);
        this.colorSlider_.selectHandle(handle);

        this.updateAddButton_();
        this.updateSliderVisibility_();
    }

    /**
     * Change config of an existing color item
     * 
     * @param {object} item new item config 
     * @param {number} index index of item
     */
    setColor(item, index) {
        if (this.presetDropdown_) this.presetDropdown_.deselect();
        const handle = this.colorSlider_.getHandle(index);
        item = Object.assign(handle, Utils.validateColorItem(item));
        this.colorSlider_.update();
    }

    /**
     * Delete a color item from the color scale
     * 
     * @param {number} index index of item
     */
    deleteColor(index) {
        if (this.presetDropdown_) this.presetDropdown_.deselect();
        this.colorSlider_.removeHandle(this.colorSlider_.getHandle(index));

        this.updateAddButton_();
        this.updateSliderVisibility_();
    }

    /**
     * Get the color scale as an array
     */
    getColorScale() {
        return this.colorSlider_.getColorScale();
    }

    /**
     * Set the scale of the color chooser
     * based of an existing Array of color items
     * 
     * @param {object[]} scale color scale array
     */
    setScale(scale) {
        if (!scale) {
            this.clearItems();
        } else {
            if (this.presetDropdown_) this.presetDropdown_.deselect();

            this.colorSlider_.setColorScale(scale)

            this.updateAddButton_();
            this.updateSliderVisibility_();
        }
    }

    /**
     * Remove all items from the ColorChooser
     */
    clearItems() {
        if (this.presetDropdown_) this.presetDropdown_.deselect();
        this.colorSlider_.clear();

        this.updateAddButton_();
        this.updateSliderVisibility_();
    }

    /**
     * set presets to be selected by the user from a
     * dropdown list
     * 
     * @param {object[]} presets
     */
    setPresets(presets) {
        if (this.presetDropdown_) this.presetDropdown_.getElement().remove();
        this.presetDropdown_ = new ColorScaleDropdown(null, {
            items: presets,
            placeholder: "select a color scale ...",
            showDeselectItem: false
        });
        this.presetDropdown_.on("select", payload => {
            const scale = payload.item;
            this.colorSlider_.setColorScale(scale);
            this.updateAddButton_();
            this.updateSliderVisibility_();
        });

        this.presetDropdown_.appendTo(this.getElement().querySelector(".preset-container"));
    }

    /**
     * show the color picker for a given color item object
     * 
     * @param {object} item 
     * @private
     */
    showColorPicker_(item) {
        this.colorPickerElement_.style.display = "block";
        const selectedItem = this.colorSlider_.getSelectedHandle();

        // storing opacity, because "set" changes item.opacity
        const opacity = selectedItem.opacity;
        this.colorPicker_.color.set(selectedItem.color);
        this.colorPicker_.color.alpha = opacity;
    }

    /**
     * check if the add-button has to be hidden, depending on the amount
     * of colors compared to options.maxItems
     * 
     * @private
     */
    updateAddButton_() {
        if (this.colorSlider_.getLength() < this.options_.maxItems) {
            this.getElement().querySelector(".btn-add-color").disabled = false;
        } else {
            this.getElement().querySelector(".btn-add-color").disabled = true;
        }
    }

    /**
     * check if the slider has to be hidden, if there are no items
     * 
     * @private
     */
    updateSliderVisibility_() {
        const get = sel => this.getElement().querySelector(sel);
        if (this.colorSlider_.getLength() == 0) {
            get(".color-slider-placeholder").style.display = "none";
            get(".min-max-form").style.display = "none";
        } else {
            get(".color-slider-placeholder").style.display = "block";
            get(".min-max-form").style.display = "block";
        }
    }

    /**
     * hide the color picker element
     * 
     * @private
     */
    hideColorPicker_() {
        this.colorPickerElement_.style.display = "none";
    }

    /**
     * Initialize the main Ui of the color chooser.
     * Called only once in the constructor
     * 
     * @private
     */
    initElement_() {
        const element = this.getElement();
        element.classList.add("color-scale-picker");

        element.insertAdjacentHTML("beforeend", `
            <div class="preset-container"></div>
            <div class="min-max-form">
                <button title="stretch the scale with equal distances in the given range" class="btn-linear-stretch"><i class="fas fa-ruler"></i>Stretch</button>
                <button title="invert the color scale order" class="btn-invert"><i class="fas fa-exchange-alt"></i> Invert</button>
                <button title="remove overhang range (adjust min and max)" class="btn-adjust-range"><i class="fas fa-arrows-alt-h"></i> Clip Range</button>
                <br/>
                <input spellcheck="false" title="minimum value" class="min" type="text" value="${this.options_.min}"/><div class="separator">-</div><input title="maximum value" class="max" type="text" value="${this.options_.max}"/>
            </div>
            <div class="color-slider-placeholder"></div>
            <div class="color-picker">
                <div class="iro-color-picker"></div>
                <div class="color-picker-form">
                    <span>Value</span><input spellcheck="false" type="text" class="value-input"/><br/>
                    <span>R</span><input spellcheck="false" type="text" class="color-picker-form-r"/><br/>
                    <span>G</span><input spellcheck="false" type="text" class="color-picker-form-g"/><br/>
                    <span>B</span><input spellcheck="false" type="text" class="color-picker-form-b"/><br/>
                    <span>HEX</span><input spellcheck="false" type="text" class="color-picker-form-hex"/>
                    <br/><button class="btn-delete-color"><i class="fas fa-trash-alt"></i> Delete</button>
                </div>
            </div>
            <div class="footer-buttons">
                <button class="btn-add-color"><i class="fas fa-plus"></i> Add Color</button><!--
             --><button class="btn-clear"><i class="fas fa-times"></i> Clear</button><!--
             --><button class="btn-apply-colors"><i class="fas fa-check"></i> Apply</button>
            </div>
        `);

        this.colorSlider_ = new ColorSlider(element.querySelector(".color-slider-placeholder"));

        // value range input with synchronisation

        const btnMin = element.querySelector(".min-max-form .min");
        const btnMax = element.querySelector(".min-max-form .max");
        let activeFitToScale = false;

        const fitToScale = () => {
            const min = parseFloat(btnMin.value);
            const max = parseFloat(btnMax.value);
            if (Number.isFinite(min) && Number.isFinite(max)) {
                this.colorSlider_.min_ = min;
                this.colorSlider_.max_ = max;
                activeFitToScale = true;
                this.colorSlider_.update();
            }
        };
        btnMin.addEventListener("input", fitToScale);
        btnMax.addEventListener("input", fitToScale);

        this.colorSlider_.on("update", () => {
            if (activeFitToScale) {
                activeFitToScale = false;
                const min = parseFloat(btnMin.value);
                const max = parseFloat(btnMax.value);

                if (this.colorSlider_.min_ != min) {
                    btnMin.classList.add("error")
                } else {
                    btnMin.classList.remove("error")
                }
                if (this.colorSlider_.max_ != max) {
                    btnMax.classList.add("error")
                } else {
                    btnMax.classList.remove("error")
                }
            } else {
                btnMax.classList.remove("error");
                btnMin.classList.remove("error");

                btnMax.value = roundNumber(this.colorSlider_.max_, 5);
                btnMin.value = roundNumber(this.colorSlider_.min_, 5);
            }

            this.options_.min = this.colorSlider_.min_;
            this.options_.max = this.colorSlider_.max_;
        })

        // general buttons

        element.querySelector(".btn-adjust-range").addEventListener("click", () => {
            this.colorSlider_.update(true);
        }, false);

        element.querySelector(".btn-linear-stretch").addEventListener("click", () => {
            this.colorSlider_.stretchToScale(this.options_.min, this.options_.max);
        }, false);

        element.querySelector(".btn-invert").addEventListener("click", () => {
            this.colorSlider_.invertScale();
        }, false);

        element.querySelector(".btn-add-color").addEventListener("click", () => {
            this.addColor(null, true);
        }, false);

        element.querySelector(".btn-apply-colors").addEventListener("click", () => {
            this.colorSlider_.update();
            this.apply_();
        }, false);

        element.querySelector(".btn-clear").addEventListener("click", () => {
            this.clearItems();
        }, false);

        element.querySelector(".btn-delete-color").addEventListener("click", () => {
            if (this.presetDropdown_) this.presetDropdown_.deselect();
            const selectedItem = this.colorSlider_.getSelectedHandle();
            if (selectedItem) this.colorSlider_.removeHandle(selectedItem);
            this.updateAddButton_();
            this.updateSliderVisibility_();
        }, false);

        const colorPicker = new iro.ColorPicker(element.querySelector(".iro-color-picker"), {
            width: 120,
            color: "rgb(255, 0, 0)",
            borderWidth: 1,
            borderColor: "#fff",
            layoutDirection: "horizontal",
            layout: [
                {
                    component: iro.ui.Wheel,
                    options: {}
                },
                {
                    component: iro.ui.Slider,
                    options: {
                        sliderType: 'value',
                        sliderSize: 10
                    }
                },
                {
                    component: iro.ui.Slider,
                    options: {
                        sliderType: 'alpha',
                        sliderSize: 10
                    }
                }
            ]
        });

        // color channel input elements
        const colorChannels = { "r": null, "g": null, "b": null };

        for (let i in colorChannels) {
            colorChannels[i] = element.querySelector(".color-picker-form-" + i);
            colorChannels[i].addEventListener("change", () => {
                colorPicker.color.setChannel('rgb', i, colorChannels[i].value);
            });
        }

        const hex = element.querySelector(".color-picker-form-hex");
        hex.addEventListener("change", () => {
            colorPicker.color.set(hex.value);
        });

        const valueInput = element.querySelector(".value-input");
        valueInput.addEventListener("input", () => {
            const val = parseFloat(valueInput.value);
            if (Number.isFinite(val)) {
                const handle = this.colorSlider_.getSelectedHandle();
                handle.value = val;
                this.colorSlider_.update();
            }
        });

        colorPicker.on(["color:init", "color:change"], (color) => {
            if (this.presetDropdown_) this.presetDropdown_.deselect();
            const selectedItem = this.colorSlider_.getSelectedHandle();
            if (selectedItem) {
                selectedItem.color = colorPicker.color.hexString;
                selectedItem.opacity = colorPicker.color.alpha;
                this.colorSlider_.update();
            }
            colorChannels["r"].value = color.rgb.r;
            colorChannels["g"].value = color.rgb.g;
            colorChannels["b"].value = color.rgb.b;
            hex.value = color.hexString;
        });

        this.colorPicker_ = colorPicker;
        this.colorPickerElement_ = element.querySelector(".color-picker");

        const showValue = () => {
            valueInput.value = this.colorSlider_.getSelectedHandle().value;
        };

        this.colorSlider_.on("select", () => {
            this.showColorPicker_();
            showValue();
        });
        this.colorSlider_.on("deselect", () => this.hideColorPicker_());
        this.colorSlider_.on("slide", showValue);
        this.colorSlider_.on("stop", showValue);
    }

    /**
     * Trigger a change event and generate the event payload
     * containing WMS params for O2A template styles
     * 
     * @private
     */
    apply_() {
        this.fire("apply", this.getColorScale());
    }

}