/**
* @file projectUtils.js
* @author Christophe Avenel
* @see {@link projectUtils}
*/
/**
* @namespace projectUtils
* @version projectUtils 2.0
* @classdesc The root namespace for projectUtils.
*/
var projectUtils = {
_activeState:{},
_hideCSVImport: false,
_settings:[
{
"module":"dataUtils",
"function":"_autoLoadCSV",
"value":"boolean",
"desc":"Automatically load csv with default headers"
},
{
"module":"markerUtils",
"function":"_startMarkersOn",
"value":"boolean",
"desc":"Load with all markers visible"
},
{
"function": "_linkMarkersToChannels",
"module": "overlayUtils",
"value": "boolean",
"desc": "Link markers to channels in slider"
},
{
"function": "_hideCSVImport",
"module": "projectUtils",
"value": "boolean",
"desc": "Hide CSV file input on project load"
}
]
}
/**
* This method is used to save the TissUUmaps state (gene expression, cell morphology, regions) */
projectUtils.saveProject = function() {
projectUtils.getActiveProject().then((state) => {
interfaceUtils.prompt("Save project under the name:","NewProject")
.then((filename) => {
state.filename = filename;
var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(state, null, 4));
var dlAnchorElem=document.createElement("a");
dlAnchorElem.setAttribute("hidden","");
dlAnchorElem.setAttribute("href", dataStr );
dlAnchorElem.setAttribute("download", filename + ".tmap");
document.body.appendChild(dlAnchorElem);
dlAnchorElem.click();
document.body.removeChild(dlAnchorElem);
})
})
}
projectUtils.getActiveProject = function () {
return new Promise((resolve, reject) => {
var state = projectUtils._activeState;
var tabsNotSaved = [];
for (const uid in dataUtils.data) {
if (dataUtils.data[uid]["fromButton"] === undefined) {
tabsNotSaved.push(uid);
}
}
function makeButtons (callback) {
uid = tabsNotSaved.pop();
console.log("uid:", uid, tabsNotSaved)
if (uid === undefined) {
return callback();
}
tabName = document.getElementById(uid + "_tab-name").value;
projectUtils.makeButtonFromTab(uid, "The tab "+tabName+" is not saved as a button yet","modalButton_" + uid)
.then(() => makeButtons(callback));
}
function callback () {
state.regions = regionUtils._regions;
state.layers = tmapp.layers;
state.filters = filterUtils._filtersUsed;
state.layerFilters = filterUtils._filterItems;
state.compositeMode = filterUtils._compositeMode;
state.layerOpacities = {}
state.layerVisibilities = {}
tmapp.layers.forEach(function(layer, i) {
state.layerOpacities[i] = $("#opacity-layer-"+i).val();
state.layerVisibilities[i] = $("#visible-layer-"+i).is(":checked");
});
setTimeout(function() {
resolve(state);
},300);
}
makeButtons(callback)
})
}
/**
* This method is used to load the TissUUmaps state (gene expression, cell morphology, regions) */
projectUtils.makeButtonFromTab = function(dataset, title, modalUID) {
return new Promise((resolve, reject) => {
csvFile = document.getElementById(dataset + "_csv").value.replace(/^.*[\\\/]/, '');
if (!csvFile) {
if (dataUtils.data[dataset]) {
csvFile = dataUtils.data[dataset]["_csv_path"];
if (!(typeof csvFile === 'string' || csvFile instanceof String)) {
csvFile = csvFile.name;
}
}
else {
interfaceUtils.alert("Select a csv file first!");
resolve();
}
}
if (modalUID === undefined) modalUID = "default";
button1=HTMLElementUtils.createButton({"id":generated+"_marker-tab-button","extraAttributes":{ "class":"btn btn-secondary mx-2", "data-bs-dismiss":"modal"}})
button1.innerText = "Cancel";
button2=HTMLElementUtils.createButton({"id":generated+"_marker-tab-button","extraAttributes":{ "class":"btn btn-primary mx-2"}})
button2.innerText = "Generate button";
buttons=divpane=HTMLElementUtils.createElement({"kind":"div"});
buttons.appendChild(button1);
buttons.appendChild(button2);
button1.addEventListener("click",function(event) {
$(`#${modalUID}_modal`).modal('hide');
resolve();
})
button2.addEventListener("click",function(event) {
function UrlExists(url)
{
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const path = urlParams.get('path')
if (path != null) {
url = path + "/" + url
}
var http = new XMLHttpRequest();
http.open('HEAD', url, false);
http.send();
return http.status!=404;
}
path = document.getElementById("generateButtonPath_" + modalUID).value
if (path.includes("[")) {path = JSON.parse(path)}
if( Object.prototype.toString.call( path ) === '[object Array]' ) {
_exists = path.every(UrlExists);
}
else {
_exists = UrlExists(path);
}
var title = document.getElementById("generateButtonTitle_" + modalUID).value
var comment = document.getElementById("generateButtonComment_" + modalUID).value
if (!_exists) {
interfaceUtils.confirm("Warning, path doesn't seem reachable from the server. Check that all files are in the same folder.<br/><br/>Are you sure you want to continue?")
.then(function(_confirm) {
if (_confirm) {
projectUtils.makeButtonFromTabAux(dataset, path, title, comment);
$(`#${modalUID}_modal`).modal('hide');
resolve();
}
else {
}
})
}
else {
projectUtils.makeButtonFromTabAux(dataset, path, title, comment);
$(`#${modalUID}_modal`).modal('hide');
resolve();
}
})
content=HTMLElementUtils.createElement({"kind":"div"});
row0=HTMLElementUtils.createElement({"kind":"p", "extraAttributes":{"class":"text-danger"}});
row0.innerText = "Warning, the csv file must be in the same folder as the saved project or as the images."
row1=HTMLElementUtils.createRow({});
col11=HTMLElementUtils.createColumn({"width":12});
label111=HTMLElementUtils.createElement({"kind":"label", "extraAttributes":{ "for":"generateButtonPath_" + modalUID }});
label111.innerText="Relative path to the csv file (on the server side)"
file112=HTMLElementUtils.createElement({"kind":"input", "id":"generateButtonPath_" + modalUID, "extraAttributes":{ "class":"form-text-input form-control", "type":"text", "value":csvFile}});
row2=HTMLElementUtils.createRow({});
col21=HTMLElementUtils.createColumn({"width":12});
label211=HTMLElementUtils.createElement({"kind":"label","extraAttributes":{"for":"generateButtonTitle_" + modalUID }});
label211.innerText="Button inner text";
select212=HTMLElementUtils.createElement({"kind":"input", "id":"generateButtonTitle_" + modalUID, "extraAttributes":{ "class":"form-text-input form-control", "type":"text", "value":"Download data"} });
row3=HTMLElementUtils.createRow({});
col31=HTMLElementUtils.createColumn({"width":12});
label311=HTMLElementUtils.createElement({"kind":"label","extraAttributes":{"for":"generateButtonComment_" + modalUID }});
label311.innerText="Comment (will be displayed on the right of the button)";
select312=HTMLElementUtils.createElement({"kind":"input", "id":"generateButtonComment_" + modalUID, "extraAttributes":{ "class":"form-text-input form-control", "type":"text", "value":""} });
content.appendChild(row0);
content.appendChild(row1);
row1.appendChild(col11);
col11.appendChild(label111);
col11.appendChild(file112);
content.appendChild(row2);
row2.appendChild(col21);
col21.appendChild(label211);
col21.appendChild(select212);
content.appendChild(row3);
row3.appendChild(col31);
col31.appendChild(label311);
col31.appendChild(select312);
if (! title) title = "Generate button from tab"
interfaceUtils.generateModal(title, content, buttons, modalUID);
})
}
projectUtils.updateMarkerButton = function(dataset) {
var data_obj = dataUtils.data[dataset];
var markerFile = projectUtils._activeState.markerFiles[data_obj["fromButton"]];
var headers = interfaceUtils._mGenUIFuncs.getTabDropDowns(dataset);
markerFile.expectedHeader = Object.assign({}, ...Object.keys(headers).map((k) => ({[k]: headers[k].value})));
var radios = interfaceUtils._mGenUIFuncs.getTabRadiosAndChecks(dataset);
markerFile.expectedRadios = Object.assign({}, ...Object.keys(radios).map((k) => ({[k]: radios[k].checked})));
}
projectUtils.removeTabFromProject = function (dataset) {
if (dataUtils.data[dataset].fromButton !== undefined) {
let stateMarkerFile = projectUtils._activeState.markerFiles[dataUtils.data[dataset].fromButton];
if (stateMarkerFile.autoLoad) {
projectUtils._activeState.markerFiles.splice(dataUtils.data[dataset].fromButton,1);
// Reduce fromButton value for all datasets and buttons with larger fromButton value:
for (data_obj_uid in dataUtils.data) {
let data_obj = dataUtils.data[data_obj_uid];
if (data_obj.fromButton){
if (data_obj.fromButton > dataUtils.data[dataset].fromButton){
data_obj.fromButton -= 1;
}
}
}
for (markerFile of projectUtils._activeState.markerFiles) {
if (markerFile.fromButton){
if (markerFile.fromButton > dataUtils.data[dataset].fromButton){
markerFile.fromButton -= 1;
}
}
}
}
}
}
projectUtils.makeButtonFromTabAux = function (dataset, csvFile, title, comment, autoLoad) {
if (!csvFile)
return;
if (autoLoad === undefined)
autoLoad = false;
if (!autoLoad && projectUtils._activeState.markerFiles) {
// We check if a markerFile exists with autoload, to remove it:
projectUtils.removeTabFromProject(dataset);
}
markerFile = {
"path": csvFile,
"comment":comment,
"title":title,
"hideSettings":true,
"autoLoad":autoLoad,
"uid":dataset
};
tabName = document.getElementById(dataset + "_tab-name").value;
markerFile.name = tabName;
headers = interfaceUtils._mGenUIFuncs.getTabDropDowns(dataset);
markerFile.expectedHeader = Object.assign({}, ...Object.keys(headers).map((k) => ({[k]: headers[k].value})));
radios = interfaceUtils._mGenUIFuncs.getTabRadiosAndChecks(dataset);
markerFile.expectedRadios = Object.assign({}, ...Object.keys(radios).map((k) => ({[k]: radios[k].checked})));
if (!projectUtils._activeState.markerFiles) {
projectUtils._activeState.markerFiles = [];
}
projectUtils._activeState.markerFiles.push(markerFile);
markerFile.fromButton = projectUtils._activeState.markerFiles.length - 1;
dataUtils.data[dataset].fromButton = projectUtils._activeState.markerFiles.length - 1;
if (!autoLoad) {
if( Object.prototype.toString.call( markerFile.path ) === '[object Array]' ) {
interfaceUtils.createDownloadDropdownMarkers(markerFile);
}
else {
interfaceUtils.createDownloadButtonMarkers(markerFile);
}
}
}
projectUtils.loadProjectFile = function() {
var input = document.createElement('input');
input.type = 'file';
input.onchange = e => {
// getting a hold of the file reference
var file = e.target.files[0];
// setting up the reader
var reader = new FileReader();
reader.readAsText(file,'UTF-8');
// here we tell the reader what to do when it's done reading...
reader.onload = readerEvent => {
var content = readerEvent.target.result; // this is the content!
projectUtils.loadProject(JSON.parse(content));
}
}
input.click();
}
projectUtils.loadProjectFileFromServer = function(path) {
$.getJSON(path, function(json) {
projectUtils.loadProject(json);
})
.fail(function(jqXHR, textStatus, errorThrown) { interfaceUtils.alert("error: " + textStatus); })
}
/**
* This method is used to load the TissUUmaps state (gene expression, cell morphology, regions) */
projectUtils.loadProject = function(state) {
/*
{
markerFiles: [
{
path: "my/server/path.csv",
title: "",
comment: ""
}
],
CPFiles: [],
regionFiles: [],
layers: [
{
name:"",
path:""
}
],
filters: [
{
name:"",
default:"",
}
],
compositeMode: ""
}
*/
document.getElementById("divMarkersDownloadButtons").innerHTML = "";
if (state.backgroundColor) {
$(".openseadragon-canvas")[0].style.backgroundColor=state.backgroundColor;
}
if (state.plugins) {
state.plugins.forEach(function(pluginName) {
pluginUtils.addPlugin(pluginName);
});
}
if (state.regions && Object.keys(state.regions).length > 0) {
regionUtils.JSONValToRegions(state.regions);
}
if (state.regionFile) {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const path = urlParams.get('path')
if (path != null) {
regionUtils.JSONToRegions(path + "/" + state.regionFile);
}
}
projectUtils._activeState = state;
tmapp.fixed_file = "";
if (state.compositeMode) {
filterUtils._compositeMode = state.compositeMode;
}
if (state.markerFiles) {
state.markerFiles.forEach(function(markerFile, buttonIndex) {
markerFile["fromButton"] = buttonIndex;
// For compatibility reasons:
if (markerFile.expectedCSV) {
projectUtils.convertOldMarkerFile(markerFile);
state.hideTabs = true;
}
if( Object.prototype.toString.call( markerFile.path ) === '[object Array]' || markerFile.dropdownOptions) {
interfaceUtils.createDownloadDropdownMarkers(markerFile);
}
else {
interfaceUtils.createDownloadButtonMarkers(markerFile);
}
});
}
if (state.regionFiles) {
state.regionFiles.forEach(function(regionFile) {
if( Object.prototype.toString.call( regionFile.path ) === '[object Array]' ) {
interfaceUtils.createDownloadDropdownRegions(regionFile);
}
else {
interfaceUtils.createDownloadButtonRegions(regionFile);
}
});
}
if (state.filename) {
tmapp.slideFilename = state.filename;
document.getElementById("project_title").innerHTML = state.filename;
}
if (state.link) {
document.getElementById("project_title").href = state.link;
document.getElementById("project_title").target = "_blank";
}
if (state.settings) {
projectUtils.applySettings(state.settings);
}
if (state.hideTabs) {
document.getElementById("level-1-tabs").classList.add("d-none");
}
if (state.menuButtons) {
state.menuButtons.forEach(function(menuButton, i) {
if ( Object.prototype.toString.call( menuButton.text ) !== '[object Array]' ) {
menuButton.text = [menuButton.text]
}
interfaceUtils.addMenuItem(menuButton.text, function(){ window.open(menuButton.url, '_self').focus();});
});
}
if (state.mpp !== undefined) {
// If ppm == 0, we display pixel size
// If ppm != 0, we display scale bar with metric length
var PIXEL_LENGTH = function(ppm, minSize) {
return OpenSeadragon.ScalebarSizeAndTextRenderer.METRIC_GENERIC(ppm, minSize, "pixels")
}
var op = tmapp["object_prefix"];
var vname = op + "_viewer";
tmapp[vname].scalebar({
pixelsPerMeter: state.mpp ? (1e6 / state.mpp) : 1,
xOffset: 200,
yOffset: 10,
zIndex: 12,
barThickness: 3,
color: '#555555',
fontColor: '#333333',
backgroundColor: 'rgba(255, 255, 255, 0.5)',
sizeAndTextRenderer: state.mpp ? OpenSeadragon.ScalebarSizeAndTextRenderer.METRIC_LENGTH : PIXEL_LENGTH,
location: OpenSeadragon.ScalebarLocation.BOTTOM_RIGHT
});
}
// for backward compatibility only:
if (state.compositeMode == "collection") {
state.compositeMode = "source-over";
state.collectionMode = true;
}
projectUtils.loadLayers(state);
//tmapp[tmapp["object_prefix"] + "_viewer"].world.resetItems()
}
/**
* This method is used to load the TissUUmaps layers from state */
projectUtils.loadLayers = function(state) {
tmapp.layers = state.layers;
if (state.filters) {
filterUtils._filtersUsed = state.filters;
$(".filterSelection").prop("checked",false);
state.filters.forEach(function(filterused, i) {
$("#filterCheck_" + filterused).prop("checked",true);
});
}
if (state.layerFilters) {
filterUtils._filterItems = state.layerFilters;
}
tmapp[tmapp["object_prefix"] + "_viewer"].world.removeAll();
overlayUtils.addAllLayers();
if (state.compositeMode) {
filterUtils._compositeMode = state.compositeMode;
filterUtils.setCompositeOperation();
}
/*if (projectUtils._hideCSVImport) {
document.getElementById("ISS_data_panel").style.display="none";
}*/
setTimeout(function(){
if (state.rotate) {
var op = tmapp["object_prefix"];
var vname = op + "_viewer";
tmapp[vname].viewport.setRotation(state.rotate);
}
if (state.boundingBox) {
setTimeout(function() {
tmapp[tmapp["object_prefix"] + "_viewer"].viewport.fitBounds(new OpenSeadragon.Rect(state.boundingBox.x, state.boundingBox.y, state.boundingBox.width, state.boundingBox.height), false);
},1000);
}
if (state.compositeMode) {
filterUtils._compositeMode = state.compositeMode;
filterUtils.setCompositeOperation();
}
if (state.layerOpacities && state.layerVisibilities) {
$(".visible-layers").prop("checked",true);$(".visible-layers").click();
tmapp.layers.forEach(function(layer, i) {
$("#opacity-layer-"+i).val(state.layerOpacities[i]);
if (state.layerVisibilities[i] != 0) {
$("#visible-layer-"+i).click();
}
});
}
},300);
}
projectUtils.convertOldMarkerFile = function(markerFile) {
if (!markerFile.expectedHeader)
markerFile.expectedHeader = {}
markerFile.expectedHeader.X = markerFile.expectedCSV.X_col;
markerFile.expectedHeader.Y = markerFile.expectedCSV.Y_col;
if (markerFile.expectedCSV.key == "letters") {
markerFile.expectedHeader.gb_col = markerFile.expectedCSV.group;
markerFile.expectedHeader.gb_name = markerFile.expectedCSV.name;
}
else {
markerFile.expectedHeader.gb_col = markerFile.expectedCSV.name;
markerFile.expectedHeader.gb_name = markerFile.expectedCSV.group;
}
if (!markerFile.expectedRadios)
markerFile.expectedRadios = {}
if (markerFile.expectedCSV.piechart) {
markerFile.expectedRadios.pie_check = true;
markerFile.expectedHeader.pie_col = markerFile.expectedCSV.piechart
} else {markerFile.expectedRadios.pie_check = false;}
if (markerFile.expectedCSV.color) {
markerFile.expectedRadios.cb_gr = false;
markerFile.expectedRadios.cb_col = true;
markerFile.expectedHeader.cb_col = markerFile.expectedCSV.color
} else {markerFile.expectedRadios.cb_col = false;}
if (markerFile.expectedCSV.scale) {
markerFile.expectedRadios.scale_check = true;
markerFile.expectedHeader.scale_col = markerFile.expectedCSV.scale
} else {markerFile.expectedRadios.scale_check = false;}
if (!markerFile.uid)
markerFile.uid = "uniquetab";
markerFile.name = markerFile.title.replace("Download","");
if (markerFile.settings) {
markerFile.expectedRadios.cb_gr = true;
markerFile.expectedRadios.cb_gr_dict = false;
markerFile.expectedRadios.cb_gr_rand = false;
markerFile.expectedRadios.cb_gr_key = true;
for (setting of markerFile.settings) {
//if (setting.module == "glUtils" && setting.function == "_globalMarkerScale")
// markerFile.expectedHeader.scale_factor = setting.value;
if (setting.module == "markerUtils" && setting.function == "_selectedShape"){
dictSymbol = {6:6}
if (dictSymbol[setting.value]) setting.value = dictSymbol[setting.value];
markerFile.expectedHeader.shape_fixed = markerUtils._symbolStrings[setting.value];
}
if (setting.module == "markerUtils" && setting.function == "_randomShape") {
markerFile.expectedRadios.shape_fixed = !setting.value;
if (!markerFile.expectedHeader.shape_fixed) {
markerFile.expectedHeader.shape_fixed = markerUtils._symbolStrings[2];
}
}
if (setting.module == "markerUtils" && setting.function == "_colorsperkey") {
markerFile.expectedRadios.cb_gr = true;
markerFile.expectedRadios.cb_gr_rand = false;
markerFile.expectedRadios.cb_gr_key = false;
markerFile.expectedRadios.cb_gr_dict = true;
markerFile.expectedHeader.cb_gr_dict = JSON.stringify(setting.value);
}
if (setting.module == "HTMLElementUtils" && setting.function == "_colorsperiter") {
markerFile.expectedRadios.cb_gr = true;
markerFile.expectedRadios.cb_gr_rand = false;
markerFile.expectedRadios.cb_gr_key = false;
markerFile.expectedRadios.cb_gr_dict = true;
markerFile.expectedHeader.cb_gr_dict = JSON.stringify(setting.value);
}
if (setting.module == "glUtils" && setting.function == "_markerOpacity") {
markerFile.expectedHeader.opacity = setting.value;
setting.function = "_markerOpacityOld"
}
if (setting.module == "HTMLElementUtils" && setting.function == "_colorsperbarcode") {
markerFile.expectedRadios.cb_gr = true;
markerFile.expectedRadios.cb_gr_rand = false;
markerFile.expectedRadios.cb_gr_key = false;
markerFile.expectedRadios.cb_gr_dict = true;
markerFile.expectedHeader.cb_gr_dict = JSON.stringify(setting.value);
}
}
}
delete markerFile.expectedCSV;
markerFile["hideSettings"] = true;
}
/**
* @summary Given an array of layers, return the longest common path
* @param {!Array<!layers>} strs
* @returns {string}
*/
projectUtils.commonPath = function(strs) {
let prefix = ""
if(strs === null || strs.length === 0) return prefix
for (let i=0; i < strs[0].tileSource.length; i++){
const char = strs[0].tileSource[i] // loop through all characters of the very first string.
for (let j = 1; j < strs.length; j++){
// loop through all other strings in the array
if(strs[j].tileSource[i] !== char) {
prefix = prefix.substring(0, prefix.lastIndexOf('/')+1);
return prefix
}
}
prefix = prefix + char
}
prefix = prefix.substring(0, prefix.lastIndexOf('/')+1);
return prefix
}
/** Applying settings */
projectUtils.applySettings = function (settings) {
if (settings) {
settings.forEach(function(setting, i) {
if (window[setting.module]) {
if (typeof window[setting.module][setting.function] === 'function') {
window[setting.module][setting.function].apply(this, setting.value);
}
else {
window[setting.module][setting.function] = setting.value;
}
}
});
}
}
/** Adding marker legend in the upper left corner */
projectUtils.addLegend = function (htmlContent) {
if (! htmlContent) {
if (document.getElementById("markerLegend")) {
document.getElementById("markerLegend").style.display= "none";
}
return;
}
var op = tmapp["object_prefix"];
if (document.getElementById("markerLegend") == undefined) {
var elt = document.createElement('div');
elt.className = "px-1 mx-1 viewer-layer"
elt.id = "markerLegend"
elt.style.zIndex = "13";
elt.style.left = "10px";
elt.style.top = "10px";
elt.style.padding = "5px";
elt.style.overflowY = "auto";
elt.style.maxHeight = "Calc(100vh - 245px)";
tmapp[tmapp["object_prefix"] + "_viewer"].addControl(elt,{anchor: OpenSeadragon.ControlAnchor.TOP_LEFT});
}
elt = document.getElementById("markerLegend");
elt.style.display="block";
elt.innerHTML = htmlContent;
}