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\"><</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\">></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\"><</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\">></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 === "")
);
}