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) {
                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 === '');
}