PMA.UI Documentation by Pathomation

view/controls/dimensionSelector.js

import { Control } from "ol/control";
import noUiSlider from "nouislider";
import "nouislider/distribute/nouislider.css";
import { guidGenerator } from "../helpers";
import { Resources } from "../../resources/resources";

const steps = [
    {
        step: 1,
        class: "x1",
    },
    {
        step: 0.5,
        class: "x2",
    },
    {
        step: 0.2,
        class: "x3",
    },
    {
        step: 0.1,
        class: "x4",
    },
    {
        step: 0.05,
        class: "x5",
    },
];

export /**
 * Creates a new dimension selector control
 * @alias DimensionSelector
 * @memberof PMA.UI.View.Controls
 * @param {object} opt_options Options to initialize
 * @param {Viewport} opt_options.pmaViewport Viewport instance this control belongs to
 * @param {string} opt_options.tipLabel Label for the button
 * @param {boolean} opt_options.collapsed Whether the control starts collapsed
 * @param {Object} opt_options.stateManager The state manager to keep settings in sync
 */
class DimensionSelector extends Control {
    constructor(opt_options) {
        var options = opt_options || {};
        var element = document.createElement("div");

        super({
            element: element,
            target: options.target,
        });

        this.options = options;
        this.pmaViewport = options.pmaViewport;
        this.stateManager = options.stateManager ? options.stateManager : null;
        if (this.stateManager) {
            if (!this.stateManager.dimensionSelector) {
                this.stateManager.dimensionSelector = {};
                this.stateManager.dimensionSelector.collapsed = options.collapsed ? options.collapsed : false;
            }

            this.collapsed_ = this.stateManager.dimensionSelector.collapsed === true;
        } else {
            this.collapsed_ = options.collapsed ? options.collapsed : false;
        }

        this.sliders = [];
        this.slidersValues = [];
        this.currentStep = 0;

        var className = options.className ? options.className : "ol-dimension-selector";
        this.tipLabel = options.tipLabel ? options.tipLabel : "Channels";

        var collapseLabel = options.collapseLabel ? options.collapseLabel : "\u00BB";
        this.collapseLabel_ = document.createElement("span");
        this.collapseLabel_.innerHTML = collapseLabel;

        var label = options.label ? options.label : "\u00AB";
        this.label_ = document.createElement("span");
        this.label_.innerHTML = label;

        var activeLabel = this.collapsed_ ? this.collapseLabel_ : this.label_;

        var button = document.createElement("button");
        button.type = "button";
        button.title = this.tipLabel;
        button.appendChild(activeLabel);
        button.className = "collapse-button";
        button.style = "float: left";
        this.btnEventUsed = "click";

        if ("ontouchstart" in document.documentElement) {
            this.btnEventUsed = "touchstart";
        }

        button.addEventListener(this.btnEventUsed, this.buttonClk.bind(this), false);

        var cssClasses = className + " " + "ol-unselectable ol-control " + (this.collapsed_ ? " ol-collapsed" : "");

        this.dimensionsDiv = document.createElement("div");
        this.dimensionsDiv.className = "ol-dimensions-container";
        this.renderDimensionsControl();

        this.dimensionControlsDiv = document.createElement("div");
        this.dimensionControlsDiv.className = "ol-dimensions-container";
        this.renderSelectors(true);

        element.className = cssClasses;
        if (this.dimensionsDiv && this.dimensionsDiv !== "") {
            element.appendChild(this.dimensionsDiv);
        }

        if (this.dimensionControlsDiv && this.dimensionControlsDiv !== "") {
            element.appendChild(this.dimensionControlsDiv);
        }

        element.appendChild(button);

        var tipLabel = "";
        var label_ = "";

        tipLabel = "Decrease resolution";
        label = '<i class="fa fa-minus" aria-hidden="true"></i>';
        label_ = document.createElement("span");
        label_.innerHTML = label;

        this.lastSelection = -1;

        //this.element.addEventListener("wheel", this.wheelEvent.bind(this), false);
        this.pmaViewport.element.addEventListener(
            "wheel",
            (e) => {
                if (e.ctrlKey) {
                    this.wheelEvent(e, true);
                }
                if (e.shiftKey) {
                    this.wheelEvent(e, false);
                }
            },
            false
        );

        /*this.pmaViewport.listen("change:dimension", function () {
            _this.destroySliders();
            _this.renderDimensionsControl();
            _this.renderSelectors(false);
            _this.renderSliders(_this.currentStep);
        });*/
    }

    buttonClk(event) {
        if (event) {
            event.preventDefault();
            event.stopPropagation();
        }

        if ((" " + this.element.className + " ").indexOf(" ol-collapsed ") > -1) {
            this.element.className = this.element.className.replace(/ol-collapsed/g, "");
        } else {
            this.element.className += " ol-collapsed";
        }

        if (!isBrightfield(this.pmaViewport.imageInfo.TimeFrames[0].Layers[0])) {
            if (!this.collapsed_) {
                this.label_.parentNode.replaceChild(this.collapseLabel_, this.label_);
            } else {
                this.collapseLabel_.parentNode.replaceChild(this.label_, this.collapseLabel_);
            }
        }
        this.collapsed_ = !this.collapsed_;
        if (this.stateManager) {
            this.stateManager.dimensionSelector.collapsed = this.collapsed_;
        }
    }

    /**
     * Gets the collapsed state of the control
     * @return {boolean} True if the control is currently collapsed
     */
    getCollapsed() {
        return (" " + this.element.className + " ").indexOf(" ol-collapsed ") > -1;
    }

    /**
     * Sets the collapsed state of the control
     * @param {boolean} collapsed - True to collapse the control, otherwise false
     */
    setCollapsed(collapsed) {
        if (this.getCollapsed() != collapsed) {
            this.buttonClk();
        }
    }

    renderDimensionsControl() {
        // assign change event to the channel check boxes
        function chkChange() {
            // assign the selected channels back to the viewport
            var chans = [];
            var chks = this.dimensionsDiv.querySelectorAll(".ol-dimensions-container ul input[type='checkbox']");
            for (var i = 0; i < chks.length; i++) {
                if (chks[i].checked) {
                    chans.push(i);
                    this.lastSelection = i;
                }
            }

            if (chans.length === 0 && this.lastSelection >= 0 && this.lastSelection < chks.length) {
                chks[this.lastSelection].checked = true;
                chans.push(this.lastSelection);
            }

            this.pmaViewport.setActiveChannels(chans);
        }

        this.sliders = [];
        if (!isBrightfield(this.pmaViewport.imageInfo.TimeFrames[0].Layers[0])) {
            var channelsHtml = "<div id='channels'>" + this.tipLabel + "</div><div id='step'></div><ul>";

            const rendOpts = this.pmaViewport.getChannelRenderingOptions();

            for (var i = 0; i < this.pmaViewport.imageInfo.TimeFrames[0].Layers[0].Channels.length; i++) {
                const guid = guidGenerator();
                this.sliders.push(guid);
                var c = this.pmaViewport.imageInfo.TimeFrames[0].Layers[0].Channels[i];
                this.slidersValues.push(rendOpts[i].clipping);
                channelsHtml += "<li style='background-color: #" + rendOpts[i].color.substr(2, 6) + "'>";
                channelsHtml +=
                    "<span class='channel-span'><label title='" +
                    rendOpts[i].name +
                    "' for='" +
                    `${guid}_channel_checkbox` +
                    "'>" +
                    rendOpts[i].name +
                    "</label><input id='" +
                    `${guid}_channel_checkbox` +
                    "' type='checkbox' " +
                    (c.Active ? "checked='checked'" : "") +
                    " />";
                channelsHtml +=
                    "<button id='" +
                    `${guid}_color_reset` +
                    "' title=\"Click to reset channel's color to initial value\" class='color-reset' style='" +
                    (this.options.supportsRenderingOptions ? "" : "display: none") +
                    "'><i class='fa fa-undo' aria-hidden='true'></i></button>";
                channelsHtml +=
                    "<input id='" +
                    `${guid}_color_selector` +
                    "' title=\"Click to select channel's color\" type='color' class='color-selector' style='" +
                    (this.options.supportsRenderingOptions ? "" : "display: none") +
                    "' data-default-color='#" +
                    rendOpts[i].defaultColor.substr(2, 6) +
                    "' value='#" +
                    rendOpts[i].color.substr(2, 6) +
                    "' />";
                channelsHtml += "</span>";
                if (c.Name !== "Default") {
                    channelsHtml += "<span class='slider-span' id='" + `${guid}_slider` + "'></span>";
                }

                channelsHtml += "</li>";
            }

            channelsHtml += "</ul>";

            if (this.sliders.length > 0) {
                channelsHtml += "<div style='display: block; height: 25px;'>";
                channelsHtml += "<button id='increaseButton' type='button' title='Increase resolution' style='float: right;'>";
                channelsHtml += "<span style='font-size: 10px'><i class='fa fa-plus' aria-hidden='true'></i></span>";
                channelsHtml += "</button>";
                channelsHtml += "<button id='decreaseButton' type='button' title='Decrease resolution' style='float: right;'>";
                channelsHtml += "<span style='font-size: 10px'><i class='fa fa-minus' aria-hidden='true'></i></span>";
                channelsHtml += "</button>";
                channelsHtml += "</div>";
            }

            this.dimensionsDiv.innerHTML = channelsHtml;

            var checkboxes = this.dimensionsDiv.querySelectorAll(".ol-dimensions-container ul input[type='checkbox']");
            for (i = 0; i < checkboxes.length; i++) {
                checkboxes[i].addEventListener("change", chkChange.bind(this), false);
            }

            if (this.sliders.length > 0) {
                var _this = this;
                this.dimensionsDiv.querySelector("#increaseButton").addEventListener(
                    "click",
                    function () {
                        _this.destroySliders();
                        _this.renderDimensionsControl();
                        _this.renderSelectors(false);
                        _this.currentStep = _this.currentStep < 4 ? _this.currentStep + 1 : _this.currentStep;
                        _this.renderSliders(_this.currentStep);
                    },
                    false
                );

                this.dimensionsDiv.querySelector("#decreaseButton").addEventListener(
                    "click",
                    function () {
                        _this.destroySliders();
                        _this.renderDimensionsControl();
                        _this.renderSelectors(false);
                        _this.currentStep = _this.currentStep > 0 ? _this.currentStep - 1 : _this.currentStep;
                        _this.renderSliders(_this.currentStep);
                    },
                    false
                );
            }
        }
    }

    renderSelectors(initialRender) {
        function dimensionChange(e) {
            if (e.target.name == "timeframe") {
                this.pmaViewport.setActiveTimeFrame(e.target.value);
            } else if (e.target.name == "zstack") {
                this.pmaViewport.setActiveLayer(e.target.value);
            }
            if (document.getElementById("zstackSlider")) {
                document.getElementById("zstackLabel").innerHTML = Resources.translate("Z-stack: <br /> {stack} / {total}", {
                    stack: this.pmaViewport.getActiveLayer() + 1,
                    total: this.pmaViewport.imageInfo.TimeFrames[this.pmaViewport.getActiveTimeFrame()].Layers.length,
                });
            }
            if (document.getElementById("timeframeSlider")) {
                document.getElementById("timeframeLabel").innerHTML = Resources.translate("Time frame: <br /> {frame} / {total}", {
                    frame: this.pmaViewport.getActiveTimeFrame() + 1,
                    total: this.pmaViewport.imageInfo.TimeFrames.length,
                });
            }
        }

        function prevNextClicked(e) {
            var tf = this.pmaViewport.getActiveTimeFrame();
            var l = this.pmaViewport.getActiveLayer();
            if (e.target.name == "prev-timeframe") {
                tf--;
                if (tf < 0) {
                    tf = this.pmaViewport.imageInfo.TimeFrames.length - 1;
                }

                this.pmaViewport.setActiveTimeFrame(tf);
            } else if (e.target.name == "next-timeframe") {
                tf++;
                if (tf >= this.pmaViewport.imageInfo.TimeFrames.length) {
                    tf = 0;
                }
                this.pmaViewport.setActiveTimeFrame(tf);
            } else if (e.target.name == "prev-zstack") {
                l--;
                if (l < 0) {
                    l = this.pmaViewport.imageInfo.TimeFrames[tf].Layers.length - 1;
                }
                this.pmaViewport.setActiveLayer(l);
            } else if (e.target.name == "next-zstack") {
                l++;
                if (l >= this.pmaViewport.imageInfo.TimeFrames[tf].Layers.length) {
                    l = 0;
                }
                this.pmaViewport.setActiveLayer(l);
            }
            if (document.getElementById("zstackSlider")) {
                document.getElementById("zstackSlider").value = l;
                document.getElementById("zstackLabel").innerHTML = Resources.translate("Z-stack: <br /> {stack} / {total}", {
                    stack: this.pmaViewport.getActiveLayer() + 1,
                    total: this.pmaViewport.imageInfo.TimeFrames[this.pmaViewport.getActiveTimeFrame()].Layers.length,
                });
            }
            if (document.getElementById("timeframeSlider")) {
                document.getElementById("timeframeSlider").value = tf;
                document.getElementById("timeframeLabel").innerHTML = Resources.translate("Time frame: <br /> {frame} / {total}", {
                    frame: this.pmaViewport.getActiveTimeFrame() + 1,
                    total: this.pmaViewport.imageInfo.TimeFrames.length,
                });
            }
        }

        var HTMLstring = "";
        if (this.pmaViewport.imageInfo.TimeFrames.length > 1) {
            HTMLstring +=
                '<div id="timeframeLabel">' +
                Resources.translate("Time frame: <br /> {frame} / {total}", {
                    frame: this.pmaViewport.getActiveTimeFrame() + 1,
                    total: this.pmaViewport.imageInfo.TimeFrames.length,
                }) +
                "</div>";

            HTMLstring += "<button name='prev-timeframe' class='prev-next-button' title=\"Click to switch to previous time frame\">&lt;</button>";
            HTMLstring +=
                '<input type="range" min="0" max="' +
                (this.pmaViewport.imageInfo.TimeFrames.length - 1) +
                '" value="' +
                this.pmaViewport.getActiveTimeFrame() +
                '" name="timeframe" id="timeframeSlider" title="Drag to switch between time frames (Shift + MouseWheel)">';
            HTMLstring += "<button name='next-timeframe' class='prev-next-button' title=\"Click to switch to next time frame\">&gt;</button>";
        }

        var tf = this.pmaViewport.imageInfo.TimeFrames[this.pmaViewport.getActiveTimeFrame()];
        if (tf.Layers.length > 1) {
            // if it's the first time we draw the control, select the middle z-stack layer
            if (initialRender) {
                this.pmaViewport.setActiveLayer(Math.round((tf.Layers.length - 1) / 2) | 0);
            }

            var selectedZStack = this.pmaViewport.getActiveLayer();

            HTMLstring +=
                '<div id="zstackLabel">' +
                Resources.translate("Z-stack: <br /> {stack} / {total}", { stack: selectedZStack + 1, total: tf.Layers.length }) +
                "</div>";
            HTMLstring += "<button name='prev-zstack' class='prev-next-button' title=\"Click to switch to previous z-stack\">&lt;</button>";
            HTMLstring +=
                '<input type="range" min="0" max="' +
                (tf.Layers.length - 1) +
                '" value="' +
                selectedZStack +
                '" name="zstack" id="zstackSlider" title="Drag to switch between z-stacks (Ctrl + MouseWheel)">';
            HTMLstring += "<button name='next-zstack' class='prev-next-button' title=\"Click to switch to next z-stack\">&gt;</button>";
        }

        if (HTMLstring !== "") {
            this.dimensionControlsDiv.innerHTML = "<div>" + HTMLstring + "</div>";
            var selects = this.dimensionControlsDiv.querySelectorAll(".ol-dimensions-container input[type=range]");
            for (var k = 0; k < selects.length; k++) {
                selects[k].addEventListener("input", dimensionChange.bind(this), false);
            }

            var btns = this.dimensionControlsDiv.querySelectorAll(".ol-dimensions-container .prev-next-button");
            for (k = 0; k < btns.length; k++) {
                btns[k].addEventListener(this.btnEventUsed, prevNextClicked.bind(this), false);
            }

            if (this.dimensionControlsDiv.querySelector("#zstackSlider"))
                this.dimensionControlsDiv.querySelector("#zstackSlider").addEventListener("wheel", (e) => this.wheelEvent(e, true), false);
            if (this.dimensionControlsDiv.querySelector("#timeframeSlider"))
                this.dimensionControlsDiv.querySelector("#timeframeSlider").addEventListener("wheel", (e) => this.wheelEvent(e, false), false);
        }
    }

    wheelEvent(e, zStack) {
        e.preventDefault();
        if ((" " + this.element.className + " ").indexOf(" ol-collapsed ") > -1) {
            return;
        }

        if (
            !this.pmaViewport.imageInfo.TimeFrames ||
            (this.pmaViewport.imageInfo.TimeFrames.length === 1 &&
                this.pmaViewport.imageInfo.TimeFrames[0].Layers &&
                this.pmaViewport.imageInfo.TimeFrames[0].Layers.length === 1)
        ) {
            return;
        }

        var tf = this.pmaViewport.getActiveTimeFrame();
        var l = this.pmaViewport.getActiveLayer();
        if (e.deltaY && e.deltaY < 0) {
            if (zStack) {
                l--;
                if (l < 0) {
                    ////l = this.pmaViewport.imageInfo.TimeFrames[tf].Layers.length - 1;
                    l = 0;
                }
                this.pmaViewport.setActiveLayer(l);
            } else {
                tf--;
                if (tf < 0) {
                    tf = 0;
                }
                this.pmaViewport.setActiveLayer(l);
                this.pmaViewport.setActiveTimeFrame(tf);
            }
        } else if (e.deltaY && e.deltaY > 0) {
            if (zStack) {
                l++;
                if (l >= this.pmaViewport.imageInfo.TimeFrames[tf].Layers.length) {
                    ////l = 0;
                    l = this.pmaViewport.imageInfo.TimeFrames[tf].Layers.length - 1;
                }
                this.pmaViewport.setActiveLayer(l);
            } else {
                tf++;
                if (tf >= this.pmaViewport.imageInfo.TimeFrames.length) {
                    ////l = 0;
                    tf = this.pmaViewport.imageInfo.TimeFrames.length - 1;
                }
                this.pmaViewport.setActiveLayer(l);
                this.pmaViewport.setActiveTimeFrame(tf);
            }
        }

        if (document.getElementById("zstackSlider")) {
            document.getElementById("zstackSlider").value = l;
            document.getElementById("zstackLabel").innerHTML = Resources.translate("Z-stack: <br /> {stack} / {total}", {
                stack: this.pmaViewport.getActiveLayer() + 1,
                total: this.pmaViewport.imageInfo.TimeFrames[this.pmaViewport.getActiveTimeFrame()].Layers.length,
            });
        }
        if (document.getElementById("timeframeSlider")) {
            document.getElementById("timeframeSlider").value = tf;
            document.getElementById("timeframeLabel").innerHTML = Resources.translate("Time frame: <br /> {frame} / {total}", {
                frame: this.pmaViewport.getActiveTimeFrame() + 1,
                total: this.pmaViewport.imageInfo.TimeFrames.length,
            });
        }
    }

    round(value, precision) {
        var multiplier = Math.pow(10, precision || 0);
        return Math.round(value * multiplier) / multiplier;
    }

    renderSliders(currentStep = 0) {
        if (this.sliders.length === 0) {
            return;
        }

        const self = this;

        for (var i = 0; i < this.sliders.length; i++) {
            var start;
            if (this.slidersValues[i]) {
                start = this.slidersValues[i];
            } else {
                start = [0, 100];
            }
            const guid = this.sliders[i];
            var slider = document.getElementById(`${guid}_slider`);
            if (slider) {
                slider.title = "Drag to adjust channel clipping";
                steps.forEach((s) => {
                    slider.parentElement.classList.remove(s.class);
                });
                slider.parentElement.classList.add(steps[currentStep].class);

                document.getElementById("step").innerText = "Step: " + steps[currentStep].step;

                noUiSlider.create(slider, {
                    start: start,
                    tooltips: [true, true],
                    step: steps[currentStep].step,
                    connect: true,
                    margin: steps[currentStep].step,
                    keyboardSupport: true,
                    range: {
                        min: 0,
                        max: 100,
                    },
                    format: {
                        to: function (value) {
                            return self.round(value, 2).toFixed(2);
                        },
                        from: function (value) {
                            return self.round(value, 2).toFixed(2);
                        },
                    },
                });

                slider.noUiSlider.channelIndex = i;

                slider.noUiSlider.on("change", function (value) {
                    const colorValue = document.getElementById(this.target.id.replace("slider", "color_selector")).value;
                    self.pmaViewport.setChannelRenderingOptions({ index: this.channelIndex, clipping: value, color: colorValue.replace("#", "ff") });
                });
            }

            var colorSelector = document.getElementById(`${guid}_color_selector`);
            if (colorSelector) {
                $(colorSelector).data("channel", i);
                colorSelector.addEventListener("change", function (ev) {
                    var clippingValue;
                    const clippingElement = document.getElementById(ev.target.id.replace("color_selector", "slider"));
                    if (clippingElement) {
                        const clippingSlider = clippingElement.noUiSlider;
                        if (clippingSlider) {
                            clippingValue = clippingSlider.get();
                        }
                    }
                    if (!clippingValue) clippingValue = ["0.00", "100.00"];
                    document.getElementById(ev.target.id.replace("color_selector", "slider")).parentElement.style = "background-color: " + ev.target.value;
                    self.pmaViewport.setChannelRenderingOptions({
                        index: $(ev.target).data("channel"),
                        clipping: clippingValue,
                        color: ev.target.value.replace("#", "ff"),
                    });
                });
            }

            var colorReset = document.getElementById(`${guid}_color_reset`);
            if (colorReset) {
                $(colorReset).data("channel", i);
                colorReset.addEventListener("click", function (ev) {
                    this.blur();
                    const defaultColor = $(document.getElementById(ev.currentTarget.id.replace("color_reset", "color_selector"))).data("default-color");
                    document.getElementById(ev.currentTarget.id.replace("color_reset", "color_selector")).value = defaultColor;
                    var clippingValue;
                    const clippingElement = document.getElementById(ev.currentTarget.id.replace("color_reset", "slider"));
                    if (clippingElement) {
                        const clippingSlider = clippingElement.noUiSlider;
                        if (clippingSlider) {
                            clippingValue = clippingSlider.get();
                        }
                    }
                    if (!clippingValue) clippingValue = ["0.00", "100.00"];
                    document.getElementById(ev.currentTarget.id.replace("color_reset", "slider")).parentElement.style = "background-color: " + defaultColor;
                    self.pmaViewport.setChannelRenderingOptions({
                        index: $(ev.currentTarget).data("channel"),
                        clipping: clippingValue,
                        color: defaultColor.replace("#", "ff"),
                    });
                });
            }
        }
    }

    destroySliders() {
        for (var i = 0; i < this.sliders.length; i++) {
            var slider = document.getElementById(`${this.sliders[i]}_slider`);
            this.slidersValues[i] = slider.noUiSlider.get();
            slider.noUiSlider.destroy();
        }

        this.sliders = [];
    }
}

export function isBrightfield(pmaViewportInfo) {
    return (
        pmaViewportInfo.Channels.length === 1 &&
        pmaViewportInfo.Channels[0].Color === "ffffffff" &&
        (pmaViewportInfo.Channels[0].Name === "Default" || pmaViewportInfo.Channels[0].Name === "Brightfield" || pmaViewportInfo.Channels[0].Name === "")
    );
}