import { checkBrowserCompatibility } from '../../view/helpers';
import { Events } from './components';
import { Resources } from '../../resources/resources';
import $ from 'jquery';
const singleFileFormatExts = ["tif", "tiff", "jpeg", "jpg", "jp2",
"png", "ndpi", "dcm", "svs", "scn", "svslide", "lif", "zvi", "czi",
"lsm", "nd2", "mds", "zif", "sws", "qptiff", "tmap", "jxr", "kfb",
"isyntax", "rts", "bif", "oir", "szi", "mdsx"];
const multiFileFormatExts = ["ndpis", "mrxs", "afi", "vsi"];
function beforeLast(value, delimiter) {
value = value || ''
if (delimiter === '') {
return value
}
const substrings = value.split(delimiter)
return substrings.length === 1
? value // delimiter is not part of the string
: substrings.slice(0, -1).join(delimiter)
}
function removePathInitialSlash(path) {
path = path || '';
if (!path.startsWith("/")) {
return path;
}
return path.slice(1);
}
function getRandomId() {
return Math.random().toString(36).slice(2).toUpperCase();
}
function parseAfiNode(node) {
const childNodes = node.childNodes;
if (childNodes.length === 0) {
return node.nodeValue;
} else if (childNodes.length === 1 && childNodes[0].nodeType === Node.TEXT_NODE) {
return childNodes[0].nodeValue;
} else {
const obj = {};
childNodes.forEach(childNode => {
const childName = childNode.nodeName;
const childValue = obj[childName];
if (childValue !== undefined) {
if (Array.isArray(childValue)) {
childValue.push(parseAfiNode(childNode));
} else {
obj[childName] = [childValue, parseAfiNode(childNode)];
}
} else {
obj[childName] = parseAfiNode(childNode);
}
});
return obj;
}
}
function parseAfi(str) {
str = str.replace(/ {4}|[\t\n\r]/gm, '');
str = str.replace(/>\s*/g, '>');
str = str.replace(/\s*</g, '<');
const dom = (new DOMParser()).parseFromString(str, 'text/xml')
const result = { [dom.nodeName]: parseAfiNode(dom) };
return result;
}
function readFileAsText(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', (event) => {
const result = event.target.result;
resolve(result);
});
reader.onerror = function (e) {
reject(e);
};
reader.readAsText(file, "UTF-8");
});
}
function parseIni(iniString) {
let regex = {
section: /^\s*\[\s*([^\]]*)\s*\]\s*$/,
param: /^\s*([^=]+?)\s*=\s*(.*?)\s*$/,
comment: /^\s*;.*$/
};
let value = {};
let lines = iniString.split(/[\r\n]+/);
let section = null;
lines.forEach(function (line) {
if (regex.comment.test(line)) {
return;
} else if (regex.param.test(line)) {
let match = line.match(regex.param);
if (section) {
value[section][match[1]] = match[2];
} else {
value[match[1]] = match[2];
}
} else if (regex.section.test(line)) {
let match = line.match(regex.section);
value[match[1]] = {};
section = match[1];
} else if (line.length == 0 && section) {
section = null;
}
});
return value;
}
// Validate MRXS file
async function mrxsChecker(file, files, existing = false) {
let includedFiles = [file];
let missingFiles = [];
files = files.filter(x => x);
if (files.length === 0) {
return { status: ImageUpload.SlideStatus.MissingFiles, errors: ["No additional files provided"], files: includedFiles };
}
// Find ini file
const directoryToCheck = existing ?
beforeLast(files[0].path ? removePathInitialSlash(files[0].path || files[0].name) : (files[0].webkitRelativePath || files[0].name), '.') :
beforeLast(file.path ? removePathInitialSlash(file.path || file.name) : (file.webkitRelativePath || file.name), '.');
const iniFile = files.find(x => removePathInitialSlash(x.path) === directoryToCheck + "/Slidedat.ini" || x.webkitRelativePath === directoryToCheck + "/Slidedat.ini");
if (!iniFile) {
return { status: ImageUpload.SlideStatus.MissingFiles, errors: [`Missing folder: ${file.name.split('.').shift()}`], files: includedFiles };
}
//Read ini file
let iniData = await readFileAsText(iniFile);
if (!iniData) {
return { status: ImageUpload.SlideStatus.MissingFiles, errors: ["Slidedat.ini couldn't be opened"], files: includedFiles };
}
//Parse ini file
let ini = parseIni(iniData);
if (!ini) {
return { status: ImageUpload.SlideStatus.MissingFiles, errors: ["Slidedat.ini couldn't be parsed"], files: includedFiles };
}
includedFiles.push(iniFile);
//Check if index file included
const indexDatName = ini.HIERARCHICAL.INDEXFILE;
const indexDatFile = files.find(x => removePathInitialSlash(x.path) === directoryToCheck + '/' + indexDatName || x.webkitRelativePath === directoryToCheck + '/' + indexDatName);
if (!indexDatFile) {
missingFiles.push(indexDatName + " missing");
} else {
includedFiles.push(indexDatFile);
}
//Check if all other files included
const noOfDats = parseInt(ini.DATAFILE.FILE_COUNT);
for (let i = 0; i < noOfDats; i++) {
const datName = ini.DATAFILE["FILE_" + i];
const datFile = files.find(x => removePathInitialSlash(x.path) === directoryToCheck + '/' + datName || x.webkitRelativePath === directoryToCheck + '/' + datName);
if (!datFile) {
missingFiles.push(datName + " missing");
} else {
includedFiles.push(datFile);
}
}
return { status: missingFiles.length === 0 ? ImageUpload.SlideStatus.Queued : ImageUpload.SlideStatus.MissingFiles, errors: missingFiles, files: includedFiles, childFolder: directoryToCheck.split('/').pop() };
}
// Validate VSI file
async function vsiChecker(file, files, existing = false) {
let includedFiles = [file];
let childFolders = [""];
files = files.filter(x => x);
if (files.length === 0) {
return { status: ImageUpload.SlideStatus.MissingFiles, errors: ["No additional files provided"], files: includedFiles };
}
// const directoryToCheck = existing ?
// beforeLast(files[0].path ? removePathInitialSlash(files[0].path) : files[0].webkitRelativePath, '/') :
// beforeLast(file.path ? removePathInitialSlash(file.path) : file.webkitRelativePath, '/');
const fileName = beforeLast(existing ? files[0].name : file.name, '.');
const etsFiles = files.filter(x => {
const testPath = x.path ? x.path : x.webkitRelativePath;
if (!testPath.includes(".ets")) {
return false;
}
return testPath.includes(fileName);
});
if (etsFiles.length === 0) {
return { status: ImageUpload.SlideStatus.MissingFiles, errors: [`Missing folder: ${file.name.split('.').shift()}`], files: includedFiles };
}
for (let i = 0; i < etsFiles.length; i++) {
childFolders.push(beforeLast(etsFiles[i].path ? removePathInitialSlash(etsFiles[i].path) : etsFiles[i].webkitRelativePath, '/'))
}
return { status: ImageUpload.SlideStatus.Queued, errors: [], files: includedFiles.concat(etsFiles), childFolder: childFolders };
}
// Validate AFI file
async function afiChecker(file, files, existing = false) {
let includedFiles = [file];
let missingFiles = [];
files = files.filter(x => x);
//Read afi file
let afiData = await readFileAsText(file);
if (!afiData) {
return { status: ImageUpload.SlideStatus.MissingFiles, errors: [file.name + " couldn't be opened"], files: includedFiles };
}
//Parse afi file
let afi = parseAfi(afiData);
if (!afi) {
return { status: ImageUpload.SlideStatus.MissingFiles, errors: [file.name + " couldn't be parsed"], files: includedFiles };
}
//Check if all other files included
const svses = (afi["#document"] && afi["#document"].ImageList && afi["#document"].ImageList.Image) ? afi["#document"].ImageList.Image : null;
const noOfSvs = (svses && svses.length) ? svses.length : 0;
if (files.length === 0) {
missingFiles = svses.map(({ Path }) => Path);
return { status: ImageUpload.SlideStatus.MissingFiles, errors: missingFiles, files: includedFiles };
}
const directoryToCheck = existing ?
beforeLast(files[0].path ? removePathInitialSlash(files[0].path) : files[0].webkitRelativePath, '/') :
beforeLast(file.path ? removePathInitialSlash(file.path) : file.webkitRelativePath, '/');
for (let i = 0; i < noOfSvs; i++) {
const svsName = svses[i].Path;
const svsFile = files.find(x => removePathInitialSlash(x.path) === directoryToCheck + '/' + svsName || x.webkitRelativePath === directoryToCheck + '/' + svsName || x.name.toLowerCase() === svsName.toLowerCase());
if (!svsFile) {
missingFiles.push(svsName + " missing");
} else {
includedFiles.push(svsFile);
}
}
return { status: missingFiles.length === 0 ? ImageUpload.SlideStatus.Queued : ImageUpload.SlideStatus.MissingFiles, errors: missingFiles, files: includedFiles, childFolder: "" };
}
// Validate NDPIS file
async function ndpisChecker(file, files, existing = false) {
let includedFiles = [file];
let missingFiles = [];
files = files.filter(x => x);
//Read ndpis file
let ndpisData = await readFileAsText(file);
if (!ndpisData) {
return { status: ImageUpload.SlideStatus.MissingFiles, errors: [file.name + " couldn't be opened"], files: includedFiles };
}
//Parse ndpis file
let ndpis = parseIni(ndpisData);
if (!ndpis) {
return { status: ImageUpload.SlideStatus.MissingFiles, errors: [file.name + " couldn't be parsed"], files: includedFiles };
}
//Check if all other files included
const noOfNdpisFiles = ndpis["NanoZoomer Digital Pathology Image Set"].NoImages;
if (files.length === 0) {
for (let i = 0; i < noOfNdpisFiles; i++) {
missingFiles.push(ndpis["NanoZoomer Digital Pathology Image Set"]["Image" + i]);
}
return { status: ImageUpload.SlideStatus.MissingFiles, errors: missingFiles, files: includedFiles };
}
for (let i = 0; i < noOfNdpisFiles; i++) {
const ndpiName = ndpis["NanoZoomer Digital Pathology Image Set"]["Image" + i];
const ndpiFile = files.find(({ name }) => name === ndpiName);
if (!ndpiFile) {
missingFiles.push(ndpiName + " missing");
} else {
includedFiles.push(ndpiFile);
}
}
return { status: missingFiles.length === 0 ? ImageUpload.SlideStatus.Queued : ImageUpload.SlideStatus.MissingFiles, errors: missingFiles, files: includedFiles, childFolder: "" };
}
async function multiFileSlideChecker(ext, file, files, existing = false) {
switch (ext.toLowerCase()) {
case "mrxs":
return await mrxsChecker(file, files, existing);
case "vsi":
return await vsiChecker(file, files, existing);
case "afi":
return await afiChecker(file, files, existing);
case "ndpis":
return await ndpisChecker(file, files, existing);
default:
return { status: ImageUpload.SlideStatus.NotSupported, errors: ["Not supported format"], files: [] };
}
}
function beforeUnloadListener(e) {
e.preventDefault();
let confirmationMessage = Resources.translate("Uploading is in progess. If you leave now, your uploads will be lost.");
(e || window.event).returnValue = confirmationMessage;
return confirmationMessage;
}
async function startUploading() {
let that = this;
if (this.lastLoadedImages.length === 0) {
return;
}
this.uploading = true;
if (this.notifyOnExit) {
window.addEventListener("beforeunload", beforeUnloadListener);
}
for (; ;) {
const slide = this.lastLoadedImages.find(x => x.status === ImageUpload.SlideStatus.Queued);
if (!slide || !this.uploadingStatus) {
break;
}
await this.context.uploadSlide({
serverUrl: this.serverUrl,
targetPath: slide.targetPath,
targetChildPath: slide.targetChildPath,
slide: slide,
progress: (s, p) => {
let slideIndex = that.lastLoadedImages.findIndex(x => x.id === s.id);
if (slideIndex === -1) {
return;
}
that.lastLoadedImages[slideIndex].uploadProgress = p;
that.lastLoadedImages[slideIndex].status = ImageUpload.SlideStatus.Uploading;
that.fireEvent(Events.OnProgress, {
type: "uploadProgress",
slide: that.lastLoadedImages[slideIndex],
slides: that.lastLoadedImages,
});
updateResults.call(that);
},
success: (s) => {
let slideIndex = that.lastLoadedImages.findIndex(x => x.id === s.id);
if (slideIndex === -1) {
return;
}
that.lastLoadedImages[slideIndex].uploadProgress = 100;
that.lastLoadedImages[slideIndex].status = ImageUpload.SlideStatus.Done;
that.fireEvent(Events.OnProgress, {
type: "uploadSuccess",
slide: that.lastLoadedImages[slideIndex],
slides: that.lastLoadedImages,
});
updateResults.call(that);
},
error: (slide, errorMessage) => {
let slideIndex = that.lastLoadedImages.findIndex(x => x.id === slide.id);
if (slideIndex === -1) {
return;
}
that.lastLoadedImages[slideIndex].uploadProgress = 100;
that.lastLoadedImages[slideIndex].status = ImageUpload.SlideStatus.Error;
that.lastLoadedImages[slideIndex].error = errorMessage;
that.fireEvent(Events.OnProgress, {
type: "uploadError",
slide: that.lastLoadedImages[slideIndex],
slides: that.lastLoadedImages,
});
updateResults.call(that);
}
});
updateResults.call(this);
}
this.uploading = false;
if (this.notifyOnExit) {
window.removeEventListener("beforeunload", beforeUnloadListener);
}
}
function getStatus(status, msg) {
const popover = '<span class="image-upload-tooltip" data-tip="' + msg + '" tabindex="1"><i style="cursor: pointer;" class="fa fa-question-circle-o" /></span>';
switch (status) {
case ImageUpload.SlideStatus.MissingFiles:
return '<span style="color: purple;">' + Resources.translate("Files missing") + ' ' + popover + '</span>';
case ImageUpload.SlideStatus.Queued:
return '<span style="color: #ffa700;">' + Resources.translate("Queued") + ' <i class="fa fa-clock-o"/></span>';
case ImageUpload.SlideStatus.Error:
return '<span style="color: red;">' + Resources.translate("Error") + ' ' + popover + '</span>';
case ImageUpload.SlideStatus.Done:
return '<span style="color: green;">' + Resources.translate("Uploaded") + ' <i class="fa fa-check-circle-o"/></span>';
case ImageUpload.SlideStatus.Uploading:
return '<span style="color: blue;">' + Resources.translate("Uploading") + ' <i class="fa fa-upload"/></span>';
case ImageUpload.SlideStatus.NotSupported:
return '<span>' + Resources.translate("Not supported") + ' ' + popover + '</span>';
}
}
function getProgress(status, progress) {
let html = '<div class="meter ';
switch (status) {
case ImageUpload.SlideStatus.MissingFiles:
case ImageUpload.SlideStatus.Error:
case ImageUpload.SlideStatus.NotSupported:
html += "danger";
progress = 100;
break;
case ImageUpload.SlideStatus.Queued:
html += "warning";
progress = 100;
break;
case ImageUpload.SlideStatus.Done:
html += "success";
progress = 100;
break;
case ImageUpload.SlideStatus.Uploading:
html += "info";
break;
}
html += '"><span style="width: ' + progress + '%"></span></div>';
return html;
}
function updateResults() {
let that = this;
if (!this.statusElement) {
return;
}
const resultsDiv = this.statusElement;
while (resultsDiv.firstChild) {
resultsDiv.removeChild(resultsDiv.firstChild);
}
if (this.lastLoadedImages.length === 0) {
return;
}
const resultsMain = document.createElement('div');
const resultsTable = document.createElement('table');
const resultsHeader = document.createElement('tr');
let resultsHeaderCell = document.createElement('th');
resultsHeaderCell.className = "text-center";
resultsHeaderCell.innerText = "#";
resultsHeader.appendChild(resultsHeaderCell);
resultsHeaderCell = document.createElement('th');
resultsHeaderCell.innerText = Resources.translate("Name");
resultsHeader.appendChild(resultsHeaderCell);
// resultsHeaderCell = document.createElement('th');
// resultsHeaderCell.innerText = Resources.translate("Relative Path");
// resultsHeader.appendChild(resultsHeaderCell);
resultsHeaderCell = document.createElement('th');
resultsHeaderCell.className = "text-right";
resultsHeaderCell.innerText = Resources.translate("Size (in bytes)");
resultsHeader.appendChild(resultsHeaderCell);
resultsHeaderCell = document.createElement('th');
resultsHeaderCell.innerText = Resources.translate("Target Path");
resultsHeader.appendChild(resultsHeaderCell);
resultsHeaderCell = document.createElement('th');
resultsHeaderCell.className = "text-center";
resultsHeaderCell.innerText = Resources.translate("Status");
resultsHeader.appendChild(resultsHeaderCell);
resultsHeaderCell = document.createElement('th');
resultsHeaderCell.className = "text-center";
resultsHeaderCell.innerText = Resources.translate("Progress");
resultsHeader.appendChild(resultsHeaderCell);
resultsHeaderCell = document.createElement('th');
resultsHeaderCell.className = "text-center";
resultsHeader.appendChild(resultsHeaderCell);
resultsTable.appendChild(resultsHeader);
this.lastLoadedImages.forEach((x, i) => {
let resultsTableRow = document.createElement('tr');
let resultsTableRowCell = document.createElement('th');
resultsTableRowCell.className = "text-center";
resultsTableRowCell.innerText = i + 1;
resultsTableRow.appendChild(resultsTableRowCell);
resultsTableRowCell = document.createElement('td');
resultsTableRowCell.innerText = x.name;
resultsTableRow.appendChild(resultsTableRowCell);
// resultsTableRowCell = document.createElement('td');
resultsTableRowCell = document.createElement('td');
resultsTableRowCell.className = "text-right";
resultsTableRowCell.innerText = x.size.toLocaleString("en");
resultsTableRow.appendChild(resultsTableRowCell);
resultsTableRowCell = document.createElement('td');
resultsTableRowCell.innerText = x.targetPath;
resultsTableRow.appendChild(resultsTableRowCell);
resultsTableRowCell = document.createElement('td');
resultsTableRowCell.className = "text-center";
resultsTableRowCell.innerHTML = getStatus(x.status, x.error);
resultsTableRow.appendChild(resultsTableRowCell);
resultsTableRowCell = document.createElement('td');
resultsTableRowCell.className = "text-center";
resultsTableRowCell.innerHTML = getProgress(x.status, x.uploadProgress);
resultsTableRow.appendChild(resultsTableRowCell);
resultsTableRowCell = document.createElement('td');
resultsTableRowCell.className = "text-center";
let removeBtn = document.createElement("button");
removeBtn.title = Resources.translate("Remove slide from list");
removeBtn.innerHTML = '<i class="fa fa-trash-o" aria-hidden="true"></i>';
removeBtn.onclick = function () {
that.removeSlide(i);
};
removeBtn.disabled = x.status === ImageUpload.SlideStatus.Uploading;
resultsTableRowCell.appendChild(removeBtn);
resultsTableRow.appendChild(resultsTableRowCell);
resultsTable.appendChild(resultsTableRow);
});
resultsMain.appendChild(resultsTable);
resultsDiv.appendChild(resultsMain);
}
/**
* Organizes a list of files into a tree structure.
* @param {File[]} fileList
* @return {ImageUpload~TreeNode[]}
* @ignore
*/
function buildTree(fileList) {
const tree = {};
for (let i = 0; i < fileList.length; i++) {
// for each file in the provided list
const file = fileList[i];
const pathParts = file.path ? removePathInitialSlash(file.path).split('/') : file.webkitRelativePath.split('/'); // Split the relative path into an array of parts
let currentNode = tree;
// for each sub-directory
for (let j = 0; j < pathParts.length; j++) {
const part = pathParts[j];
if (!currentNode.children) {
// Create an empty array for children if it doesn't exist
currentNode.children = [];
}
let childNode = currentNode.children.find(child => child.name === part);
if (!childNode) {
// Create a new child node if it doesn't exist
childNode = { name: part, children: [] };
currentNode.children.push(childNode);
}
if (j === pathParts.length - 1) {
// Set the leaf node's properties
childNode.file = file;
childNode.path = file.path ? removePathInitialSlash(file.path) : file.webkitRelativePath;
childNode.isLeaf = true;
delete childNode.children;
} else {
childNode.isLeaf = false;
}
currentNode = childNode; // Traverse to the next level
}
}
return tree.children[0];
}
function getAllFiles(treeNode) {
let files = [];
function traverse(node) {
if (node.isLeaf && node.path) {
files.push(node.file);
}
if (node.children && Array.isArray(node.children)) {
node.children.forEach(child => {
traverse(child);
});
}
}
traverse(treeNode);
return files;
}
async function checkSlidesForMissingFiles(treeNode, filesArray) {
if (!treeNode) {
return;
}
let mffSlideIndex = -1;
if (!treeNode.name) {
if (treeNode.isLeaf) {
let indexesToDelete = [];
for (let i = 0; i < filesArray.length; i++) {
const f = filesArray[i];
mffSlideIndex = this.lastLoadedImages.findIndex(x => x.error.toLowerCase().includes(f.name.toLowerCase()) && x.status === ImageUpload.SlideStatus.MissingFiles);
if (mffSlideIndex != -1) {
const mffSlide = this.lastLoadedImages[mffSlideIndex];
const ext = mffSlide.name.split('.').pop();
const checkResult = await multiFileSlideChecker(ext, mffSlide.files[0], [...mffSlide.files].concat(f), true);
this.lastLoadedImages[mffSlideIndex] = {
id: mffSlide.id,
name: mffSlide.name,
files: checkResult.files,
status: checkResult.status,
error: checkResult.errors.join(", "),
targetPath: mffSlide.targetPath,
relativePath: mffSlide.relativePath,
targetChildPath: checkResult.childFolder,
size: checkResult.files.map(f => (f && f.size) ? f.size : 0).reduce((accumulator, current) => accumulator + current, 0),
uploadProgress: 0,
};
indexesToDelete.push(filesArray.findIndex(({ name }) => name.toLowerCase() === f.name.toLowerCase()));
}
}
indexesToDelete.filter(x => x !== -1).forEach(x => {
filesArray[x] = undefined;
});
filesArray = filesArray.filter(x => x !== undefined);
}
} else {
const folderName = treeNode.name;
mffSlideIndex = this.lastLoadedImages.findIndex(x => folderName.toLowerCase().includes(x.name.toLowerCase().split('.').shift()) && x.status === ImageUpload.SlideStatus.MissingFiles);
if (mffSlideIndex != -1) {
const mffSlide = this.lastLoadedImages[mffSlideIndex];
const ext = mffSlide.name.split('.').pop();
const checkResult = await multiFileSlideChecker(ext, mffSlide.files[0], ([...mffSlide.files]).concat(getAllFiles(treeNode)), true);
this.lastLoadedImages[mffSlideIndex] = {
id: mffSlide.id,
name: mffSlide.name,
files: checkResult.files,
status: checkResult.status,
error: checkResult.errors.join(", "),
targetPath: mffSlide.targetPath,
relativePath: mffSlide.relativePath,
targetChildPath: checkResult.childFolder,
size: checkResult.files.map(f => (f && f.size) ? f.size : 0).reduce((accumulator, current) => accumulator + current, 0),
uploadProgress: 0,
};
}
}
}
async function checkSlidesAdded() {
if (this.lastLoadedImages.length === 0) {
return;
}
const brokenSlides = this.lastLoadedImages.filter(({ status }) => status === ImageUpload.SlideStatus.MissingFiles);
let indexesToDelete = [];
for (let i = 0; i < brokenSlides.length; i++) {
for (let j = 0; j < this.lastLoadedImages.length; j++) {
const s = this.lastLoadedImages[j];
const mffSlide = brokenSlides[i];
if (mffSlide.error.toLowerCase().includes(s.name.toLowerCase())) {
const ext = mffSlide.name.split('.').pop();
const mffSlideIndex = this.lastLoadedImages.findIndex(({ id }) => id === mffSlide.id);
if (mffSlideIndex !== -1) {
const checkResult = await multiFileSlideChecker(ext, mffSlide.files[0], [...mffSlide.files].concat(...s.files), true);
this.lastLoadedImages[mffSlideIndex] = {
id: mffSlide.id,
name: mffSlide.name,
files: checkResult.files,
status: checkResult.status,
error: checkResult.errors.join(", "),
targetPath: mffSlide.targetPath,
relativePath: mffSlide.relativePath,
targetChildPath: checkResult.childFolder,
size: checkResult.files.map(f => (f && f.size) ? f.size : 0).reduce((accumulator, current) => accumulator + current, 0),
uploadProgress: 0,
};
brokenSlides[i] = this.lastLoadedImages[mffSlideIndex];
indexesToDelete.push(j);
}
}
}
}
indexesToDelete.forEach(x => {
this.lastLoadedImages[x] = undefined;
});
this.lastLoadedImages = this.lastLoadedImages.filter(x => x !== undefined);
}
async function removeAuxilliarySlides() {
if (this.lastLoadedImages.length === 0) {
return;
}
const normalMFFSlides = this.lastLoadedImages.filter(x => x.status === ImageUpload.SlideStatus.Queued && x.files.length > 1);
let indexesToDelete = [];
for (let i = 0; i < normalMFFSlides.length; i++) {
const mffSlide = normalMFFSlides[i];
for (let j = 1; j < mffSlide.files.length; j++) {
const mffFile = mffSlide.files[j];
const sffSlideIndex = this.lastLoadedImages.findIndex(({ name }) => name === mffFile.name);
if (sffSlideIndex !== -1) {
indexesToDelete.push(sffSlideIndex);
}
}
}
indexesToDelete.forEach(x => {
this.lastLoadedImages[x] = undefined;
});
this.lastLoadedImages = this.lastLoadedImages.filter(x => x !== undefined);
}
async function checkFile(file, filesArray, targetPath) {
if (!file) {
return
}
const ext = file.name.split('.').pop();
if (file.type === "image/jpeg") {
return {
id: getRandomId(),
name: file.name,
files: [file],
status: ImageUpload.SlideStatus.Queued,
error: "",
targetPath: targetPath,
relativePath: file.path ? beforeLast(removePathInitialSlash(file.path), '/') + "/" : beforeLast(file.webkitRelativePath, '/') + "/",
size: file.size,
uploadProgress: 0,
};
} else if (singleFileFormatExts.includes(ext)) {
return {
id: getRandomId(),
name: file.name,
files: [file],
status: ImageUpload.SlideStatus.Queued,
error: "",
targetPath: targetPath,
relativePath: file.path ? beforeLast(removePathInitialSlash(file.path), '/') + "/" : beforeLast(file.webkitRelativePath, '/') + "/",
size: file.size,
uploadProgress: 0,
};
} else if (multiFileFormatExts.includes(ext)) {
const checkResult = await multiFileSlideChecker(ext, file, filesArray);
return {
id: getRandomId(),
name: file.name,
files: checkResult.files,
status: checkResult.status,
error: checkResult.errors.join(", "),
targetPath: targetPath,
relativePath: file.path ? beforeLast(removePathInitialSlash(file.path), '/') + "/" : beforeLast(file.webkitRelativePath, '/') + "/",
targetChildPath: checkResult.childFolder,
size: checkResult.files.map(f => (f && f.size) ? f.size : 0).reduce((accumulator, current) => accumulator + current, 0),
uploadProgress: 0,
};
}
}
/**
* Traverses the tree of file objects breadth first and finds slides, which are coupled with their auxiliary files if provided.
* @param {ImageUpload~TreeNode} root - The tree top node
* @param {string} targetPath - The path the slides must be uploaded to
* @returns {ImageUpload~Slide[] | undefined}
* @ignore
*/
async function findSlides(root, targetPath) {
// Check if the tree is empty
if (!root) {
console.Error('Tree is empty');
return;
}
// Initialize an array to store results and an object to keep track of nodes by level
const result = [];
const nodesByLevel = {};
// Define a recursive function to traverse the tree
async function traverse(parentNode, currentNode, currentLevel, targetPath) {
// Base case: if the node is null, return
if (!currentNode) {
return;
}
// Add the current node's data to the result array for the current level
if (!nodesByLevel[currentLevel]) {
nodesByLevel[currentLevel] = [];
result.push(nodesByLevel[currentLevel]);
}
if (!currentNode.isLeaf) {
if (currentNode.parsed) {
return;
}
// Recursively traverse the children of the current node
const children = currentNode.children.sort((a, b) => b.isLeaf ? 1 : -1);
for (let i = 0; i < children.length; i++) {
const childNode = currentNode.children[i];
await traverse(currentNode, childNode, currentLevel + 1, targetPath);
}
} else {
currentNode.parsed = true;
if (parentNode) {
const folderName = beforeLast(currentNode.name, '.');
const child = parentNode.children.find(c => c.name === folderName);
if (child) {
const files = child.children.map(x => x.file);
nodesByLevel[currentLevel].push(await checkFile.call(this, currentNode.file, files, targetPath));
child.parsed = true;
} else {
nodesByLevel[currentLevel].push(await checkFile.call(this, currentNode.file, [], targetPath));
}
} else {
nodesByLevel[currentLevel].push(await checkFile.call(this, currentNode.file, [], targetPath));
}
}
}
// Start the traversal with the root node at level 0
await traverse(null, root, 0, targetPath);
// Return the result array
return result;
}
export
/**
* Represents a UI component that allows the uploading of single- and multi-file format slides to PMA.core servers. Provides events to handle uploading progress, as well as ready to use upload and status components.
* @memberof PMA.UI.Components
* @alias ImageUpload
* @param {Context} context
* @param {object} options - Configuration options
* @param {string} options.serverUrl - A string indicating the PMA.core server to upload to.
* @param {string|HTMLElement} [options.uploadElement] - Optional selector or HTML element where the UI for browsing and dragging files & folders will be rendered. If not provided, no UI will be rendered.
* @param {string|HTMLElement} [options.statusElement] - Optional selector or HTML element where each image’s status will be displayed. This element will display a list of slides and also give the option to the user to remove slides.
* @param {boolean} [options.notifyOnExit] - A boolean indicating whether or not an alert should appear on leaving page while uploading.
* @fires PMA.UI.Components.Events.OnProgress
*/
class ImageUpload {
constructor(context, options) {
if (!checkBrowserCompatibility()) {
return;
}
if (!options.serverUrl) {
console.error("No server url defined in options");
}
if (options.uploadElement) {
if (options.uploadElement instanceof HTMLElement) {
this.uploadElement = options.uploadElement;
}
else if (typeof options.uploadElement == "string") {
let el = document.querySelector(options.uploadElement);
if (!el) {
console.error("Invalid selector for upload element");
}
else {
this.uploadElement = el;
}
}
else {
console.error("Invalid upload element");
return;
}
}
if (options.statusElement) {
if (options.statusElement instanceof HTMLElement) {
this.statusElement = options.statusElement;
}
else if (typeof options.statusElement == "string") {
let el = document.querySelector(options.statusElement);
if (!el) {
console.error("Invalid selector for status element");
}
else {
this.statusElement = el;
}
}
else {
console.error("Invalid status element");
return;
}
}
this.listeners = {};
this.listeners[Events.OnProgress] = [];
this.serverUrl = options.serverUrl;
this.notifyOnExit = options.notifyOnExit ? true : false;
this.context = context;
// a helper array that holds the last loaded images as objects { serverUrl, sessionId, imageArray }
this.lastLoadedImages = [];
this.uploadingStatus = false;
this.uploading = false;
$(this.uploadElement).addClass("pma-ui-image-upload");
$(this.statusElement).addClass("pma-ui-image-upload-status");
}
/**
* Attaches an event listener
* @param {PMA.UI.Components.Events} eventName - The name of the event to listen to
* @param {function} callback - The function to call when the event occurs
*/
listen(eventName, callback) {
// if (!this.listeners.hasOwnProperty(eventName)) {
if (!Object.prototype.hasOwnProperty.call(this.listeners, eventName)) {
console.error(eventName + " is not a valid event");
}
this.listeners[eventName].push(callback);
}
// fires an event
fireEvent(eventName, eventArgs) {
// if (!this.listeners.hasOwnProperty(eventName)) {
if (!Object.prototype.hasOwnProperty.call(this.listeners, eventName)) {
console.error(eventName + " does not exist");
return;
}
for (let i = 0, max = this.listeners[eventName].length; i < max; i++) {
this.listeners[eventName][i].call(this, eventArgs);
}
}
/**
*
* @param {File[]} files - The array of File objects to add to upload list
* @param {string} targetPath - The path to upload images to
* @fires PMA.UI.Components.Events.OnProgress
*/
async addFiles(files, targetPath) {
if (targetPath === null || targetPath === undefined || targetPath.length === 0) return;
const filesArray = Array.from(files);
if (filesArray.length === 0) {
return;
}
const tree = buildTree(filesArray);
let slidesToAdd = [];
if (this.lastLoadedImages.length > 0) {
await checkSlidesForMissingFiles.call(this, tree, filesArray);
}
// if (tree.children) {
// slidesToAdd = (await findSlides(tree, targetPath)).filter(x => x).flatMap(x => x);
// } else {
for (let i = 0; i < filesArray.length; i++) {
slidesToAdd.push(await checkFile.call(this, filesArray[i], filesArray, targetPath));
}
// }
this.lastLoadedImages = this.lastLoadedImages.concat(slidesToAdd.filter(x => x));
await checkSlidesAdded.call(this);
await removeAuxilliarySlides.call(this);
this.fireEvent(Events.OnProgress, {
type: "uploadAdded",
slide: null,
slides: this.lastLoadedImages,
});
updateResults.call(this);
if (this.uploadingStatus === true && this.uploading === false) {
startUploading.call(this);
}
}
/**
* Returns the list of processed slides, or null
* @return {ImageUpload~Slide}
*/
getSlides() {
return this.lastLoadedImages;
}
/**
* Removes a slide from the list
* @param {Number} index - The index of the slide to remove
* @fires PMA.UI.Components.Events.OnProgress
*/
removeSlide(index) {
if (index === null || index === undefined || index < 0 || index >= this.lastLoadedImages.length) {
return;
}
let slideToRemove = this.lastLoadedImages[index];
this.lastLoadedImages.splice(index, 1);
this.fireEvent(Events.OnProgress, {
type: "uploadRemoved",
slide: slideToRemove,
slides: this.lastLoadedImages,
});
updateResults.call(this);
}
/**
* Pauses or resumes uploading.
* @param {boolean} enable - Resumes uploading if true, else pauses uploading
* @fires PMA.UI.Components.Events.OnProgress
*/
setUploadingStatus(enable) {
this.uploadingStatus = enable;
this.fireEvent(Events.OnProgress, {
type: enable ? "uploadingEnabled" : "uploadingDisabled",
slide: null,
slides: this.lastLoadedImages,
});
if (enable && this.uploading === false) {
startUploading.call(this);
}
}
/**
* Returns the uploading status
* @return {boolean}
*/
getUploadingStatus() {
return this.uploadingStatus;
}
}
/**
*
* @typedef ImageUpload~Slide
* @property {string} id - Upload id
* @property {Array<File>} files - Files contained in slide
* @property {ImageUpload.SlideStatus} status - Current slide uploadsing status
* @property {string} error - Errors returned from slide checking
* @property {string} targetPath - Virtual path slide will be uploaded to
* @property {string} relativePath - Relative path of slide in local filesystem
* @property {number} size - Total size of slide
* @property {number} uploadProgress - Upload progress
*/
/**
* ImageUpload slide status
* @readonly
* @enum {number}
*/
ImageUpload.SlideStatus = {
/**
* Slide is missing files
*/
MissingFiles: 0,
/**
* Slide is queued for upload
*/
Queued: 1,
/**
* Slide is uploading
*/
Uploading: 2,
/**
* Slide uploading was unsuccessful
*/
Error: 3,
/**
* Slide uploaded successfully
*/
Done: 4,
/**
* Slide not supported
*/
NotSupported: 5,
};
// /**
// * @typedef ImageUpload~TreeNode
// * @property {string} name - File or directory name
// * @property {ImageUpload~TreeNode[]} children - Child nodes
// * @property {File} file - The file object for file nodes
// * @property {string} path - Relative path
// * @property {boolean} isLeaf
// */