From 32c73d14bfeb0a0b5f00dd8f19d21c5afe6946b0 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Wed, 25 Jun 2025 19:34:01 -0700 Subject: [PATCH 01/52] - #1186 (pending CORS) --- static/css/style.css | 4 ++-- static/js/downloader.js | 33 ++++++++++++--------------------- static/js/image_search.js | 18 +++++------------- static/js/tables.js | 11 +++++++++-- 4 files changed, 28 insertions(+), 38 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index 38f826104..199975ded 100755 --- a/static/css/style.css +++ b/static/css/style.css @@ -4506,6 +4506,6 @@ d-topics-list iframe { font-size: smaller; } -.collapse-all, .open-all { +.collapse-all, .open-all, .download-all-instances { cursor: pointer; -} \ No newline at end of file +} diff --git a/static/js/downloader.js b/static/js/downloader.js index 7ade5d6ba..e89056250 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -21,17 +21,13 @@ require.config({ baseUrl: STATIC_FILES_URL + 'js/', paths: { jquery: 'libs/jquery-3.7.1.min', - underscore: 'libs/underscore-min', base: 'base' - }, - shim: { - 'underscore': {exports: '_'} } }); require([ - 'base', // This must always be loaded -], function (base) { + 'base', 'jquery' +], function (base, $) { let downloadWorkers = []; let downloadWorker = null; @@ -112,7 +108,6 @@ require([ console.error(error) self.postMessage({message: "error", path: s3_url, error: error}) } - } `; @@ -127,23 +122,20 @@ require([ } // TODO: replace with call to JS messenger block - function statusMessage(message) { - const messageP = document.createElement("p"); - messageP.innerText = message; - document.body.appendChild(messageP); + function statusMessage(message, type) { + base.showJsMessage(type, message); } - const progressP = document.querySelector("#progress"); function progressUpdate(message) { - progressP.innerText = message; + base.showJsMessage('info', message, true); } function workerOnMessage (event) { let thisWorker = event.target; if (event.data.message === 'error') { - statusMessage(`Error ${JSON.stringify(event.data)}`); + statusMessage(`Error ${JSON.stringify(event.data)}`, 'alert'); } if (event.data.message === 'done') { - progressUpdate(`${s3_urls.length} remaining: ${event.data.path} downloaded`); + progressUpdate(`Download progress: ${s3_urls.length} remaining, ${event.data.path} downloaded`); } if (s3_urls.length == 0 || thisWorker.downloadCount > workerDownloadThreshold) { finalizeWorker(thisWorker); @@ -160,7 +152,7 @@ require([ downloadWorker.onerror = function(event) { let thisWorker = event.target console.error('Main: Error in worker:', event.message, event); - statusMessage('Main: Error in worker:', event.message, event); + statusMessage(`Error in worker: ${event.message}`, 'error'); finalizeWorker(thisWorker); } downloadWorkers.downloadCount = 0; @@ -174,7 +166,7 @@ require([ function triggerWorkerDownloads() { if (s3_urls.length == 0 && downloadWorkers.length == 0) { if (workerObjectURL) URL.revokeObjectURL(workerObjectURL); - statusMessage(`Downloads complete`); + statusMessage(`Downloads complete`, 'info'); } else { while (s3_urls.length > 0) { let targetWorker = null; @@ -246,9 +238,9 @@ require([ } } - $('.container-fluid').on('.download-all-instances', 'click', function(){ + $('.container-fluid').on('click', '.download-all-instances', function(){ let bucket = $(this).attr('data-bucket'); - let crdc_series_id = $(this).attr('data-crdc-series-id'); + let crdc_series_id = $(this).attr('data-series'); getAllS3ObjectKeys(bucket, "us-east-1", crdc_series_id).then( keys => { keys.forEach((key) => { if (key != "") { @@ -256,8 +248,7 @@ require([ } }); beginDownload().then( - // TODO: use JS messager - function(){statusMessage("Download complete.");} + function(){statusMessage("Download underway.", 'info');} ); }); }); diff --git a/static/js/image_search.js b/static/js/image_search.js index 92a03f878..0fd14b223 100644 --- a/static/js/image_search.js +++ b/static/js/image_search.js @@ -18,6 +18,7 @@ require.config({ baseUrl: STATIC_FILES_URL + 'js/', paths: { + downloader: 'downloader', jquery: 'libs/jquery-3.7.1.min', bootstrap: 'libs/bootstrap.min', jqueryui: 'libs/jquery-ui.min', @@ -35,8 +36,6 @@ require.config({ cartutils: 'cartutils', tippy: 'libs/tippy-bundle.umd.min', '@popperjs/core': 'libs/popper.min' - - }, shim: { 'bootstrap': ['jquery'], @@ -57,7 +56,6 @@ require.config({ exports: 'tippy', deps: ['@popperjs/core'] } - } }); @@ -74,7 +72,8 @@ require([ 'base', // This must ALWAYS be loaded! 'jquerydt', 'jqueryui', - 'bootstrap' + 'bootstrap', + 'downloader' ], function(plotutils,filterutils,sliderutils, tables, cartutils, tippy,$, _, base) { @@ -806,8 +805,7 @@ require([ localStorage.setItem("projA", JSON.stringify(projA)); localStorage.setItem("maxSeries", maxSeries); localStorage.setItem("maxStudies", maxStudies); - } - else{ + } else{ if ("projA" in sessionStorage){ localStorage.remove("projA"); } @@ -825,12 +823,6 @@ require([ localStorage.removeItem("cartcleared"); window.resetCart(); } - - } - - $('body').on('input', 'focus', function(){ - console.log($(this)); - }); - + }; }); diff --git a/static/js/tables.js b/static/js/tables.js index 5389b6abd..8a8316c40 100644 --- a/static/js/tables.js +++ b/static/js/tables.js @@ -1127,7 +1127,8 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, "dom": '<"dataTables_controls"ilp>rt<"bottom"><"clear">', "order": [[0, "asc"]], "createdRow": function (row, data, dataIndex) { - $(row).attr('id', 'series_' + data['SeriesInstanceUID']) + $(row).attr('id', 'series_' + data['SeriesInstanceUID']); + $(row).attr('data-seriesid', data['SeriesInstanceUID']); $(row).attr('data-studyid', data['StudyInstanceUID']); $(row).attr('data-caseid', data['PatientID']); $(row).attr('data-projectid', data['collection_id'][0]); @@ -1323,7 +1324,13 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, "type":"html", "orderable": false, data: 'SeriesInstanceUID', render: function (data){ - return '' + return '' + } + }, { + "type":"html", + "orderable": false, + data: 'SeriesInstanceUID', render: function (data, type, row){ + return `` } } ], From 7b0fcc2ee48e0e71e035ca9f615f749917715c33 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Wed, 25 Jun 2025 22:00:16 -0700 Subject: [PATCH 02/52] -> Ask for the right ID --- static/js/explore.js | 20 ++++++++++++++++++++ static/js/tables.js | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/static/js/explore.js b/static/js/explore.js index 0c4845df7..3a8e448a3 100644 --- a/static/js/explore.js +++ b/static/js/explore.js @@ -546,6 +546,26 @@ require([ maxWidth: 85 }); + tippy.delegate('.series-table', { + content: 'Download all of the image instances for this series.', + theme: 'dark', + placement: 'left', + arrow: false, + interactive:true, + target: '.download-all-instances', + maxWidth: 200 + }); + + tippy.delegate('.series-table', { + content: 'Download a manifest file for this series.', + theme: 'dark', + placement: 'left', + arrow: false, + interactive:true, + target: '.export-button', + maxWidth: 200 + }); + tippy.delegate('.series-table', { content: 'Some or all of the images in this collection are not publicly available.', theme: 'dark', diff --git a/static/js/tables.js b/static/js/tables.js index 8a8316c40..177610478 100644 --- a/static/js/tables.js +++ b/static/js/tables.js @@ -1330,7 +1330,7 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, "type":"html", "orderable": false, data: 'SeriesInstanceUID', render: function (data, type, row){ - return `` + return `` } } ], From f36c06f49180993cc522360073cf793dc352cf1b Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Wed, 25 Jun 2025 22:28:41 -0700 Subject: [PATCH 03/52] -> Error message cleanup --- static/js/downloader.js | 8 ++++++-- static/js/explore.js | 10 ++++++++++ static/js/tables.js | 36 +++++++++++++++++++----------------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index e89056250..c2aa18169 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -132,7 +132,7 @@ require([ function workerOnMessage (event) { let thisWorker = event.target; if (event.data.message === 'error') { - statusMessage(`Error ${JSON.stringify(event.data)}`, 'alert'); + statusMessage(`Error ${JSON.stringify(event)}`, 'error'); } if (event.data.message === 'done') { progressUpdate(`Download progress: ${s3_urls.length} remaining, ${event.data.path} downloaded`); @@ -243,10 +243,14 @@ require([ let crdc_series_id = $(this).attr('data-series'); getAllS3ObjectKeys(bucket, "us-east-1", crdc_series_id).then( keys => { keys.forEach((key) => { - if (key != "") { + if (key !== "") { s3_urls.push(`https://${bucket}.s3.us-east-1.amazonaws.com/${key}`); } }); + if(s3_urls.length <= 0) { + statusMessage('Error while parsing instance list!', 'error'); + return; + } beginDownload().then( function(){statusMessage("Download underway.", 'info');} ); diff --git a/static/js/explore.js b/static/js/explore.js index 3a8e448a3..054ac08c7 100644 --- a/static/js/explore.js +++ b/static/js/explore.js @@ -566,6 +566,16 @@ require([ maxWidth: 200 }); + tippy.delegate('.series-table', { + content: 'Direct download is only available in Chromium browsers.', + theme: 'dark', + placement: 'left', + arrow: false, + interactive:true, + target: '.download-all-disabled', + maxWidth: 200 + }); + tippy.delegate('.series-table', { content: 'Some or all of the images in this collection are not publicly available.', theme: 'dark', diff --git a/static/js/tables.js b/static/js/tables.js index 177610478..d56c6a35c 100644 --- a/static/js/tables.js +++ b/static/js/tables.js @@ -1132,9 +1132,9 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, $(row).attr('data-studyid', data['StudyInstanceUID']); $(row).attr('data-caseid', data['PatientID']); $(row).attr('data-projectid', data['collection_id'][0]); - $(row).attr('data-crdc', data['crdc_series_uuid']) - $(row).attr('data-aws', data['aws_bucket']) - $(row).attr('data-gcs', data['gcs_bucket']) + $(row).attr('data-crdc', data['crdc_series_uuid']); + $(row).attr('data-aws', data['aws_bucket']); + $(row).attr('data-gcs', data['gcs_bucket']); $(row).addClass('text_head'); $(row).attr('data-aws', data['aws_bucket']) @@ -1142,17 +1142,17 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, if ('cart_series_in_collection' in data){ $(row).attr('cart_series_in_collection', data['cart_series_in_collection']); } else{ - $(row).attr('cart_series_in_collection','0') + $(row).attr('cart_series_in_collection','0'); } if ('filter_series_in_collection' in data){ $(row).attr('filter_series_in_collection', data['filter_series_in_collection']); } else { - $(row).attr('filter_series_in_collection','0') + $(row).attr('filter_series_in_collection','0'); } if ('filter_cart_series_in_collection' in data){ $(row).attr('filter_cart_series_in_collection', data['filter_cart_series_in_collection']); } else { - $(row).attr('filter_cart_series_in_collection','0') + $(row).attr('filter_cart_series_in_collection','0'); } if ('cart_series_in_case' in data){ @@ -1163,28 +1163,28 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, if ('filter_series_in_case' in data){ $(row).attr('filter_series_in_case', data['filter_series_in_case']); } else { - $(row).attr('filter_series_in_case','0') + $(row).attr('filter_series_in_case','0'); } if ('filter_cart_series_in_case' in data){ $(row).attr('filter_cart_series_in_case', data['filter_cart_series_in_case']); } else { - $(row).attr('filter_cart_series_in_case','0') + $(row).attr('filter_cart_series_in_case','0'); } if ('cart_series_in_study' in data){ $(row).attr('cart_series_in_study', data['cart_series_in_study']); } else { - $(row).attr('cart_series_in_study','0') + $(row).attr('cart_series_in_study','0'); } if ('filter_series_in_study' in data){ $(row).attr('filter_series_in_study', data['filter_series_in_study']); } else { - $(row).attr('filter_series_in_study','0') + $(row).attr('filter_series_in_study','0'); } if ('filter_cart_series_in_study' in data){ $(row).attr('filter_cart_series_in_study', data['filter_cart_series_in_study']); } else { - $(row).attr('filter_cart_series_in_study','0') + $(row).attr('filter_cart_series_in_study','0'); } var collection_id=data['collection_id'][0]; @@ -1197,7 +1197,7 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, $(row).addClass('someInCart'); } else { $(row).attr('series_in_cart','0'); - $(row).attr('series_in_filter_and_cart','0') + $(row).attr('series_in_filter_and_cart','0'); $(row).removeClass('someInCart'); } $(row).attr('series_in_filter','1'); @@ -1221,7 +1221,6 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, {className: "series-description", "targets": [6]}, {className: "ohif open-viewer", "targets": [7]}, {className: "download-col", "targets": [8]}, - ], "columns": [ {"type": "html", "orderable": false, render: function () { @@ -1278,12 +1277,12 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, if (Array.isArray(row['collection_id'])){ coll_id=row['collection_id'][0]; } else { - coll_id=row['collection_id'] + coll_id=row['collection_id']; } if (row['access'].includes('Limited') ) { return ''; } else if ( (Array.isArray(row['Modality']) && row['Modality'].some(function(el){ - return nonViewAbleModality.has(el) + return nonViewAbleModality.has(el); }) ) || nonViewAbleModality.has(row['Modality']) ) { let tooltip = "no-viewer-tooltip"; return ``; @@ -1291,7 +1290,7 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, return slimViewAbleModality.has(el)} ) ) || (slimViewAbleModality.has(row['Modality']))) { return '' + '" target="_blank" rel="noopener noreferrer">'; } else { let v2_link = is_xc ? "" : OHIF_V2_PATH + row['StudyInstanceUID'] + '?SeriesInstanceUID=' + data; let v3_link = OHIF_V3_PATH + "=" + row['StudyInstanceUID'] + '&SeriesInstanceUIDs=' + data; @@ -1330,7 +1329,10 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, "type":"html", "orderable": false, data: 'SeriesInstanceUID', render: function (data, type, row){ - return `` + if("showDirectoryPicker" in window) { + return `` + } + return `` } } ], From 198b4453f86cf2eb840296ca12032f3bfe6998c9 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Wed, 25 Jun 2025 22:30:19 -0700 Subject: [PATCH 04/52] -> Error message cleanup --- static/js/downloader.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index c2aa18169..41db3d3a4 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -123,7 +123,7 @@ require([ // TODO: replace with call to JS messenger block function statusMessage(message, type) { - base.showJsMessage(type, message); + base.showJsMessage(type, message, true); } function progressUpdate(message) { base.showJsMessage('info', message, true); @@ -132,7 +132,7 @@ require([ function workerOnMessage (event) { let thisWorker = event.target; if (event.data.message === 'error') { - statusMessage(`Error ${JSON.stringify(event)}`, 'error'); + statusMessage(`Error ${JSON.stringify(event)}`, 'error', true); } if (event.data.message === 'done') { progressUpdate(`Download progress: ${s3_urls.length} remaining, ${event.data.path} downloaded`); @@ -152,7 +152,7 @@ require([ downloadWorker.onerror = function(event) { let thisWorker = event.target console.error('Main: Error in worker:', event.message, event); - statusMessage(`Error in worker: ${event.message}`, 'error'); + statusMessage(`Error in worker: ${event.message}`, 'error', true); finalizeWorker(thisWorker); } downloadWorkers.downloadCount = 0; @@ -166,7 +166,7 @@ require([ function triggerWorkerDownloads() { if (s3_urls.length == 0 && downloadWorkers.length == 0) { if (workerObjectURL) URL.revokeObjectURL(workerObjectURL); - statusMessage(`Downloads complete`, 'info'); + statusMessage(`Downloads complete`, 'info', true); } else { while (s3_urls.length > 0) { let targetWorker = null; From 3e71b77b222feb29568eaca65113bb192d172838 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Thu, 3 Jul 2025 12:01:06 -0700 Subject: [PATCH 05/52] -> login banner --- templates/idc/landing.html | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/templates/idc/landing.html b/templates/idc/landing.html index 7adaccf43..122e31ee7 100644 --- a/templates/idc/landing.html +++ b/templates/idc/landing.html @@ -45,6 +45,21 @@ +
From 92158fba0e72582fc210fc2f6c2e6a978d4bcd3f Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Thu, 3 Jul 2025 15:48:14 -0700 Subject: [PATCH 06/52] -> Downloads --- static/js/downloader.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index 41db3d3a4..3886ba773 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -82,8 +82,8 @@ require([ const collection_id = event.data.collection_id || 'unknown_collection' response = await fetch(s3_url) if (!response.ok) { - console.error('Worker: Failed to fetch S3 URL:', s3_url, response.statusText) - self.postMessage({message: "error", error: "Failed to fetch S3 URL"}) + console.error('Worker: Failed to fetch URL:', s3_url, response.statusText) + self.postMessage({message: "error", error: "Failed to fetch URL"}) return } const arrayBuffer = await response.arrayBuffer() @@ -105,7 +105,8 @@ require([ await writable.close() self.postMessage({message: "done", path: s3_url, localFilePath: filePath}) } catch (error) { - console.error(error) + console.error("Error when attempting to fetch URL "+s3_url); + console.error(error); self.postMessage({message: "error", path: s3_url, error: error}) } } @@ -121,7 +122,6 @@ require([ downloadWorkers = downloadWorkers.filter(w => w !== worker); } - // TODO: replace with call to JS messenger block function statusMessage(message, type) { base.showJsMessage(type, message, true); } @@ -132,7 +132,7 @@ require([ function workerOnMessage (event) { let thisWorker = event.target; if (event.data.message === 'error') { - statusMessage(`Error ${JSON.stringify(event)}`, 'error', true); + statusMessage(`Worker Error ${JSON.stringify(event)}`, 'error', true); } if (event.data.message === 'done') { progressUpdate(`Download progress: ${s3_urls.length} remaining, ${event.data.path} downloaded`); @@ -151,7 +151,7 @@ require([ downloadWorker.onmessage = workerOnMessage; downloadWorker.onerror = function(event) { let thisWorker = event.target - console.error('Main: Error in worker:', event.message, event); + console.error('Main: Error in worker:', event.message || "No message given", event); statusMessage(`Error in worker: ${event.message}`, 'error', true); finalizeWorker(thisWorker); } From 86bb4db0665b8614d92b7112b7844fabacf78544 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Thu, 3 Jul 2025 16:27:31 -0700 Subject: [PATCH 07/52] -> Move worker code to its own file --- static/js/downloadWorker.js | 95 +++++++++++++++++++++++++++++++++++++ static/js/downloader.js | 88 ++-------------------------------- 2 files changed, 98 insertions(+), 85 deletions(-) create mode 100644 static/js/downloadWorker.js diff --git a/static/js/downloadWorker.js b/static/js/downloadWorker.js new file mode 100644 index 000000000..364495592 --- /dev/null +++ b/static/js/downloadWorker.js @@ -0,0 +1,95 @@ +/** + * + * Copyright 2025, Institute for Systems Biology + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// by @pieper 6/25/25 +async function createNestedDirectories(topLevelDirectoryHandle, path) { + const pathSegments = path.split('/').filter((segment) => segment !== ''); + let currentDirectoryHandle = topLevelDirectoryHandle; + for (const segment of pathSegments) { + try { + // Attempt to get the directory handle without creating it + const entry = await currentDirectoryHandle.getDirectoryHandle(segment, { + create: false + }) + currentDirectoryHandle = entry; + } catch (error) { + // If the error is specifically about the directory not existing, create it + if (error.name === 'NotFoundError') { + const entry = await currentDirectoryHandle.getDirectoryHandle(segment, { + create: true + }) + currentDirectoryHandle = entry; + } else { + // TODO: Handle other potential errors (e.g., name conflicts) + return false; // Indicate failure + } + } + } + // Return the last directory handle + return currentDirectoryHandle; +} + +importScripts('https://cdn.jsdelivr.net/npm/dcmjs@0.41.0/build/dcmjs.min.js') + +function dicomValue(dataset, tagName) { + let value = "Undefined-" + tagName; + const entry = dcmjs.data.DicomMetaDictionary.nameMap[tagName]; + if (entry && entry.tag) { + const hexTag = entry.tag.replace("(", "").replace(",", "").replace(")", ""); + if (hexTag in dataset.dict) { + value = dataset.dict[hexTag].Value; + } + } + return value +} + +self.onmessage = async function (event) { + const s3_url = event.data.url; + try { + const directoryHandle = event.data.directoryHandle; + const collection_id = event.data.collection_id || 'unknown_collection'; + response = await fetch(s3_url) + if (!response.ok) { + console.error('Worker: Failed to fetch URL:', s3_url, response.statusText); + self.postMessage({message: "error", error: "Failed to fetch URL"}); + return + } + const arrayBuffer = await response.arrayBuffer(); + const dataset = dcmjs.data.DicomMessage.readFile(arrayBuffer); + const modality = dicomValue(dataset, "Modality"); + const patientID = dicomValue(dataset, "PatientID"); + const sopInstanceUID = dicomValue(dataset, "SOPInstanceUID"); + const studyInstanceUID = dicomValue(dataset, "StudyInstanceUID"); + const seriesInstanceUID = dicomValue(dataset, "SeriesInstanceUID"); + const seriesDirectory = modality + "_" + seriesInstanceUID; + const filePath = [collection_id, patientID, studyInstanceUID, seriesDirectory].join("/"); + const fileName = sopInstanceUID + ".dcm"; + const blob = new Blob([arrayBuffer], {type: 'application/dicom'}); + const file = new File([blob], fileName, {type: 'application/dicom'}); + const subDirectoryHandle = await createNestedDirectories(directoryHandle, filePath); + const fileHandle = await subDirectoryHandle.getFileHandle(fileName, {create: true,}); + const writable = await fileHandle.createWritable(); + await writable.write(arrayBuffer); + await writable.close(); + self.postMessage({message: "done", path: s3_url, localFilePath: filePath}); + } catch (error) { + console.error("Error when attempting to fetch URL " + s3_url); + console.error(error); + self.postMessage({message: "error", path: s3_url, error: error}); + } +} \ No newline at end of file diff --git a/static/js/downloader.js b/static/js/downloader.js index 3886ba773..2ba248386 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -33,87 +33,6 @@ require([ let downloadWorker = null; const s3_urls = []; - const workerCode = ` - async function createNestedDirectories(topLevelDirectoryHandle, path) { - const pathSegments = path.split('/').filter((segment) => segment !== '') - let currentDirectoryHandle = topLevelDirectoryHandle - for (const segment of pathSegments) { - try { - // Attempt to get the directory handle without creating it - const entry = await currentDirectoryHandle.getDirectoryHandle(segment, { - create: false, - }) - currentDirectoryHandle = entry - } catch (error) { - // If the error is specifically about the directory not existing, create it - if (error .name === 'NotFoundError') { - const entry = await currentDirectoryHandle.getDirectoryHandle(segment, { - create: true, - }) - currentDirectoryHandle = entry - } else { - // Handle other potential errors (e.g., name conflicts) - return false // Indicate failure - } - } - } - // Return the last directory handle - return currentDirectoryHandle - } - - importScripts('https://cdn.jsdelivr.net/npm/dcmjs@0.41.0/build/dcmjs.min.js') - - function dicomValue(dataset,tagName) { - let value = "Undefined-"+tagName - const entry = dcmjs.data.DicomMetaDictionary.nameMap[tagName] - if (entry && entry.tag) { - const hexTag = entry.tag.replace("(","").replace(",","").replace(")","") - if (hexTag in dataset.dict) { - value = dataset.dict[hexTag].Value - } - } - return value - } - - self.onmessage = async function(event) { - const s3_url = event.data.url - try { - const directoryHandle = event.data.directoryHandle - const collection_id = event.data.collection_id || 'unknown_collection' - response = await fetch(s3_url) - if (!response.ok) { - console.error('Worker: Failed to fetch URL:', s3_url, response.statusText) - self.postMessage({message: "error", error: "Failed to fetch URL"}) - return - } - const arrayBuffer = await response.arrayBuffer() - const dataset = dcmjs.data.DicomMessage.readFile(arrayBuffer) - const modality = dicomValue(dataset,"Modality") - const patientID = dicomValue(dataset,"PatientID") - const sopInstanceUID = dicomValue(dataset,"SOPInstanceUID") - const studyInstanceUID = dicomValue(dataset,"StudyInstanceUID") - const seriesInstanceUID = dicomValue(dataset,"SeriesInstanceUID") - const seriesDirectory = modality + "_" + seriesInstanceUID - const filePath = [collection_id,patientID,studyInstanceUID,seriesDirectory].join("/") - const fileName = sopInstanceUID + ".dcm" - const blob = new Blob([arrayBuffer], { type: 'application/dicom' }) - const file = new File([blob], fileName, { type: 'application/dicom' }) - const subDirectoryHandle = await createNestedDirectories(directoryHandle, filePath) - const fileHandle = await subDirectoryHandle.getFileHandle(fileName, { create: true, }) - const writable = await fileHandle.createWritable() - await writable.write(arrayBuffer) - await writable.close() - self.postMessage({message: "done", path: s3_url, localFilePath: filePath}) - } catch (error) { - console.error("Error when attempting to fetch URL "+s3_url); - console.error(error); - self.postMessage({message: "error", path: s3_url, error: error}) - } - } - `; - - const workerCodeBlob = new Blob([workerCode], { type: 'application/javascript' }); - const workerObjectURL = URL.createObjectURL(workerCodeBlob); const workerDownloadThreshold = 100; const availableWorkers = []; @@ -147,17 +66,17 @@ require([ } function allocateWorker() { - downloadWorker = new Worker(workerObjectURL); + downloadWorker = new Worker(STATIC_FILES_URL + 'js/downloadWorker.js'); downloadWorker.onmessage = workerOnMessage; downloadWorker.onerror = function(event) { - let thisWorker = event.target + let thisWorker = event.target; console.error('Main: Error in worker:', event.message || "No message given", event); statusMessage(`Error in worker: ${event.message}`, 'error', true); finalizeWorker(thisWorker); } downloadWorkers.downloadCount = 0; downloadWorkers.push(downloadWorker); - return downloadWorker + return downloadWorker; } let workerLimit = navigator.hardwareConcurrency; @@ -165,7 +84,6 @@ require([ function triggerWorkerDownloads() { if (s3_urls.length == 0 && downloadWorkers.length == 0) { - if (workerObjectURL) URL.revokeObjectURL(workerObjectURL); statusMessage(`Downloads complete`, 'info', true); } else { while (s3_urls.length > 0) { From 84246c5b479fdebbdcdf6273ebe953e12c80adb7 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Thu, 3 Jul 2025 16:48:05 -0700 Subject: [PATCH 08/52] -> Inline load the worker code via textJS --- static/js/downloader.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index 2ba248386..b484d6b2b 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -21,18 +21,21 @@ require.config({ baseUrl: STATIC_FILES_URL + 'js/', paths: { jquery: 'libs/jquery-3.7.1.min', - base: 'base' + base: 'base', + text: 'libs/text' } }); require([ - 'base', 'jquery' -], function (base, $) { + 'base', 'jquery', 'text!downloadWorker.js' +], function (base, $, workerCode) { let downloadWorkers = []; let downloadWorker = null; const s3_urls = []; + const workerCodeBlob = new Blob([workerCode], { type: 'application/javascript' }); + const workerObjectURL = URL.createObjectURL(workerCodeBlob); const workerDownloadThreshold = 100; const availableWorkers = []; @@ -66,17 +69,17 @@ require([ } function allocateWorker() { - downloadWorker = new Worker(STATIC_FILES_URL + 'js/downloadWorker.js'); + downloadWorker = new Worker(workerObjectURL); downloadWorker.onmessage = workerOnMessage; downloadWorker.onerror = function(event) { - let thisWorker = event.target; + let thisWorker = event.target console.error('Main: Error in worker:', event.message || "No message given", event); statusMessage(`Error in worker: ${event.message}`, 'error', true); finalizeWorker(thisWorker); } downloadWorkers.downloadCount = 0; downloadWorkers.push(downloadWorker); - return downloadWorker; + return downloadWorker } let workerLimit = navigator.hardwareConcurrency; @@ -84,6 +87,7 @@ require([ function triggerWorkerDownloads() { if (s3_urls.length == 0 && downloadWorkers.length == 0) { + if (workerObjectURL) URL.revokeObjectURL(workerObjectURL); statusMessage(`Downloads complete`, 'info', true); } else { while (s3_urls.length > 0) { From b787481da1304fcd4071c11d725d707a00780ec1 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Thu, 3 Jul 2025 17:12:23 -0700 Subject: [PATCH 09/52] -> Inline load the worker code via textJS --- static/js/libs/text.js | 412 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 static/js/libs/text.js diff --git a/static/js/libs/text.js b/static/js/libs/text.js new file mode 100644 index 000000000..631926ecf --- /dev/null +++ b/static/js/libs/text.js @@ -0,0 +1,412 @@ +/** + * @license text 2.0.16 Copyright jQuery Foundation and other contributors. + * Released under MIT license, http://github.com/requirejs/text/LICENSE + */ +/*jslint regexp: true */ +/*global require, XMLHttpRequest, ActiveXObject, + define, window, process, Packages, + java, location, Components, FileUtils */ + +define(['module'], function (module) { + 'use strict'; + + var text, fs, Cc, Ci, xpcIsWindows, + progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], + xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im, + bodyRegExp = /]*>\s*([\s\S]+)\s*<\/body>/im, + hasLocation = typeof location !== 'undefined' && location.href, + defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''), + defaultHostName = hasLocation && location.hostname, + defaultPort = hasLocation && (location.port || undefined), + buildMap = {}, + masterConfig = (module.config && module.config()) || {}; + + function useDefault(value, defaultValue) { + return value === undefined || value === '' ? defaultValue : value; + } + + //Allow for default ports for http and https. + function isSamePort(protocol1, port1, protocol2, port2) { + if (port1 === port2) { + return true; + } else if (protocol1 === protocol2) { + if (protocol1 === 'http') { + return useDefault(port1, '80') === useDefault(port2, '80'); + } else if (protocol1 === 'https') { + return useDefault(port1, '443') === useDefault(port2, '443'); + } + } + return false; + } + + text = { + version: '2.0.16', + + strip: function (content) { + //Strips declarations so that external SVG and XML + //documents can be added to a document without worry. Also, if the string + //is an HTML document, only the part inside the body tag is returned. + if (content) { + content = content.replace(xmlRegExp, ""); + var matches = content.match(bodyRegExp); + if (matches) { + content = matches[1]; + } + } else { + content = ""; + } + return content; + }, + + jsEscape: function (content) { + return content.replace(/(['\\])/g, '\\$1') + .replace(/[\f]/g, "\\f") + .replace(/[\b]/g, "\\b") + .replace(/[\n]/g, "\\n") + .replace(/[\t]/g, "\\t") + .replace(/[\r]/g, "\\r") + .replace(/[\u2028]/g, "\\u2028") + .replace(/[\u2029]/g, "\\u2029"); + }, + + createXhr: masterConfig.createXhr || function () { + //Would love to dump the ActiveX crap in here. Need IE 6 to die first. + var xhr, i, progId; + if (typeof XMLHttpRequest !== "undefined") { + return new XMLHttpRequest(); + } else if (typeof ActiveXObject !== "undefined") { + for (i = 0; i < 3; i += 1) { + progId = progIds[i]; + try { + xhr = new ActiveXObject(progId); + } catch (e) {} + + if (xhr) { + progIds = [progId]; // so faster next time + break; + } + } + } + + return xhr; + }, + + /** + * Parses a resource name into its component parts. Resource names + * look like: module/name.ext!strip, where the !strip part is + * optional. + * @param {String} name the resource name + * @returns {Object} with properties "moduleName", "ext" and "strip" + * where strip is a boolean. + */ + parseName: function (name) { + var modName, ext, temp, + strip = false, + index = name.lastIndexOf("."), + isRelative = name.indexOf('./') === 0 || + name.indexOf('../') === 0; + + if (index !== -1 && (!isRelative || index > 1)) { + modName = name.substring(0, index); + ext = name.substring(index + 1); + } else { + modName = name; + } + + temp = ext || modName; + index = temp.indexOf("!"); + if (index !== -1) { + //Pull off the strip arg. + strip = temp.substring(index + 1) === "strip"; + temp = temp.substring(0, index); + if (ext) { + ext = temp; + } else { + modName = temp; + } + } + + return { + moduleName: modName, + ext: ext, + strip: strip + }; + }, + + xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/, + + /** + * Is an URL on another domain. Only works for browser use, returns + * false in non-browser environments. Only used to know if an + * optimized .js version of a text resource should be loaded + * instead. + * @param {String} url + * @returns Boolean + */ + useXhr: function (url, protocol, hostname, port) { + var uProtocol, uHostName, uPort, + match = text.xdRegExp.exec(url); + if (!match) { + return true; + } + uProtocol = match[2]; + uHostName = match[3]; + + uHostName = uHostName.split(':'); + uPort = uHostName[1]; + uHostName = uHostName[0]; + + return (!uProtocol || uProtocol === protocol) && + (!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) && + ((!uPort && !uHostName) || isSamePort(uProtocol, uPort, protocol, port)); + }, + + finishLoad: function (name, strip, content, onLoad) { + content = strip ? text.strip(content) : content; + if (masterConfig.isBuild) { + buildMap[name] = content; + } + onLoad(content); + }, + + load: function (name, req, onLoad, config) { + //Name has format: some.module.filext!strip + //The strip part is optional. + //if strip is present, then that means only get the string contents + //inside a body tag in an HTML string. For XML/SVG content it means + //removing the declarations so the content can be inserted + //into the current doc without problems. + + // Do not bother with the work if a build and text will + // not be inlined. + if (config && config.isBuild && !config.inlineText) { + onLoad(); + return; + } + + masterConfig.isBuild = config && config.isBuild; + + var parsed = text.parseName(name), + nonStripName = parsed.moduleName + + (parsed.ext ? '.' + parsed.ext : ''), + url = req.toUrl(nonStripName), + useXhr = (masterConfig.useXhr) || + text.useXhr; + + // Do not load if it is an empty: url + if (url.indexOf('empty:') === 0) { + onLoad(); + return; + } + + //Load the text. Use XHR if possible and in a browser. + if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) { + text.get(url, function (content) { + text.finishLoad(name, parsed.strip, content, onLoad); + }, function (err) { + if (onLoad.error) { + onLoad.error(err); + } + }); + } else { + //Need to fetch the resource across domains. Assume + //the resource has been optimized into a JS module. Fetch + //by the module name + extension, but do not include the + //!strip part to avoid file system issues. + req([nonStripName], function (content) { + text.finishLoad(parsed.moduleName + '.' + parsed.ext, + parsed.strip, content, onLoad); + }, function (err) { + if (onLoad.error) { + onLoad.error(err); + } + }); + } + }, + + write: function (pluginName, moduleName, write, config) { + if (buildMap.hasOwnProperty(moduleName)) { + var content = text.jsEscape(buildMap[moduleName]); + write.asModule(pluginName + "!" + moduleName, + "define(function () { return '" + + content + + "';});\n"); + } + }, + + writeFile: function (pluginName, moduleName, req, write, config) { + var parsed = text.parseName(moduleName), + extPart = parsed.ext ? '.' + parsed.ext : '', + nonStripName = parsed.moduleName + extPart, + //Use a '.js' file name so that it indicates it is a + //script that can be loaded across domains. + fileName = req.toUrl(parsed.moduleName + extPart) + '.js'; + + //Leverage own load() method to load plugin value, but only + //write out values that do not have the strip argument, + //to avoid any potential issues with ! in file names. + text.load(nonStripName, req, function (value) { + //Use own write() method to construct full module value. + //But need to create shell that translates writeFile's + //write() to the right interface. + var textWrite = function (contents) { + return write(fileName, contents); + }; + textWrite.asModule = function (moduleName, contents) { + return write.asModule(moduleName, fileName, contents); + }; + + text.write(pluginName, nonStripName, textWrite, config); + }, config); + } + }; + + if (masterConfig.env === 'node' || (!masterConfig.env && + typeof process !== "undefined" && + process.versions && + !!process.versions.node && + !process.versions['node-webkit'] && + !process.versions['atom-shell'])) { + //Using special require.nodeRequire, something added by r.js. + fs = require.nodeRequire('fs'); + + text.get = function (url, callback, errback) { + try { + var file = fs.readFileSync(url, 'utf8'); + //Remove BOM (Byte Mark Order) from utf8 files if it is there. + if (file[0] === '\uFEFF') { + file = file.substring(1); + } + callback(file); + } catch (e) { + if (errback) { + errback(e); + } + } + }; + } else if (masterConfig.env === 'xhr' || (!masterConfig.env && + text.createXhr())) { + text.get = function (url, callback, errback, headers) { + var xhr = text.createXhr(), header; + xhr.open('GET', url, true); + + //Allow plugins direct access to xhr headers + if (headers) { + for (header in headers) { + if (headers.hasOwnProperty(header)) { + xhr.setRequestHeader(header.toLowerCase(), headers[header]); + } + } + } + + //Allow overrides specified in config + if (masterConfig.onXhr) { + masterConfig.onXhr(xhr, url); + } + + xhr.onreadystatechange = function (evt) { + var status, err; + //Do not explicitly handle errors, those should be + //visible via console output in the browser. + if (xhr.readyState === 4) { + status = xhr.status || 0; + if (status > 399 && status < 600) { + //An http 4xx or 5xx error. Signal an error. + err = new Error(url + ' HTTP status: ' + status); + err.xhr = xhr; + if (errback) { + errback(err); + } + } else { + callback(xhr.responseText); + } + + if (masterConfig.onXhrComplete) { + masterConfig.onXhrComplete(xhr, url); + } + } + }; + xhr.send(null); + }; + } else if (masterConfig.env === 'rhino' || (!masterConfig.env && + typeof Packages !== 'undefined' && typeof java !== 'undefined')) { + //Why Java, why is this so awkward? + text.get = function (url, callback) { + var stringBuffer, line, + encoding = "utf-8", + file = new java.io.File(url), + lineSeparator = java.lang.System.getProperty("line.separator"), + input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)), + content = ''; + try { + stringBuffer = new java.lang.StringBuffer(); + line = input.readLine(); + + // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324 + // http://www.unicode.org/faq/utf_bom.html + + // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK: + // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058 + if (line && line.length() && line.charAt(0) === 0xfeff) { + // Eat the BOM, since we've already found the encoding on this file, + // and we plan to concatenating this buffer with others; the BOM should + // only appear at the top of a file. + line = line.substring(1); + } + + if (line !== null) { + stringBuffer.append(line); + } + + while ((line = input.readLine()) !== null) { + stringBuffer.append(lineSeparator); + stringBuffer.append(line); + } + //Make sure we return a JavaScript string and not a Java string. + content = String(stringBuffer.toString()); //String + } finally { + input.close(); + } + callback(content); + }; + } else if (masterConfig.env === 'xpconnect' || (!masterConfig.env && + typeof Components !== 'undefined' && Components.classes && + Components.interfaces)) { + //Avert your gaze! + Cc = Components.classes; + Ci = Components.interfaces; + Components.utils['import']('resource://gre/modules/FileUtils.jsm'); + xpcIsWindows = ('@mozilla.org/windows-registry-key;1' in Cc); + + text.get = function (url, callback) { + var inStream, convertStream, fileObj, + readData = {}; + + if (xpcIsWindows) { + url = url.replace(/\//g, '\\'); + } + + fileObj = new FileUtils.File(url); + + //XPCOM, you so crazy + try { + inStream = Cc['@mozilla.org/network/file-input-stream;1'] + .createInstance(Ci.nsIFileInputStream); + inStream.init(fileObj, 1, 0, false); + + convertStream = Cc['@mozilla.org/intl/converter-input-stream;1'] + .createInstance(Ci.nsIConverterInputStream); + convertStream.init(inStream, "utf-8", inStream.available(), + Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); + + convertStream.readString(inStream.available(), readData); + convertStream.close(); + inStream.close(); + callback(readData.value); + } catch (e) { + throw new Error((fileObj && fileObj.path || '') + ': ' + e); + } + }; + } + return text; +}); From e77151fc7d53871e9253e7078b716e0b19ebe316 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Mon, 7 Jul 2025 09:28:11 -0700 Subject: [PATCH 10/52] -> Inline load the worker code via textJS --- static/js/downloader.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index b484d6b2b..c6103d856 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -22,7 +22,8 @@ require.config({ paths: { jquery: 'libs/jquery-3.7.1.min', base: 'base', - text: 'libs/text' + text: 'libs/text', + downloadWorker: 'downloadWorker' } }); From be6bc24aa3a8b8b08f3a4c27becdc543c1677c09 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Mon, 7 Jul 2025 09:50:27 -0700 Subject: [PATCH 11/52] -> Inline load the worker code via textJS --- static/js/downloader.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index c6103d856..2ceec2b5f 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -22,8 +22,8 @@ require.config({ paths: { jquery: 'libs/jquery-3.7.1.min', base: 'base', - text: 'libs/text', - downloadWorker: 'downloadWorker' + 'downloadWorker.js': 'downloadWorker', + text: 'libs/text' } }); From b21f05cd874b6de1f7664e8f9472ec722e68210d Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Mon, 7 Jul 2025 11:13:43 -0700 Subject: [PATCH 12/52] -> Inline load the worker code via textJS --- static/js/downloader.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index 2ceec2b5f..597328ce7 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -22,13 +22,13 @@ require.config({ paths: { jquery: 'libs/jquery-3.7.1.min', base: 'base', - 'downloadWorker.js': 'downloadWorker', - text: 'libs/text' + text: 'libs/text', + downloadWorker: 'downloadWorker.js' } }); require([ - 'base', 'jquery', 'text!downloadWorker.js' + 'base', 'jquery', 'text!downloadWorker' ], function (base, $, workerCode) { let downloadWorkers = []; From fdbf6cffb2a3e15743a997f2bd232aa3d913dd2f Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Mon, 7 Jul 2025 11:35:02 -0700 Subject: [PATCH 13/52] -> Inline load the worker code via textJS --- static/js/{downloadWorker.js => downloadWorker.jsx} | 0 static/js/downloader.js | 5 ++--- 2 files changed, 2 insertions(+), 3 deletions(-) rename static/js/{downloadWorker.js => downloadWorker.jsx} (100%) diff --git a/static/js/downloadWorker.js b/static/js/downloadWorker.jsx similarity index 100% rename from static/js/downloadWorker.js rename to static/js/downloadWorker.jsx diff --git a/static/js/downloader.js b/static/js/downloader.js index 597328ce7..94d318469 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -22,13 +22,12 @@ require.config({ paths: { jquery: 'libs/jquery-3.7.1.min', base: 'base', - text: 'libs/text', - downloadWorker: 'downloadWorker.js' + text: 'libs/text' } }); require([ - 'base', 'jquery', 'text!downloadWorker' + 'base', 'jquery', 'text!downloadWorker.jsx' ], function (base, $, workerCode) { let downloadWorkers = []; From 5a096cab907c15fa85deff66c85b4fac33fd481e Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Mon, 7 Jul 2025 12:20:19 -0700 Subject: [PATCH 14/52] -> Inline load the worker code via textJS --- static/js/download.js | 51 ------------------- .../{downloadWorker.jsx => downloadWorker.js} | 0 static/js/downloader.js | 4 +- 3 files changed, 3 insertions(+), 52 deletions(-) delete mode 100644 static/js/download.js rename static/js/{downloadWorker.jsx => downloadWorker.js} (100%) diff --git a/static/js/download.js b/static/js/download.js deleted file mode 100644 index 15c2f8f87..000000000 --- a/static/js/download.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * - * Copyright 2020, Institute for Systems Biology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -require.config({ - baseUrl: STATIC_FILES_URL + 'js/', - paths: { - jquery: 'libs/jquery-3.7.1.min', - bootstrap: 'libs/bootstrap.min', - jqueryui: 'libs/jquery-ui.min', - underscore: 'libs/underscore-min', - assetscore: 'libs/assets.core', - assetsresponsive: 'libs/assets.responsive', - base: 'base', - sqlFormatter: 'libs/sql-formatter.min' - }, - shim: { - 'bootstrap': ['jquery'], - 'jqueryui': ['jquery'], - 'assetscore': ['jquery', 'bootstrap', 'jqueryui'], - 'assetsresponsive': ['jquery', 'bootstrap', 'jqueryui'] - } -}); - -require([ - 'jquery', - 'jqueryui', - 'base', - 'sqlFormatter', - 'bootstrap', - 'assetscore', - 'assetsresponsive' -], function($, jqueryui, base, sqlFormatter) { - $('#download-display').on('show.bs.modal', function() { - - }; -}); \ No newline at end of file diff --git a/static/js/downloadWorker.jsx b/static/js/downloadWorker.js similarity index 100% rename from static/js/downloadWorker.jsx rename to static/js/downloadWorker.js diff --git a/static/js/downloader.js b/static/js/downloader.js index 94d318469..89cdc0553 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -26,8 +26,10 @@ require.config({ } }); +let downloadworker_path = STATIC_FILES_URL + 'js/downloadWorker.js' + require([ - 'base', 'jquery', 'text!downloadWorker.jsx' + 'base', 'jquery', `text!${downloadworker_path}` ], function (base, $, workerCode) { let downloadWorkers = []; From f9a9d59cb80ceb599c2958be7d2db8f56750971a Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Mon, 7 Jul 2025 13:27:41 -0700 Subject: [PATCH 15/52] -> Script doesn't like the inline load --- static/js/downloadWorker.js | 95 ------------------------------------- static/js/downloader.js | 88 +++++++++++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 101 deletions(-) delete mode 100644 static/js/downloadWorker.js diff --git a/static/js/downloadWorker.js b/static/js/downloadWorker.js deleted file mode 100644 index 364495592..000000000 --- a/static/js/downloadWorker.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * - * Copyright 2025, Institute for Systems Biology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -// by @pieper 6/25/25 -async function createNestedDirectories(topLevelDirectoryHandle, path) { - const pathSegments = path.split('/').filter((segment) => segment !== ''); - let currentDirectoryHandle = topLevelDirectoryHandle; - for (const segment of pathSegments) { - try { - // Attempt to get the directory handle without creating it - const entry = await currentDirectoryHandle.getDirectoryHandle(segment, { - create: false - }) - currentDirectoryHandle = entry; - } catch (error) { - // If the error is specifically about the directory not existing, create it - if (error.name === 'NotFoundError') { - const entry = await currentDirectoryHandle.getDirectoryHandle(segment, { - create: true - }) - currentDirectoryHandle = entry; - } else { - // TODO: Handle other potential errors (e.g., name conflicts) - return false; // Indicate failure - } - } - } - // Return the last directory handle - return currentDirectoryHandle; -} - -importScripts('https://cdn.jsdelivr.net/npm/dcmjs@0.41.0/build/dcmjs.min.js') - -function dicomValue(dataset, tagName) { - let value = "Undefined-" + tagName; - const entry = dcmjs.data.DicomMetaDictionary.nameMap[tagName]; - if (entry && entry.tag) { - const hexTag = entry.tag.replace("(", "").replace(",", "").replace(")", ""); - if (hexTag in dataset.dict) { - value = dataset.dict[hexTag].Value; - } - } - return value -} - -self.onmessage = async function (event) { - const s3_url = event.data.url; - try { - const directoryHandle = event.data.directoryHandle; - const collection_id = event.data.collection_id || 'unknown_collection'; - response = await fetch(s3_url) - if (!response.ok) { - console.error('Worker: Failed to fetch URL:', s3_url, response.statusText); - self.postMessage({message: "error", error: "Failed to fetch URL"}); - return - } - const arrayBuffer = await response.arrayBuffer(); - const dataset = dcmjs.data.DicomMessage.readFile(arrayBuffer); - const modality = dicomValue(dataset, "Modality"); - const patientID = dicomValue(dataset, "PatientID"); - const sopInstanceUID = dicomValue(dataset, "SOPInstanceUID"); - const studyInstanceUID = dicomValue(dataset, "StudyInstanceUID"); - const seriesInstanceUID = dicomValue(dataset, "SeriesInstanceUID"); - const seriesDirectory = modality + "_" + seriesInstanceUID; - const filePath = [collection_id, patientID, studyInstanceUID, seriesDirectory].join("/"); - const fileName = sopInstanceUID + ".dcm"; - const blob = new Blob([arrayBuffer], {type: 'application/dicom'}); - const file = new File([blob], fileName, {type: 'application/dicom'}); - const subDirectoryHandle = await createNestedDirectories(directoryHandle, filePath); - const fileHandle = await subDirectoryHandle.getFileHandle(fileName, {create: true,}); - const writable = await fileHandle.createWritable(); - await writable.write(arrayBuffer); - await writable.close(); - self.postMessage({message: "done", path: s3_url, localFilePath: filePath}); - } catch (error) { - console.error("Error when attempting to fetch URL " + s3_url); - console.error(error); - self.postMessage({message: "error", path: s3_url, error: error}); - } -} \ No newline at end of file diff --git a/static/js/downloader.js b/static/js/downloader.js index 89cdc0553..f1e83ec68 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -21,16 +21,92 @@ require.config({ baseUrl: STATIC_FILES_URL + 'js/', paths: { jquery: 'libs/jquery-3.7.1.min', - base: 'base', - text: 'libs/text' + base: 'base' } }); -let downloadworker_path = STATIC_FILES_URL + 'js/downloadWorker.js' - require([ - 'base', 'jquery', `text!${downloadworker_path}` -], function (base, $, workerCode) { + 'base', 'jquery' +], function (base, $) { + + let workerCode = ` + async function createNestedDirectories(topLevelDirectoryHandle, path) { + const pathSegments = path.split('/').filter((segment) => segment !== ''); + let currentDirectoryHandle = topLevelDirectoryHandle; + for (const segment of pathSegments) { + try { + // Attempt to get the directory handle without creating it + const entry = await currentDirectoryHandle.getDirectoryHandle(segment, { + create: false + }) + currentDirectoryHandle = entry; + } catch (error) { + // If the error is specifically about the directory not existing, create it + if (error.name === 'NotFoundError') { + const entry = await currentDirectoryHandle.getDirectoryHandle(segment, { + create: true + }) + currentDirectoryHandle = entry; + } else { + // TODO: Handle other potential errors (e.g., name conflicts) + return false; // Indicate failure + } + } + } + // Return the last directory handle + return currentDirectoryHandle; + } + + importScripts('https://cdn.jsdelivr.net/npm/dcmjs@0.41.0/build/dcmjs.min.js') + + function dicomValue(dataset, tagName) { + let value = "Undefined-" + tagName; + const entry = dcmjs.data.DicomMetaDictionary.nameMap[tagName]; + if (entry && entry.tag) { + const hexTag = entry.tag.replace("(", "").replace(",", "").replace(")", ""); + if (hexTag in dataset.dict) { + value = dataset.dict[hexTag].Value; + } + } + return value + } + + self.onmessage = async function (event) { + const s3_url = event.data.url; + try { + const directoryHandle = event.data.directoryHandle; + const collection_id = event.data.collection_id || 'unknown_collection'; + response = await fetch(s3_url) + if (!response.ok) { + console.error('Worker: Failed to fetch URL:', s3_url, response.statusText); + self.postMessage({message: "error", error: "Failed to fetch URL"}); + return + } + const arrayBuffer = await response.arrayBuffer(); + const dataset = dcmjs.data.DicomMessage.readFile(arrayBuffer); + const modality = dicomValue(dataset, "Modality"); + const patientID = dicomValue(dataset, "PatientID"); + const sopInstanceUID = dicomValue(dataset, "SOPInstanceUID"); + const studyInstanceUID = dicomValue(dataset, "StudyInstanceUID"); + const seriesInstanceUID = dicomValue(dataset, "SeriesInstanceUID"); + const seriesDirectory = modality + "_" + seriesInstanceUID; + const filePath = [collection_id, patientID, studyInstanceUID, seriesDirectory].join("/"); + const fileName = sopInstanceUID + ".dcm"; + const blob = new Blob([arrayBuffer], {type: 'application/dicom'}); + const file = new File([blob], fileName, {type: 'application/dicom'}); + const subDirectoryHandle = await createNestedDirectories(directoryHandle, filePath); + const fileHandle = await subDirectoryHandle.getFileHandle(fileName, {create: true,}); + const writable = await fileHandle.createWritable(); + await writable.write(arrayBuffer); + await writable.close(); + self.postMessage({message: "done", path: s3_url, localFilePath: filePath}); + } catch (error) { + console.error("Error when attempting to fetch URL " + s3_url); + console.error(error); + self.postMessage({message: "error", path: s3_url, error: error}); + } + } + `; let downloadWorkers = []; let downloadWorker = null; From 35a4a6e26b5342ff0a7a83dcbd75e3ad734d425d Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Mon, 7 Jul 2025 13:28:00 -0700 Subject: [PATCH 16/52] -> Don't need text.js --- static/js/libs/text.js | 412 ----------------------------------------- 1 file changed, 412 deletions(-) delete mode 100644 static/js/libs/text.js diff --git a/static/js/libs/text.js b/static/js/libs/text.js deleted file mode 100644 index 631926ecf..000000000 --- a/static/js/libs/text.js +++ /dev/null @@ -1,412 +0,0 @@ -/** - * @license text 2.0.16 Copyright jQuery Foundation and other contributors. - * Released under MIT license, http://github.com/requirejs/text/LICENSE - */ -/*jslint regexp: true */ -/*global require, XMLHttpRequest, ActiveXObject, - define, window, process, Packages, - java, location, Components, FileUtils */ - -define(['module'], function (module) { - 'use strict'; - - var text, fs, Cc, Ci, xpcIsWindows, - progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], - xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im, - bodyRegExp = /]*>\s*([\s\S]+)\s*<\/body>/im, - hasLocation = typeof location !== 'undefined' && location.href, - defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''), - defaultHostName = hasLocation && location.hostname, - defaultPort = hasLocation && (location.port || undefined), - buildMap = {}, - masterConfig = (module.config && module.config()) || {}; - - function useDefault(value, defaultValue) { - return value === undefined || value === '' ? defaultValue : value; - } - - //Allow for default ports for http and https. - function isSamePort(protocol1, port1, protocol2, port2) { - if (port1 === port2) { - return true; - } else if (protocol1 === protocol2) { - if (protocol1 === 'http') { - return useDefault(port1, '80') === useDefault(port2, '80'); - } else if (protocol1 === 'https') { - return useDefault(port1, '443') === useDefault(port2, '443'); - } - } - return false; - } - - text = { - version: '2.0.16', - - strip: function (content) { - //Strips declarations so that external SVG and XML - //documents can be added to a document without worry. Also, if the string - //is an HTML document, only the part inside the body tag is returned. - if (content) { - content = content.replace(xmlRegExp, ""); - var matches = content.match(bodyRegExp); - if (matches) { - content = matches[1]; - } - } else { - content = ""; - } - return content; - }, - - jsEscape: function (content) { - return content.replace(/(['\\])/g, '\\$1') - .replace(/[\f]/g, "\\f") - .replace(/[\b]/g, "\\b") - .replace(/[\n]/g, "\\n") - .replace(/[\t]/g, "\\t") - .replace(/[\r]/g, "\\r") - .replace(/[\u2028]/g, "\\u2028") - .replace(/[\u2029]/g, "\\u2029"); - }, - - createXhr: masterConfig.createXhr || function () { - //Would love to dump the ActiveX crap in here. Need IE 6 to die first. - var xhr, i, progId; - if (typeof XMLHttpRequest !== "undefined") { - return new XMLHttpRequest(); - } else if (typeof ActiveXObject !== "undefined") { - for (i = 0; i < 3; i += 1) { - progId = progIds[i]; - try { - xhr = new ActiveXObject(progId); - } catch (e) {} - - if (xhr) { - progIds = [progId]; // so faster next time - break; - } - } - } - - return xhr; - }, - - /** - * Parses a resource name into its component parts. Resource names - * look like: module/name.ext!strip, where the !strip part is - * optional. - * @param {String} name the resource name - * @returns {Object} with properties "moduleName", "ext" and "strip" - * where strip is a boolean. - */ - parseName: function (name) { - var modName, ext, temp, - strip = false, - index = name.lastIndexOf("."), - isRelative = name.indexOf('./') === 0 || - name.indexOf('../') === 0; - - if (index !== -1 && (!isRelative || index > 1)) { - modName = name.substring(0, index); - ext = name.substring(index + 1); - } else { - modName = name; - } - - temp = ext || modName; - index = temp.indexOf("!"); - if (index !== -1) { - //Pull off the strip arg. - strip = temp.substring(index + 1) === "strip"; - temp = temp.substring(0, index); - if (ext) { - ext = temp; - } else { - modName = temp; - } - } - - return { - moduleName: modName, - ext: ext, - strip: strip - }; - }, - - xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/, - - /** - * Is an URL on another domain. Only works for browser use, returns - * false in non-browser environments. Only used to know if an - * optimized .js version of a text resource should be loaded - * instead. - * @param {String} url - * @returns Boolean - */ - useXhr: function (url, protocol, hostname, port) { - var uProtocol, uHostName, uPort, - match = text.xdRegExp.exec(url); - if (!match) { - return true; - } - uProtocol = match[2]; - uHostName = match[3]; - - uHostName = uHostName.split(':'); - uPort = uHostName[1]; - uHostName = uHostName[0]; - - return (!uProtocol || uProtocol === protocol) && - (!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) && - ((!uPort && !uHostName) || isSamePort(uProtocol, uPort, protocol, port)); - }, - - finishLoad: function (name, strip, content, onLoad) { - content = strip ? text.strip(content) : content; - if (masterConfig.isBuild) { - buildMap[name] = content; - } - onLoad(content); - }, - - load: function (name, req, onLoad, config) { - //Name has format: some.module.filext!strip - //The strip part is optional. - //if strip is present, then that means only get the string contents - //inside a body tag in an HTML string. For XML/SVG content it means - //removing the declarations so the content can be inserted - //into the current doc without problems. - - // Do not bother with the work if a build and text will - // not be inlined. - if (config && config.isBuild && !config.inlineText) { - onLoad(); - return; - } - - masterConfig.isBuild = config && config.isBuild; - - var parsed = text.parseName(name), - nonStripName = parsed.moduleName + - (parsed.ext ? '.' + parsed.ext : ''), - url = req.toUrl(nonStripName), - useXhr = (masterConfig.useXhr) || - text.useXhr; - - // Do not load if it is an empty: url - if (url.indexOf('empty:') === 0) { - onLoad(); - return; - } - - //Load the text. Use XHR if possible and in a browser. - if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) { - text.get(url, function (content) { - text.finishLoad(name, parsed.strip, content, onLoad); - }, function (err) { - if (onLoad.error) { - onLoad.error(err); - } - }); - } else { - //Need to fetch the resource across domains. Assume - //the resource has been optimized into a JS module. Fetch - //by the module name + extension, but do not include the - //!strip part to avoid file system issues. - req([nonStripName], function (content) { - text.finishLoad(parsed.moduleName + '.' + parsed.ext, - parsed.strip, content, onLoad); - }, function (err) { - if (onLoad.error) { - onLoad.error(err); - } - }); - } - }, - - write: function (pluginName, moduleName, write, config) { - if (buildMap.hasOwnProperty(moduleName)) { - var content = text.jsEscape(buildMap[moduleName]); - write.asModule(pluginName + "!" + moduleName, - "define(function () { return '" + - content + - "';});\n"); - } - }, - - writeFile: function (pluginName, moduleName, req, write, config) { - var parsed = text.parseName(moduleName), - extPart = parsed.ext ? '.' + parsed.ext : '', - nonStripName = parsed.moduleName + extPart, - //Use a '.js' file name so that it indicates it is a - //script that can be loaded across domains. - fileName = req.toUrl(parsed.moduleName + extPart) + '.js'; - - //Leverage own load() method to load plugin value, but only - //write out values that do not have the strip argument, - //to avoid any potential issues with ! in file names. - text.load(nonStripName, req, function (value) { - //Use own write() method to construct full module value. - //But need to create shell that translates writeFile's - //write() to the right interface. - var textWrite = function (contents) { - return write(fileName, contents); - }; - textWrite.asModule = function (moduleName, contents) { - return write.asModule(moduleName, fileName, contents); - }; - - text.write(pluginName, nonStripName, textWrite, config); - }, config); - } - }; - - if (masterConfig.env === 'node' || (!masterConfig.env && - typeof process !== "undefined" && - process.versions && - !!process.versions.node && - !process.versions['node-webkit'] && - !process.versions['atom-shell'])) { - //Using special require.nodeRequire, something added by r.js. - fs = require.nodeRequire('fs'); - - text.get = function (url, callback, errback) { - try { - var file = fs.readFileSync(url, 'utf8'); - //Remove BOM (Byte Mark Order) from utf8 files if it is there. - if (file[0] === '\uFEFF') { - file = file.substring(1); - } - callback(file); - } catch (e) { - if (errback) { - errback(e); - } - } - }; - } else if (masterConfig.env === 'xhr' || (!masterConfig.env && - text.createXhr())) { - text.get = function (url, callback, errback, headers) { - var xhr = text.createXhr(), header; - xhr.open('GET', url, true); - - //Allow plugins direct access to xhr headers - if (headers) { - for (header in headers) { - if (headers.hasOwnProperty(header)) { - xhr.setRequestHeader(header.toLowerCase(), headers[header]); - } - } - } - - //Allow overrides specified in config - if (masterConfig.onXhr) { - masterConfig.onXhr(xhr, url); - } - - xhr.onreadystatechange = function (evt) { - var status, err; - //Do not explicitly handle errors, those should be - //visible via console output in the browser. - if (xhr.readyState === 4) { - status = xhr.status || 0; - if (status > 399 && status < 600) { - //An http 4xx or 5xx error. Signal an error. - err = new Error(url + ' HTTP status: ' + status); - err.xhr = xhr; - if (errback) { - errback(err); - } - } else { - callback(xhr.responseText); - } - - if (masterConfig.onXhrComplete) { - masterConfig.onXhrComplete(xhr, url); - } - } - }; - xhr.send(null); - }; - } else if (masterConfig.env === 'rhino' || (!masterConfig.env && - typeof Packages !== 'undefined' && typeof java !== 'undefined')) { - //Why Java, why is this so awkward? - text.get = function (url, callback) { - var stringBuffer, line, - encoding = "utf-8", - file = new java.io.File(url), - lineSeparator = java.lang.System.getProperty("line.separator"), - input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)), - content = ''; - try { - stringBuffer = new java.lang.StringBuffer(); - line = input.readLine(); - - // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324 - // http://www.unicode.org/faq/utf_bom.html - - // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK: - // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058 - if (line && line.length() && line.charAt(0) === 0xfeff) { - // Eat the BOM, since we've already found the encoding on this file, - // and we plan to concatenating this buffer with others; the BOM should - // only appear at the top of a file. - line = line.substring(1); - } - - if (line !== null) { - stringBuffer.append(line); - } - - while ((line = input.readLine()) !== null) { - stringBuffer.append(lineSeparator); - stringBuffer.append(line); - } - //Make sure we return a JavaScript string and not a Java string. - content = String(stringBuffer.toString()); //String - } finally { - input.close(); - } - callback(content); - }; - } else if (masterConfig.env === 'xpconnect' || (!masterConfig.env && - typeof Components !== 'undefined' && Components.classes && - Components.interfaces)) { - //Avert your gaze! - Cc = Components.classes; - Ci = Components.interfaces; - Components.utils['import']('resource://gre/modules/FileUtils.jsm'); - xpcIsWindows = ('@mozilla.org/windows-registry-key;1' in Cc); - - text.get = function (url, callback) { - var inStream, convertStream, fileObj, - readData = {}; - - if (xpcIsWindows) { - url = url.replace(/\//g, '\\'); - } - - fileObj = new FileUtils.File(url); - - //XPCOM, you so crazy - try { - inStream = Cc['@mozilla.org/network/file-input-stream;1'] - .createInstance(Ci.nsIFileInputStream); - inStream.init(fileObj, 1, 0, false); - - convertStream = Cc['@mozilla.org/intl/converter-input-stream;1'] - .createInstance(Ci.nsIConverterInputStream); - convertStream.init(inStream, "utf-8", inStream.available(), - Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); - - convertStream.readString(inStream.available(), readData); - convertStream.close(); - inStream.close(); - callback(readData.value); - } catch (e) { - throw new Error((fileObj && fileObj.path || '') + ': ' + e); - } - }; - } - return text; -}); From 0679a6f380de7efecf2f526584cfd24815af0f3d Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Mon, 7 Jul 2025 14:48:50 -0700 Subject: [PATCH 17/52] -> Possible fix for vanishing workers --- static/js/downloader.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index f1e83ec68..8c7d062d3 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -57,7 +57,7 @@ require([ return currentDirectoryHandle; } - importScripts('https://cdn.jsdelivr.net/npm/dcmjs@0.41.0/build/dcmjs.min.js') + importScripts('https://cdn.jsdelivr.net/npm/dcmjs@0.41.0/build/dcmjs.min.js'); function dicomValue(dataset, tagName) { let value = "Undefined-" + tagName; @@ -68,7 +68,7 @@ require([ value = dataset.dict[hexTag].Value; } } - return value + return value; } self.onmessage = async function (event) { @@ -135,7 +135,7 @@ require([ statusMessage(`Worker Error ${JSON.stringify(event)}`, 'error', true); } if (event.data.message === 'done') { - progressUpdate(`Download progress: ${s3_urls.length} remaining, ${event.data.path} downloaded`); + progressUpdate(`Download progress: ${s3_urls.length} remaining...`); } if (s3_urls.length == 0 || thisWorker.downloadCount > workerDownloadThreshold) { finalizeWorker(thisWorker); @@ -165,7 +165,6 @@ require([ function triggerWorkerDownloads() { if (s3_urls.length == 0 && downloadWorkers.length == 0) { - if (workerObjectURL) URL.revokeObjectURL(workerObjectURL); statusMessage(`Downloads complete`, 'info', true); } else { while (s3_urls.length > 0) { From 1135cf45a8463a089c0ec7d6dc1d9ca2710f87e5 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Mon, 7 Jul 2025 15:41:25 -0700 Subject: [PATCH 18/52] -> message box --- static/css/style.css | 8 ++++++++ static/js/base.js | 2 ++ static/js/downloader.js | 16 ++++++++++++---- static/js/utils.js | 40 ++++++++++++++++++++++++++++++++++++++-- templates/base.html | 1 + 5 files changed, 61 insertions(+), 6 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index 199975ded..7dc4f4bba 100755 --- a/static/css/style.css +++ b/static/css/style.css @@ -4509,3 +4509,11 @@ d-topics-list iframe { .collapse-all, .open-all, .download-all-instances { cursor: pointer; } + +#floating-message { + position: fixed; + bottom: 140px; + left: 20px; + right: 12px; + z-index: 9999; +} diff --git a/static/js/base.js b/static/js/base.js index ca832c011..cca038b1f 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -261,6 +261,8 @@ define(['jquery', 'utils'], function($, utils) { // From http://www.regular-expressions.info/email.html email: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, showJsMessage: utils.showJsMessage, + showFloatingMessage: utils.showFloatingMessage, + hideFloatingMessage: utils.hideFloatingMessage, // Simple method for standardizing storage of a message into sessionStorage so it can be retrieved and reloaded // at document load time setReloadMsg: function(type,text) { diff --git a/static/js/downloader.js b/static/js/downloader.js index 8c7d062d3..d4596c223 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -113,7 +113,7 @@ require([ const s3_urls = []; const workerCodeBlob = new Blob([workerCode], { type: 'application/javascript' }); - const workerObjectURL = URL.createObjectURL(workerCodeBlob); + let workerObjectURL = null; const workerDownloadThreshold = 100; const availableWorkers = []; @@ -123,10 +123,10 @@ require([ } function statusMessage(message, type) { - base.showJsMessage(type, message, true); + base.showFloatingMessage(type, message, true); } function progressUpdate(message) { - base.showJsMessage('info', message, true); + base.showFloatingMessage('info', message, true); } function workerOnMessage (event) { @@ -135,7 +135,7 @@ require([ statusMessage(`Worker Error ${JSON.stringify(event)}`, 'error', true); } if (event.data.message === 'done') { - progressUpdate(`Download progress: ${s3_urls.length} remaining...`); + progressUpdate(`Download progress: ${s3_urls.length} remaining, ${event.data.path} downloaded`); } if (s3_urls.length == 0 || thisWorker.downloadCount > workerDownloadThreshold) { finalizeWorker(thisWorker); @@ -165,8 +165,16 @@ require([ function triggerWorkerDownloads() { if (s3_urls.length == 0 && downloadWorkers.length == 0) { + // cleanup + if (workerObjectURL) { + URL.revokeObjectURL(workerObjectURL); + workerObjectURL = null; + } statusMessage(`Downloads complete`, 'info', true); } else { + if(!workerObjectURL){ + workerObjectURL = URL.createObjectURL(workerCodeBlob); + } while (s3_urls.length > 0) { let targetWorker = null; if (availableWorkers.length > 0) { diff --git a/static/js/utils.js b/static/js/utils.js index 813afad18..d1c5066cc 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -109,7 +109,7 @@ define(['jquery'], function($) { function _showJsMessage(type,text,withEmpty,rootSelector, add_classes) { rootSelector = rootSelector || '#js-messages'; withEmpty && $(rootSelector).empty(); - var msg = ""; + let msg = ""; if (text instanceof Array) { for (var i = 0; i < text.length; i++) { msg += text[i] + '
'; @@ -131,7 +131,41 @@ define(['jquery'], function($) { ) ); return uuid; - }; + } + + // A method for displaying a special floating message box during worker thread activity. Note there is only + // ever one floating message box + function _showFloatingMessage(type, contents, withEmpty, add_classes) { + let msgBox = $('#floating-message'); + withEmpty && msgBox.empty(); + let msg = ""; + if (contents instanceof Array) { + for (let i = 0; i < contents.length; i++) { + msg += contents[i] + '
'; + } + } else { + msg = contents; + } + msgBox.append( + $('
') + .addClass(`alert alert-${type} alert-dismissible`) + .html(msg) + .prepend( + '' + ) + ); + msgBox.show(); + } + + function _hideFloatingMessage(withEmpty) { + withEmpty && $('#floating-message').empty(); + $('#floating-message').hide(); + } + + $('body').on('click', '#floating-message .close-msg-box', function(){ + _hideFloatingMessage(true); + }); const MAX_ELAPSED = 240000; function _checkManifestReady(file_name, check_start) { @@ -198,6 +232,8 @@ define(['jquery'], function($) { return { showJsMessage: _showJsMessage, + showFloatingMessage: _showFloatingMessage, + hideFloatingMessage: _hideFloatingMessage, // Block re-requests of requests which can't be handled via AJAX (eg. file downloads) // Uses cookie polling // Request provides a parameter with a key of expectedCookie and a value of downloadToken diff --git a/templates/base.html b/templates/base.html index 8e886c4eb..9f2a410e2 100755 --- a/templates/base.html +++ b/templates/base.html @@ -106,6 +106,7 @@
{% endif %}
+
From 9c71aa52a0b81b49754d8524f965724e9925e597 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Tue, 8 Jul 2025 10:12:24 -0700 Subject: [PATCH 19/52] -> Remove dcmjs --- static/js/downloader.js | 60 ++++++++++++++++++++--------------------- static/js/tables.js | 9 ++++++- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index d4596c223..7798d1814 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -57,25 +57,17 @@ require([ return currentDirectoryHandle; } - importScripts('https://cdn.jsdelivr.net/npm/dcmjs@0.41.0/build/dcmjs.min.js'); - - function dicomValue(dataset, tagName) { - let value = "Undefined-" + tagName; - const entry = dcmjs.data.DicomMetaDictionary.nameMap[tagName]; - if (entry && entry.tag) { - const hexTag = entry.tag.replace("(", "").replace(",", "").replace(")", ""); - if (hexTag in dataset.dict) { - value = dataset.dict[hexTag].Value; - } - } - return value; - } - self.onmessage = async function (event) { - const s3_url = event.data.url; + const s3_url = event.data['url']; + const metadata = event.data['metadata']; + const modality = metadata['modality']; + const patientID = metadata['patient']; + const collection_id = metadata['collection']; + const studyInstanceUID = metadata['study']; + const seriesInstanceUID = metadata['series']; + const fileName = metadata['instance']; try { - const directoryHandle = event.data.directoryHandle; - const collection_id = event.data.collection_id || 'unknown_collection'; + const directoryHandle = event.data['directoryHandle']; response = await fetch(s3_url) if (!response.ok) { console.error('Worker: Failed to fetch URL:', s3_url, response.statusText); @@ -83,19 +75,12 @@ require([ return } const arrayBuffer = await response.arrayBuffer(); - const dataset = dcmjs.data.DicomMessage.readFile(arrayBuffer); - const modality = dicomValue(dataset, "Modality"); - const patientID = dicomValue(dataset, "PatientID"); - const sopInstanceUID = dicomValue(dataset, "SOPInstanceUID"); - const studyInstanceUID = dicomValue(dataset, "StudyInstanceUID"); - const seriesInstanceUID = dicomValue(dataset, "SeriesInstanceUID"); const seriesDirectory = modality + "_" + seriesInstanceUID; const filePath = [collection_id, patientID, studyInstanceUID, seriesDirectory].join("/"); - const fileName = sopInstanceUID + ".dcm"; const blob = new Blob([arrayBuffer], {type: 'application/dicom'}); const file = new File([blob], fileName, {type: 'application/dicom'}); const subDirectoryHandle = await createNestedDirectories(directoryHandle, filePath); - const fileHandle = await subDirectoryHandle.getFileHandle(fileName, {create: true,}); + const fileHandle = await subDirectoryHandle.getFileHandle(fileName, {create: true}); const writable = await fileHandle.createWritable(); await writable.write(arrayBuffer); await writable.close(); @@ -132,7 +117,7 @@ require([ function workerOnMessage (event) { let thisWorker = event.target; if (event.data.message === 'error') { - statusMessage(`Worker Error ${JSON.stringify(event)}`, 'error', true); + statusMessage(`Worker Error: ${JSON.stringify(event)}`, 'error'); } if (event.data.message === 'done') { progressUpdate(`Download progress: ${s3_urls.length} remaining, ${event.data.path} downloaded`); @@ -187,7 +172,7 @@ require([ } } const s3_url = s3_urls.pop(); - targetWorker.postMessage({ url: s3_url, collection_id: collection_id, directoryHandle: directoryHandle }); + targetWorker.postMessage({ 'url': s3_url['url'], 'metadata': s3_url, 'directoryHandle': directoryHandle }); } } } @@ -246,12 +231,27 @@ require([ } $('.container-fluid').on('click', '.download-all-instances', function(){ - let bucket = $(this).attr('data-bucket'); - let crdc_series_id = $(this).attr('data-series'); + const bucket = $(this).attr('data-bucket'); + const crdc_series_id = $(this).attr('data-series'); + const series_id = $(this).attr('data-series-id'); + const collection_id = $(this).attr('data-collection'); + const study_id = $(this).attr('data-study'); + const modality = $(this).attr('data-modality'); + const patient_id = $(this).attr('data-patient'); getAllS3ObjectKeys(bucket, "us-east-1", crdc_series_id).then( keys => { keys.forEach((key) => { if (key !== "") { - s3_urls.push(`https://${bucket}.s3.us-east-1.amazonaws.com/${key}`); + const keys = key.split("/"); + const instance = keys[keys.length-1]; + s3_urls.push({ + 'url': `https://${bucket}.s3.us-east-1.amazonaws.com/${key}`, + 'study': study_id, + 'collection': collection_id, + 'series': series_id, + 'modality': modality, + 'instance': instance, + 'patient': patient_id + }); } }); if(s3_urls.length <= 0) { diff --git a/static/js/tables.js b/static/js/tables.js index d56c6a35c..8657c2061 100644 --- a/static/js/tables.js +++ b/static/js/tables.js @@ -1330,7 +1330,14 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, "orderable": false, data: 'SeriesInstanceUID', render: function (data, type, row){ if("showDirectoryPicker" in window) { - return `` + return `` } return `` } From 90cb5cb651820945138fa67273a42ba80b31a9a2 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Tue, 8 Jul 2025 15:17:11 -0700 Subject: [PATCH 20/52] -> Removed repetitive table destroy statements due to possible race condition --- static/js/tables.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/static/js/tables.js b/static/js/tables.js index 8657c2061..6f58f83da 100644 --- a/static/js/tables.js +++ b/static/js/tables.js @@ -86,7 +86,6 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, // Update the rows in the Projects Table, clear the other tables. window.updateTablesAfterFilter = function (collFilt, collectionsData, collectionStats,cartStats){ - var usedCollectionData = new Array(); var hasColl = collFilt.length>0 ? true : false; @@ -107,14 +106,9 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, updateProjectTable(usedCollectionData,collectionStats, cartStats); initializeTableViewedItemsData(); initializeTableCacheData(); - $('#cases_tab').DataTable().destroy(); - $('#studies_tab').DataTable().destroy(); - $('#series_tab').DataTable().destroy(); updateCaseTable(false,""); updateStudyTable(false,""); updateSeriesTable(false,""); - - } // initialize cases, studies, and series cache data @@ -1119,6 +1113,8 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, } else { pageRows = 10; } + console.debug($('#series_tab')); + console.debug($('#series_tab').DataTable()); $('#series_tab').DataTable().destroy(); try { $('#series_tab').DataTable({ From 0c52da186a23b86e6925435c1dcb8b7e743b0738 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Wed, 9 Jul 2025 13:34:29 -0700 Subject: [PATCH 21/52] -> Status box is more configurable, can be dragged -> Cancellation option --- static/css/style.css | 12 ++++++---- static/js/base.js | 2 ++ static/js/downloader.js | 52 +++++++++++++++++++++++++++++++---------- static/js/utils.js | 47 +++++++++++++++++++++++-------------- 4 files changed, 80 insertions(+), 33 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index 7dc4f4bba..3284bbf35 100755 --- a/static/css/style.css +++ b/static/css/style.css @@ -1786,7 +1786,7 @@ a.skip-nav { background-color: #F8F8F8; border-color: #76b876; } .alert.alert-info { - color: #2d81c4; + color: #1c5c8d; background-color: #F8F8F8; border-color: #1976D2; } .alert.alert-warning { @@ -1795,6 +1795,10 @@ a.skip-nav { .alert.alert-error { background-color: #F8F8F8; border-color: #D32F2F; } + .alert.alert-message { + background-color: #F8F8F8; + border-color: #47435b; + } .alert a { font-weight: 600; @@ -4512,8 +4516,8 @@ d-topics-list iframe { #floating-message { position: fixed; - bottom: 140px; - left: 20px; - right: 12px; + top: 180px; + width: 300px; + right: 40px; z-index: 9999; } diff --git a/static/js/base.js b/static/js/base.js index cca038b1f..a2af11e60 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -214,6 +214,8 @@ require([ sessionStorage.removeItem("user-manifest"); }); + $('#floating-message').draggable(); + $(document).ready(function(){ if(sessionStorage.getItem("reloadMsg")) { var msg = JSON.parse(sessionStorage.getItem("reloadMsg")); diff --git a/static/js/downloader.js b/static/js/downloader.js index 7798d1814..bd0666fdf 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -93,9 +93,17 @@ require([ } `; + let cancel_button = ` + + `; + let close_button = ` + + `; + let downloadWorkers = []; let downloadWorker = null; const s3_urls = []; + let pending_cancellation = false; const workerCodeBlob = new Blob([workerCode], { type: 'application/javascript' }); let workerObjectURL = null; @@ -107,22 +115,28 @@ require([ downloadWorkers = downloadWorkers.filter(w => w !== worker); } - function statusMessage(message, type) { - base.showFloatingMessage(type, message, true); + // Replaces the current floating message contents with a new message, including a new icon if provided + function statusMessage(message, type, icon, withClose, withCancel) { + let buttons = []; + withCancel && buttons.push(cancel_button); + withClose && buttons.push(close_button); + base.showFloatingMessage(type, message, true, null, icon, buttons); } - function progressUpdate(message) { - base.showFloatingMessage('info', message, true); + + // Updates the current floating message contents and display class + function progressUpdate(message, type) { + base.showFloatingMessage(type, message, false); } function workerOnMessage (event) { let thisWorker = event.target; if (event.data.message === 'error') { - statusMessage(`Worker Error: ${JSON.stringify(event)}`, 'error'); + statusMessage(`Worker Error: ${JSON.stringify(event)}`, 'error', null, true, false); } if (event.data.message === 'done') { - progressUpdate(`Download progress: ${s3_urls.length} remaining, ${event.data.path} downloaded`); + progressUpdate(`Download progress: ${s3_urls.length} remaining...`); } - if (s3_urls.length == 0 || thisWorker.downloadCount > workerDownloadThreshold) { + if (s3_urls.length == 0 || thisWorker.downloadCount > workerDownloadThreshold || pending_cancellation) { finalizeWorker(thisWorker); } else { thisWorker.downloadCount += 1; @@ -136,8 +150,8 @@ require([ downloadWorker.onmessage = workerOnMessage; downloadWorker.onerror = function(event) { let thisWorker = event.target - console.error('Main: Error in worker:', event.message || "No message given", event); - statusMessage(`Error in worker: ${event.message}`, 'error', true); + console.error('[Main] Error in worker:', event.message || "No message given", event); + statusMessage(`[Worker] Error in worker: ${event.message}`, 'error', null, true, false); finalizeWorker(thisWorker); } downloadWorkers.downloadCount = 0; @@ -155,7 +169,10 @@ require([ URL.revokeObjectURL(workerObjectURL); workerObjectURL = null; } - statusMessage(`Downloads complete`, 'info', true); + let msg = pending_cancellation ? 'Download cancelled.' : `Download complete.`; + let type = pending_cancellation ? 'warning' : 'info'; + statusMessage(msg, type, null, true, false); + pending_cancellation = false; } else { if(!workerObjectURL){ workerObjectURL = URL.createObjectURL(workerCodeBlob); @@ -230,6 +247,13 @@ require([ } } + $('.container-fluid').on('click', '.cancel-download', function(){ + pending_cancellation = true; + $('.cancel-download').hide(); + $('.close-message-window').show(); + s3_urls.splice(0, s3_urls.length); + }); + $('.container-fluid').on('click', '.download-all-instances', function(){ const bucket = $(this).attr('data-bucket'); const crdc_series_id = $(this).attr('data-series'); @@ -255,11 +279,15 @@ require([ } }); if(s3_urls.length <= 0) { - statusMessage('Error while parsing instance list!', 'error'); + statusMessage('Error while parsing instance list!', 'error', null, true, false); return; } beginDownload().then( - function(){statusMessage("Download underway.", 'info');} + function(){ + $('.cancel-download').show(); + $('.close-message-window').hide(); + statusMessage("Download underway.", 'message', '', false, true); + } ); }); }); diff --git a/static/js/utils.js b/static/js/utils.js index d1c5066cc..c1d95dd0b 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -40,7 +40,7 @@ require.config({ }); // Return an object for consts/methods used by most views -define(['jquery'], function($) { +define(['jquery', 'jqueryui'], function($, jqueryui) { // Download block poll with cookie via StackOverflow: // https://stackoverflow.com/questions/1106377/detect-when-browser-receives-file-download @@ -135,26 +135,39 @@ define(['jquery'], function($) { // A method for displaying a special floating message box during worker thread activity. Note there is only // ever one floating message box - function _showFloatingMessage(type, contents, withEmpty, add_classes) { + // If withEmpty is true, the message is assumed to replace the currently visible contents of the message box proper. + // If withEmpty is false, the message is assumed to replace the .contents element + // If withEmpty is false and the type is not null and isn't found on the alert subelement, the subelement's classes + // will be changed to match the new type + function _showFloatingMessage(type, contents, withEmpty, add_classes, icon, controls) { let msgBox = $('#floating-message'); withEmpty && msgBox.empty(); - let msg = ""; - if (contents instanceof Array) { - for (let i = 0; i < contents.length; i++) { - msg += contents[i] + '
'; - } + let msg = contents instanceof Array ? contents.join("
") : contents; + controls = controls instanceof Array ? controls.join(" ") : controls; + icon = icon || ""; + if(withEmpty || msgBox.find('.alert-dismissible').length <= 0) { + msgBox.append( + $('
') + .addClass(`alert alert-${type} alert-dismissible`) + .html( + ` +

${msg} ${icon}

+

${controls}

+ ` + ) + .prepend( + '' + ) + ); } else { - msg = contents; + let alert_box = msgBox.find('.alert-dismissible'); + alert_box.find('.contents').html(msg); + if(type && !alert_box.hasClass(`alert-${type}`)) { + alert_box.removeClass(); + alert_box.addClass(`alert alert-dismissible alert-${type}`); + } } - msgBox.append( - $('
') - .addClass(`alert alert-${type} alert-dismissible`) - .html(msg) - .prepend( - '' - ) - ); msgBox.show(); } From 9752f1b6455dd79757ae239cc18247cd907eddbd Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Mon, 14 Jul 2025 13:19:58 -0700 Subject: [PATCH 22/52] -> Cleaner cancellations -> Cancellation of workers should no longer spam up the logs -> Better UI for pending downloads --- static/js/downloader.js | 63 +++++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index bd0666fdf..1d129a2d4 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -30,6 +30,14 @@ require([ ], function (base, $) { let workerCode = ` + const abort_controller = new AbortController(); + let pending_abort = false; + let working = false; + function abortFetch(msg) { + abort_controller.abort(msg || "Fetch aborted."); + pending_abort = true; + }; + async function createNestedDirectories(topLevelDirectoryHandle, path) { const pathSegments = path.split('/').filter((segment) => segment !== ''); let currentDirectoryHandle = topLevelDirectoryHandle; @@ -58,6 +66,11 @@ require([ } self.onmessage = async function (event) { + if(event.data['abort'] && working) { + abortFetch(event.data['reason']); + return; + } + working = true; const s3_url = event.data['url']; const metadata = event.data['metadata']; const modality = metadata['modality']; @@ -68,11 +81,17 @@ require([ const fileName = metadata['instance']; try { const directoryHandle = event.data['directoryHandle']; - response = await fetch(s3_url) + response = await fetch(s3_url, { + signal: abort_controller.signal + }); if (!response.ok) { - console.error('Worker: Failed to fetch URL:', s3_url, response.statusText); - self.postMessage({message: "error", error: "Failed to fetch URL"}); - return + if(pending_abort) { + console.log('User aborted downloads'); + } else { + console.error('[Worker] Failed to fetch URL: '+s3_url, response.statusText); + self.postMessage({message: "error", error: "Failed to fetch URL"}); + } + return; } const arrayBuffer = await response.arrayBuffer(); const seriesDirectory = modality + "_" + seriesInstanceUID; @@ -85,10 +104,18 @@ require([ await writable.write(arrayBuffer); await writable.close(); self.postMessage({message: "done", path: s3_url, localFilePath: filePath}); + working = false; } catch (error) { - console.error("Error when attempting to fetch URL " + s3_url); - console.error(error); - self.postMessage({message: "error", path: s3_url, error: error}); + let msg = error.name || "Unnamed Error" + " when attempting to fetch URL " + s3_url; + if(error.name === "AbortError" || (error.name === undefined && pending_abort)) { + msg = "Fetch was aborted. The user may have cancelled their downloads."; + working && console.log(msg); + } else { + console.error(msg); + console.error(error); + } + self.postMessage({message: 'error', path: s3_url, error: error, 'text': msg}); + working = false; } } `; @@ -130,13 +157,22 @@ require([ function workerOnMessage (event) { let thisWorker = event.target; - if (event.data.message === 'error') { - statusMessage(`Worker Error: ${JSON.stringify(event)}`, 'error', null, true, false); + let true_error = event.data.message === 'error' && event.data.error.name !== "AbortError"; + let cancellation = event.data.message === 'error' && (pending_cancellation || event.data.error.name === "AbortError"); + if(true_error) { + console.error(`Worker Error: ${JSON.stringify(event)}`); + statusMessage(`Encountered an error while downloading these files.`, 'error', null, true, false); } - if (event.data.message === 'done') { - progressUpdate(`Download progress: ${s3_urls.length} remaining...`); + if (event.data.message === 'done' || cancellation) { + let in_progress = downloadWorkers.length - availableWorkers.length; + let msg = `Download status: ${in_progress} files in progress, ${s3_urls.length} in queue...`; + if(s3_urls.length <= 0) { + // This means the remaining downloads are all in-progress, or we cancelled + msg = cancellation ? "Cleaning up cancelled downloads..." : `Download status: ${in_progress} file(s) in progress...`; + } + progressUpdate(msg); } - if (s3_urls.length == 0 || thisWorker.downloadCount > workerDownloadThreshold || pending_cancellation) { + if (s3_urls.length == 0 || (thisWorker.downloadCount > workerDownloadThreshold) || pending_cancellation) { finalizeWorker(thisWorker); } else { thisWorker.downloadCount += 1; @@ -252,6 +288,9 @@ require([ $('.cancel-download').hide(); $('.close-message-window').show(); s3_urls.splice(0, s3_urls.length); + downloadWorkers.forEach(worker => { + worker.postMessage({'abort': true, 'reason': 'User cancelled download.'}); + }); }); $('.container-fluid').on('click', '.download-all-instances', function(){ From 95b65cff0d62d74bb0b2b556a6b9339eefee6647 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Mon, 14 Jul 2025 18:13:04 -0700 Subject: [PATCH 23/52] -> Delete test route --- idc/urls.py | 1 - idc/views.py | 5 ----- 2 files changed, 6 deletions(-) diff --git a/idc/urls.py b/idc/urls.py index 04cbeb04f..0d96df712 100644 --- a/idc/urls.py +++ b/idc/urls.py @@ -53,7 +53,6 @@ re_path(r'^studymp/', views.studymp, name='studymp'), re_path(r'^warning/', views.warn_page, name='warn'), re_path(r'^about/', views.about_page, name='about_page'), - re_path(r'^test(.*)/', views.test_page, name='test_page'), re_path(r'^dashboard/', views.dashboard_page, name='dashboard'), re_path(r'^extended_login/$', views.extended_login_view, name='extended_login'), re_path(r'^privacy/', views.privacy_policy, name='privacy'), diff --git a/idc/views.py b/idc/views.py index 3dcc905d9..3389bdcd0 100644 --- a/idc/views.py +++ b/idc/views.py @@ -1098,11 +1098,6 @@ def cart_data(request): return JsonResponse(response, status=status) -def test_page(request, mtch): - pg=request.path[:-1]+'.html' - return render(request, 'idc'+pg) - - # User dashboard, where saved cohorts (and, in the future, uploaded/indexed data) are listed @login_required def dashboard_page(request): From f0d28052c10a799ede749c191fc110fc78dcd1a1 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Wed, 16 Jul 2025 16:31:52 -0700 Subject: [PATCH 24/52] -> Refactor to allow a continuously worked on queue --- static/js/downloader.js | 642 +++++++++++++++++++++++----------------- 1 file changed, 376 insertions(+), 266 deletions(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index 1d129a2d4..e55e85ae0 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -29,305 +29,415 @@ require([ 'base', 'jquery' ], function (base, $) { - let workerCode = ` - const abort_controller = new AbortController(); - let pending_abort = false; - let working = false; - function abortFetch(msg) { - abort_controller.abort(msg || "Fetch aborted."); - pending_abort = true; - }; - - async function createNestedDirectories(topLevelDirectoryHandle, path) { - const pathSegments = path.split('/').filter((segment) => segment !== ''); - let currentDirectoryHandle = topLevelDirectoryHandle; - for (const segment of pathSegments) { + class DownloadRequest { + region = "us-east-1"; + + constructor(request) { + this.study_id = request['study_id']; + this.collection_id = request['collection_id']; + this.series_id = request['series_id']; + this.modality = request['modality']; + this.patient_id = request['patient_id']; + this.bucket = request['bucket']; + this.crdc_series_id = request['crdc_series_id']; + this.directory = request['directory']; + } + + async getAllS3ObjectKeys() { + const allKeys = []; + let isTruncated = true; + let continuationToken = null; + while (isTruncated) { + let url = `https://${this.bucket}.s3.${this.region}.amazonaws.com/?list-type=2`; + if (this.crdc_series_id) { + url += `&prefix=${encodeURIComponent(this.crdc_series_id)}`; + } + if (continuationToken) { + url += `&continuation-token=${encodeURIComponent(continuationToken)}`; + } try { - // Attempt to get the directory handle without creating it - const entry = await currentDirectoryHandle.getDirectoryHandle(segment, { - create: false - }) - currentDirectoryHandle = entry; - } catch (error) { - // If the error is specifically about the directory not existing, create it - if (error.name === 'NotFoundError') { - const entry = await currentDirectoryHandle.getDirectoryHandle(segment, { - create: true - }) - currentDirectoryHandle = entry; + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP Error ${response.status}: ${response.statusText}`); + } + const xmlText = await response.text(); + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(xmlText, "application/xml"); + const errorCode = xmlDoc.getElementsByTagName('Code')[0]?.textContent; + if (errorCode) { + const errorMessage = xmlDoc.getElementsByTagName('Message')[0]?.textContent; + throw new Error(`S3 API Error: ${errorCode} - ${errorMessage}`); + } + const keyElements = xmlDoc.getElementsByTagName('Key'); + const keysOnPage = Array.from(keyElements).map(el => el.textContent); + allKeys.push(...keysOnPage); + isTruncated = xmlDoc.getElementsByTagName('IsTruncated')[0]?.textContent === 'true'; + if (isTruncated) { + continuationToken = xmlDoc.getElementsByTagName('NextContinuationToken')[0]?.textContent; } else { - // TODO: Handle other potential errors (e.g., name conflicts) - return false; // Indicate failure + continuationToken = null; } + } catch (error) { + console.error("Failed to fetch S3 data:", error); + throw error; // Re-throw the error to be handled by the caller } } - // Return the last directory handle - return currentDirectoryHandle; + return allKeys; } - - self.onmessage = async function (event) { - if(event.data['abort'] && working) { - abortFetch(event.data['reason']); - return; - } - working = true; - const s3_url = event.data['url']; - const metadata = event.data['metadata']; - const modality = metadata['modality']; - const patientID = metadata['patient']; - const collection_id = metadata['collection']; - const studyInstanceUID = metadata['study']; - const seriesInstanceUID = metadata['series']; - const fileName = metadata['instance']; - try { - const directoryHandle = event.data['directoryHandle']; - response = await fetch(s3_url, { - signal: abort_controller.signal - }); - if (!response.ok) { - if(pending_abort) { - console.log('User aborted downloads'); - } else { - console.error('[Worker] Failed to fetch URL: '+s3_url, response.statusText); - self.postMessage({message: "error", error: "Failed to fetch URL"}); + + async loadAllKeys() { + const s3_urls = []; + await this.getAllS3ObjectKeys().then(keys => { + keys.forEach((key) => { + if (key !== "") { + const keys = key.split("/"); + const instance = keys[keys.length - 1]; + s3_urls.push({ + 'url': `https://${this.bucket}.s3.us-east-1.amazonaws.com/${key}`, + 'study': this.study_id, + 'collection': this.collection_id, + 'series': this.series_id, + 'modality': this.modality, + 'instance': instance, + 'patient': this.patient_id, + 'directory': this.directory + }); } - return; - } - const arrayBuffer = await response.arrayBuffer(); - const seriesDirectory = modality + "_" + seriesInstanceUID; - const filePath = [collection_id, patientID, studyInstanceUID, seriesDirectory].join("/"); - const blob = new Blob([arrayBuffer], {type: 'application/dicom'}); - const file = new File([blob], fileName, {type: 'application/dicom'}); - const subDirectoryHandle = await createNestedDirectories(directoryHandle, filePath); - const fileHandle = await subDirectoryHandle.getFileHandle(fileName, {create: true}); - const writable = await fileHandle.createWritable(); - await writable.write(arrayBuffer); - await writable.close(); - self.postMessage({message: "done", path: s3_url, localFilePath: filePath}); - working = false; - } catch (error) { - let msg = error.name || "Unnamed Error" + " when attempting to fetch URL " + s3_url; - if(error.name === "AbortError" || (error.name === undefined && pending_abort)) { - msg = "Fetch was aborted. The user may have cancelled their downloads."; - working && console.log(msg); - } else { - console.error(msg); - console.error(error); - } - self.postMessage({message: 'error', path: s3_url, error: error, 'text': msg}); - working = false; - } - } - `; - - let cancel_button = ` - - `; - let close_button = ` - - `; - - let downloadWorkers = []; - let downloadWorker = null; - const s3_urls = []; - let pending_cancellation = false; - - const workerCodeBlob = new Blob([workerCode], { type: 'application/javascript' }); - let workerObjectURL = null; - const workerDownloadThreshold = 100; - const availableWorkers = []; - - function finalizeWorker(worker) { - worker.terminate(); - downloadWorkers = downloadWorkers.filter(w => w !== worker); - } + }); + }); + return s3_urls; + } - // Replaces the current floating message contents with a new message, including a new icon if provided - function statusMessage(message, type, icon, withClose, withCancel) { - let buttons = []; - withCancel && buttons.push(cancel_button); - withClose && buttons.push(close_button); - base.showFloatingMessage(type, message, true, null, icon, buttons); } - // Updates the current floating message contents and display class - function progressUpdate(message, type) { - base.showFloatingMessage(type, message, false); + class DownloadQueueManager { + WORKING_QUEUE_LIMIT = 2000; + HOPPER_LIMIT = 500; + + hopper = []; + working_queue = []; + + get pending() { + return this.working_queue.length + this.hopper.length; + } + + async _update_queue() { + if(this.working_queue.length < this.WORKING_QUEUE_LIMIT && this.hopper.length > 0) { + let request = this.hopper.pop(); + await request.loadAllKeys().then(keys => { + this.working_queue.push(...keys); + }); + } + } + + load(request) { + let request_success = false; + if(this.hopper.length < this.HOPPER_LIMIT) { + this.hopper.push(new DownloadRequest(request)); + request_success = true; + } + return request_success; + } + + isEmpty() { + return (this.working_queue.length <= 0 && this.hopper.length <= 0); + } + + async get_download_item() { + await this._update_queue(); + if(this.working_queue.length > 0) { + return this.working_queue.pop(); + } + return null; + } + + emptyQueues() { + this.working_queue.slice(0,this.working_queue.length); + this.hopper.slice(0,this.hopper.length); + } } - function workerOnMessage (event) { - let thisWorker = event.target; - let true_error = event.data.message === 'error' && event.data.error.name !== "AbortError"; - let cancellation = event.data.message === 'error' && (pending_cancellation || event.data.error.name === "AbortError"); - if(true_error) { - console.error(`Worker Error: ${JSON.stringify(event)}`); - statusMessage(`Encountered an error while downloading these files.`, 'error', null, true, false); - } - if (event.data.message === 'done' || cancellation) { - let in_progress = downloadWorkers.length - availableWorkers.length; - let msg = `Download status: ${in_progress} files in progress, ${s3_urls.length} in queue...`; - if(s3_urls.length <= 0) { - // This means the remaining downloads are all in-progress, or we cancelled - msg = cancellation ? "Cleaning up cancelled downloads..." : `Download status: ${in_progress} file(s) in progress...`; - } - progressUpdate(msg); - } - if (s3_urls.length == 0 || (thisWorker.downloadCount > workerDownloadThreshold) || pending_cancellation) { - finalizeWorker(thisWorker); - } else { - thisWorker.downloadCount += 1; - availableWorkers.push(thisWorker); - } - triggerWorkerDownloads(); + function workerOnMessage(event) { + let thisWorker = event.target; + let true_error = event.data.message === 'error' && event.data.error.name !== "AbortError"; + let cancellation = event.data.message === 'error' && (downloader_manager.pending_cancellation || event.data.error.name === "AbortError"); + if (true_error) { + console.error(`Worker Error: ${JSON.stringify(event)}`); + downloader_manager.statusMessage(`Encountered an error while downloading these files.`, 'error', null, true, false); + } + if (event.data.message === 'done' || cancellation) { + let msg = `Download status: ${downloader_manager.in_progress} files in progress, ${downloader_manager.queues.pending} in queue...`; + if (downloader_manager.queues.isEmpty()) { + // This means the remaining downloads are all in-progress, or we cancelled + msg = cancellation ? "Cleaning up cancelled downloads..." : `Download status: ${downloader_manager.in_progress} file(s) in progress...`; + } + downloader_manager.progressUpdate(msg); + } + if (downloader_manager.queues.isEmpty() || (thisWorker.downloadCount > downloader_manager.workerDownloadThreshold) || downloader_manager.pending_cancellation) { + downloader_manager.finalizeWorker(thisWorker); + } else { + thisWorker.downloadCount += 1; + downloader_manager.availableWorkers.push(thisWorker); + } + downloader_manager.triggerWorkerDownloads(); } - function allocateWorker() { - downloadWorker = new Worker(workerObjectURL); - downloadWorker.onmessage = workerOnMessage; - downloadWorker.onerror = function(event) { + function workerOnError(event) { let thisWorker = event.target console.error('[Main] Error in worker:', event.message || "No message given", event); - statusMessage(`[Worker] Error in worker: ${event.message}`, 'error', null, true, false); - finalizeWorker(thisWorker); - } - downloadWorkers.downloadCount = 0; - downloadWorkers.push(downloadWorker); - return downloadWorker + downloader_manager.statusMessage(`[Worker] Error in worker: ${event.message}`, 'error', null, true, false); + downloader_manager.finalizeWorker(thisWorker); } - let workerLimit = navigator.hardwareConcurrency; - let directoryHandle = null; + class DownloadManager { + // Text blobs + workerCode = ` + const abort_controller = new AbortController(); + let pending_abort = false; + let working = false; + function abortFetch(msg) { + abort_controller.abort(msg || "Fetch aborted."); + pending_abort = true; + }; + + async function createNestedDirectories(topLevelDirectoryHandle, path) { + const pathSegments = path.split('/').filter((segment) => segment !== ''); + let currentDirectoryHandle = topLevelDirectoryHandle; + for (const segment of pathSegments) { + try { + // Attempt to get the directory handle without creating it + const entry = await currentDirectoryHandle.getDirectoryHandle(segment, { + create: false + }) + currentDirectoryHandle = entry; + } catch (error) { + // If the error is specifically about the directory not existing, create it + if (error.name === 'NotFoundError') { + const entry = await currentDirectoryHandle.getDirectoryHandle(segment, { + create: true + }) + currentDirectoryHandle = entry; + } else { + // TODO: Handle other potential errors (e.g., name conflicts) + return false; // Indicate failure + } + } + } + // Return the last directory handle + return currentDirectoryHandle; + } + + self.onmessage = async function (event) { + if(event.data['abort'] && working) { + abortFetch(event.data['reason']); + return; + } + working = true; + const s3_url = event.data['url']; + const metadata = event.data['metadata']; + const modality = metadata['modality']; + const patientID = metadata['patient']; + const collection_id = metadata['collection']; + const studyInstanceUID = metadata['study']; + const seriesInstanceUID = metadata['series']; + const fileName = metadata['instance']; + try { + const directoryHandle = event.data['directoryHandle']; + response = await fetch(s3_url, { + signal: abort_controller.signal + }); + if (!response.ok) { + if(pending_abort) { + console.log('User aborted downloads'); + } else { + console.error('[Worker] Failed to fetch URL: '+s3_url, response.statusText); + self.postMessage({message: "error", error: "Failed to fetch URL"}); + } + return; + } + const arrayBuffer = await response.arrayBuffer(); + const seriesDirectory = modality + "_" + seriesInstanceUID; + const filePath = [collection_id, patientID, studyInstanceUID, seriesDirectory].join("/"); + const blob = new Blob([arrayBuffer], {type: 'application/dicom'}); + const file = new File([blob], fileName, {type: 'application/dicom'}); + const subDirectoryHandle = await createNestedDirectories(directoryHandle, filePath); + const fileHandle = await subDirectoryHandle.getFileHandle(fileName, {create: true}); + const writable = await fileHandle.createWritable(); + await writable.write(arrayBuffer); + await writable.close(); + self.postMessage({message: "done", path: s3_url, localFilePath: filePath}); + working = false; + } catch (error) { + let msg = error.name || "Unnamed Error" + " when attempting to fetch URL " + s3_url; + if(error.name === "AbortError" || (error.name === undefined && pending_abort)) { + msg = "Fetch was aborted. The user may have cancelled their downloads."; + working && console.log(msg); + } else { + console.error(msg); + console.error(error); + } + self.postMessage({message: 'error', path: s3_url, error: error, 'text': msg}); + working = false; + } + } + `; + cancel_button = ` + + `; + close_button = ` + + `; - function triggerWorkerDownloads() { - if (s3_urls.length == 0 && downloadWorkers.length == 0) { - // cleanup - if (workerObjectURL) { - URL.revokeObjectURL(workerObjectURL); - workerObjectURL = null; - } - let msg = pending_cancellation ? 'Download cancelled.' : `Download complete.`; - let type = pending_cancellation ? 'warning' : 'info'; - statusMessage(msg, type, null, true, false); + // Status pending_cancellation = false; - } else { - if(!workerObjectURL){ - workerObjectURL = URL.createObjectURL(workerCodeBlob); - } - while (s3_urls.length > 0) { - let targetWorker = null; - if (availableWorkers.length > 0) { - targetWorker = availableWorkers.pop(); - } else { - if (downloadWorkers.length <= workerLimit) { - targetWorker = allocateWorker(); + + // Workers + availableWorkers = []; + downloadWorkers = []; + workerDownloadThreshold = 100; + workerCodeBlob = null; + workerObjectURL = null; + workerLimit = navigator.hardwareConcurrency; + + constructor() { + this.queues = new DownloadQueueManager(); + this.workerCodeBlob = new Blob([this.workerCode], {type: 'application/javascript'}); + } + + get in_progress() { + return this.downloadWorkers.length - this.availableWorkers.length; + } + + // Replaces the current floating message contents with a new message, including a new icon if provided + statusMessage(message, type, icon, withClose, withCancel) { + let buttons = []; + withCancel && buttons.push(this.cancel_button); + withClose && buttons.push(this.close_button); + base.showFloatingMessage(type, message, true, null, icon, buttons); + } + + // Updates the current floating message contents and display class + progressUpdate(message, type) { + base.showFloatingMessage(type, message, false); + } + + finalizeWorker(worker) { + worker.terminate(); + this.downloadWorkers = this.downloadWorkers.filter(w => w !== worker); + } + + allocateWorker() { + let downloadWorker = new Worker(this.workerObjectURL); + downloadWorker.onmessage = workerOnMessage; + downloadWorker.onerror = workerOnError; + downloadWorker.downloadCount = 0; + this.downloadWorkers.push(downloadWorker); + return downloadWorker + } + + async triggerWorkerDownloads() { + // One way or another, there's nothing left to download + if (this.queues.isEmpty() && this.downloadWorkers.length <= 0) { + // cleanup our worker object URL for now + if (this.workerObjectURL) { + URL.revokeObjectURL(this.workerObjectURL); + this.workerObjectURL = null; + } + let msg = this.pending_cancellation ? 'Download cancelled.' : `Download complete.`; + let type = this.pending_cancellation ? 'warning' : 'info'; + this.statusMessage(msg, type, null, true, false); + this.pending_cancellation = false; } else { - break // all workers busy and we can't add more + if (!this.workerObjectURL) { + this.workerObjectURL = URL.createObjectURL(this.workerCodeBlob); + } + console.log("Queues:",this.queues.isEmpty()); + while (!this.queues.isEmpty()) { + let targetWorker = null; + if (this.availableWorkers.length > 0) { + targetWorker = this.availableWorkers.pop(); + } else { + if (this.downloadWorkers.length <= this.workerLimit) { + targetWorker = this.allocateWorker(); + } else { + break; // all workers busy and we can't add more + } + } + if(targetWorker) { + let item_to_download = await this.queues.get_download_item(); + targetWorker.postMessage({ + 'url': item_to_download['url'], + 'metadata': item_to_download, + 'directoryHandle': item_to_download['directory'] + }); + } else { + break; + } + } } - } - const s3_url = s3_urls.pop(); - targetWorker.postMessage({ 'url': s3_url['url'], 'metadata': s3_url, 'directoryHandle': directoryHandle }); } - } - } - async function getAllS3ObjectKeys(bucket, region, prefix) { - const allKeys = []; - let isTruncated = true; - let continuationToken = null; - while (isTruncated) { - let url = `https://${bucket}.s3.${region}.amazonaws.com/?list-type=2`; - if (prefix) { - url += `&prefix=${encodeURIComponent(prefix)}`; + addRequest(request) { + this.queues.load(request); } - if (continuationToken) { - url += `&continuation-token=${encodeURIComponent(continuationToken)}`; + + cancel() { + this.pending_cancellation = true; + $('.cancel-download').hide(); + $('.close-message-window').show(); + this.queues.emptyQueues(); + this.downloadWorkers.forEach(worker => { + worker.postMessage({'abort': true, 'reason': 'User cancelled download.'}); + }); } - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP Error ${response.status}: ${response.statusText}`); - } - const xmlText = await response.text(); - const parser = new DOMParser(); - const xmlDoc = parser.parseFromString(xmlText, "application/xml"); - const errorCode = xmlDoc.getElementsByTagName('Code')[0]?.textContent; - if (errorCode) { - const errorMessage = xmlDoc.getElementsByTagName('Message')[0]?.textContent; - throw new Error(`S3 API Error: ${errorCode} - ${errorMessage}`); - } - const keyElements = xmlDoc.getElementsByTagName('Key'); - const keysOnPage = Array.from(keyElements).map(el => el.textContent); - allKeys.push(...keysOnPage); - isTruncated = xmlDoc.getElementsByTagName('IsTruncated')[0]?.textContent === 'true'; - if (isTruncated) { - continuationToken = xmlDoc.getElementsByTagName('NextContinuationToken')[0]?.textContent; - } else { - continuationToken = null; - } - } catch (error) { - console.error("Failed to fetch S3 data:", error); - throw error; // Re-throw the error to be handled by the caller + + beginDownloads() { + if(!this.queues.isEmpty()) { + this.triggerWorkerDownloads(); + } + $('.cancel-download').show(); + $('.close-message-window').hide(); + this.statusMessage("Download underway.", 'message', '', false, true); } - } - return allKeys; } - async function beginDownload() { - directoryHandle = await window.showDirectoryPicker({ - id: 'idc-downloads', - startIn: 'downloads', - mode: 'readwrite', - }); - if (directoryHandle) { - triggerWorkerDownloads(); - } + async function getDirectory() { + let directoryHandle = await window.showDirectoryPicker({ + id: 'idc-downloads', + startIn: 'downloads', + mode: 'readwrite', + }); + return directoryHandle; } - $('.container-fluid').on('click', '.cancel-download', function(){ - pending_cancellation = true; - $('.cancel-download').hide(); - $('.close-message-window').show(); - s3_urls.splice(0, s3_urls.length); - downloadWorkers.forEach(worker => { - worker.postMessage({'abort': true, 'reason': 'User cancelled download.'}); - }); + let downloader_manager = new DownloadManager(); + + $('.container-fluid').on('click', '.cancel-download', function () { + downloader_manager.cancel(); }); - $('.container-fluid').on('click', '.download-all-instances', function(){ - const bucket = $(this).attr('data-bucket'); - const crdc_series_id = $(this).attr('data-series'); - const series_id = $(this).attr('data-series-id'); - const collection_id = $(this).attr('data-collection'); - const study_id = $(this).attr('data-study'); - const modality = $(this).attr('data-modality'); - const patient_id = $(this).attr('data-patient'); - getAllS3ObjectKeys(bucket, "us-east-1", crdc_series_id).then( keys => { - keys.forEach((key) => { - if (key !== "") { - const keys = key.split("/"); - const instance = keys[keys.length-1]; - s3_urls.push({ - 'url': `https://${bucket}.s3.us-east-1.amazonaws.com/${key}`, - 'study': study_id, - 'collection': collection_id, - 'series': series_id, - 'modality': modality, - 'instance': instance, - 'patient': patient_id - }); - } - }); - if(s3_urls.length <= 0) { - statusMessage('Error while parsing instance list!', 'error', null, true, false); - return; - } - beginDownload().then( - function(){ - $('.cancel-download').show(); - $('.close-message-window').hide(); - statusMessage("Download underway.", 'message', '', false, true); - } - ); + $('.container-fluid').on('click', '.download-all-instances', function () { + const clicked = $(this); + getDirectory().then(handle => { + const bucket = clicked.attr('data-bucket'); + const crdc_series_id = clicked.attr('data-series'); + const series_id = clicked.attr('data-series-id'); + const collection_id = clicked.attr('data-collection'); + const study_id = clicked.attr('data-study'); + const modality = clicked.attr('data-modality'); + const patient_id = clicked.attr('data-patient'); + downloader_manager.addRequest({ + 'directory': handle, + 'bucket': bucket, + 'crdc_series_id': crdc_series_id, + 'series_id': series_id, + 'collection_id': collection_id, + 'study_id': study_id, + 'modality': modality, + 'patient_id': patient_id + }); + + downloader_manager.beginDownloads(); }); }); }); \ No newline at end of file From 13f200dfd9956ca9bf31c9d95f40534d789d6f46 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Thu, 17 Jul 2025 15:12:58 -0700 Subject: [PATCH 25/52] -> Disable logins --- static/css/style.css | 5 + static/js/cohorts/export-manifest.js | 11 +- static/js/image_search.js | 21 +-- templates/base.html | 52 +++--- templates/cohorts/cohort_list.html | 34 ++-- templates/cohorts/cohort_test.html | 92 +++++------ templates/cohorts/export-manifest-modal.html | 162 +++++++++---------- templates/cohorts/save-cohort-modal.html | 104 ++++++------ templates/idc/explore.html | 37 +++-- templates/idc/landing.html | 16 +- templates/share/site_header.html | 64 ++++---- 11 files changed, 285 insertions(+), 313 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index 3284bbf35..7ebd17c2b 100755 --- a/static/css/style.css +++ b/static/css/style.css @@ -4521,3 +4521,8 @@ d-topics-list iframe { right: 40px; z-index: 9999; } + +.special-announcement { + margin-bottom: 0px; + text-align: center; +} diff --git a/static/js/cohorts/export-manifest.js b/static/js/cohorts/export-manifest.js index d30c2c121..b0843ccf6 100644 --- a/static/js/cohorts/export-manifest.js +++ b/static/js/cohorts/export-manifest.js @@ -450,16 +450,7 @@ require([ update_download_manifest_buttons(); }); - let bq_disabled_message = 'Exporting to BigQuery requires you to be logged in with a linked Google Social Account, and to save your filters as a cohort.'; - if(!user_is_auth) { - bq_disabled_message += ' Please log in with a Google Social account to enable this feature.' - } else if((user_is_social) && (typeof(user_id)!=="undefined")){ - bq_disabled_message += ' You can link your account to a Google ID from the ' - + '' - + 'Account Details page.' - } else if ((typeof(is_cohort)!=="undefined") && (!is_cohort)) { - bq_disabled_message += ' Please save these filters as a cohort to enable this feature.' - } + let bq_disabled_message = 'Exporting to BigQuery is no longer supported.' tippy.delegate('#export-manifest-modal', { content: bq_disabled_message, diff --git a/static/js/image_search.js b/static/js/image_search.js index 0fd14b223..3476340ce 100644 --- a/static/js/image_search.js +++ b/static/js/image_search.js @@ -185,7 +185,7 @@ require([ $('#search_def_access').addClass('is-hidden'); $('.access_warn').addClass('is-hidden'); } - if(is_cohort || (isFiltered && data.total > 0)) { + if(isFiltered && data.total > 0) { $('#search_def_stats').html("Cohort filter contents: " + data.totals.SeriesInstanceUID.toString() + " series from " + data.totals.PatientID.toString() + " cases / " + @@ -197,16 +197,6 @@ require([ } else { $('#search_def_stats').html(" "); } - if (is_cohort) { - (async_download && !user_is_social) && $('#need-social-account').show(); - } else { - data.total > 0 && $('#save-cohort-btn').removeAttr('disabled'); - if (user_is_auth) { - $('#save-cohort-btn').prop('title', data.total > 0 ? 'Please select at least one filter.' : 'There are no cases in this cohort.'); - } else { - $('#save-cohort-btn').prop('title', 'Log in to save.'); - } - } filterutils.updateCollectionTotals('Program', data.programs); dicofdic = {'unfilt': data.origin_set.All.attributes, 'filt': ''} @@ -486,17 +476,8 @@ require([ sessionStorage.setItem('anonymous_sliders', sliderStr); }; - - $('#save-cohort-btn, #sign-in-dropdown').on('click', function() { - if (!user_is_auth) { - save_anonymous_selection_data(); - location.href=$(this).data('uri'); - } - }); - cohort_loaded = false; - $('.fa-cog').on("click",function(){ let srt = $(this).parent().parent().parent().find('.cntr') if (srt.hasClass('is-hidden')) { diff --git a/templates/base.html b/templates/base.html index 9f2a410e2..f129eda43 100755 --- a/templates/base.html +++ b/templates/base.html @@ -91,20 +91,6 @@ {% endautoescape %} {% endif %}
- {% if request.user.is_authenticated %} -
- - We are planning to remove user accounts from our system. This will remove access to your - currently saved cohorts. If you would like to provide feedback on this decision, please visit - us at our - - Discourse Forum - . -
- {% endif %}
@@ -184,25 +170,25 @@ aria-hidden="true">
diff --git a/templates/cohorts/cohort_list.html b/templates/cohorts/cohort_list.html index caaca4c25..c9abfe8bc 100644 --- a/templates/cohorts/cohort_list.html +++ b/templates/cohorts/cohort_list.html @@ -181,23 +181,23 @@

Cohorts

diff --git a/templates/cohorts/cohort_test.html b/templates/cohorts/cohort_test.html index a455f4ace..b0a98db9a 100644 --- a/templates/cohorts/cohort_test.html +++ b/templates/cohorts/cohort_test.html @@ -205,21 +205,21 @@ @@ -228,37 +228,37 @@ diff --git a/templates/cohorts/export-manifest-modal.html b/templates/cohorts/export-manifest-modal.html index 625fd8150..67785bd3a 100644 --- a/templates/cohorts/export-manifest-modal.html +++ b/templates/cohorts/export-manifest-modal.html @@ -58,93 +58,93 @@ BigQuery -
{% if user_is_social and is_cohort %} -
- -

BigQuery table names are automatically generated and will be provided once the manifest has exported.

-
- -
- -
-
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • - -
  • -
  • -
    -
    - +{#
    #} +{# #} +{#

    BigQuery table names are automatically generated and will be provided once the manifest has exported.

    #} +{#
    #} +{# #} +{#
    #} +{#
    #} +{# #} +{# Columns#} +{# #} +{#
    #} +{#
    #} +{#
  • #} +{#
  • #} +{#
  • #} +{#
  • #} +{#
  • #} +{#
  • #} +{#
  • #} +{#
  • #} +{#
  • #} +{#
  • #} +{##} +{#
  • #} +{#
  • #} +{#
    #} +{#
    #} +{# #} {% else %}
    - Exporting to BigQuery requires you to be logged in with a linked Google Social - Account, and to save your filters as a cohort. - {% if user_is_auth and not user_is_social %} - You can link your account to a Google ID from the - Account Details page. - {% else %} - Please log in with a Google Social account to enable this feature. - {% if not is_cohort %} - Please save your filters as a cohort to enable this feature. - {% endif %} - {% endif %} +{# Exporting to BigQuery requires you to be logged in with a linked Google Social#} +{# Account, and to save your filters as a cohort.#} +{# {% if user_is_auth and not user_is_social %}#} +{# You can link your account to a Google ID from the#} +{# Account Details page.#} +{# {% else %}#} +{# Please log in with a Google Social account to enable this feature.#} +{# {% if not is_cohort %}#} +{# Please save your filters as a cohort to enable this feature.#} +{# {% endif %}#} +{# {% endif %}#} + Exporting to BigQuery is no longer supported.
    {% endif %}
    diff --git a/templates/cohorts/save-cohort-modal.html b/templates/cohorts/save-cohort-modal.html index a020cf96b..83cf97ba1 100644 --- a/templates/cohorts/save-cohort-modal.html +++ b/templates/cohorts/save-cohort-modal.html @@ -2,58 +2,58 @@ \ No newline at end of file diff --git a/templates/idc/explore.html b/templates/idc/explore.html index fbb4649d0..51113c937 100644 --- a/templates/idc/explore.html +++ b/templates/idc/explore.html @@ -55,20 +55,31 @@
    +
    +
    + + We have removed user accounts from our system. If you would like to provide feedback on this + decision or need help accessing your previously saved cohorts, please visit us at our + + Discourse topic + . +
    +

    Explore Image Data

    - - - {% if request.user.is_authenticated %} - - {% else %} - - {% endif %} +{# {% if request.user.is_authenticated %}#} +{# #} +{# {% else %}#} +{# #} +{# {% endif %}#} {% endblock %} diff --git a/templates/idc/landing.html b/templates/idc/landing.html index 122e31ee7..362000ed5 100644 --- a/templates/idc/landing.html +++ b/templates/idc/landing.html @@ -46,18 +46,16 @@
    -
    +
    - IDC is planning to remove user accounts from the data portal on July 15, 2025. Any user cohorts - and the ability to export them to BigQuery will be unavailable after this date. If you have any questions - please see our - Discourse topic. + We have removed user accounts from our system. If you would like to provide feedback on this + decision or need help accessing your previously saved cohorts, please visit us at our + + Discourse topic + .
    diff --git a/templates/share/site_header.html b/templates/share/site_header.html index 59ecea3f6..21f862ed1 100644 --- a/templates/share/site_header.html +++ b/templates/share/site_header.html @@ -54,11 +54,11 @@ Documentation - {% if user.is_authenticated %} - - {% endif %} +{# {% if user.is_authenticated %}#} +{# #} +{# {% endif %}#}
    - {% if user.is_authenticated %} - - {% else %} - - {% endif %} +{# {% if user.is_authenticated %}#} +{# #} +{# {% else %}#} +{# #} +{# {% endif %}#} From 5771e5a1c50831c258083f597a1ca801f700236c Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Mon, 21 Jul 2025 09:48:02 -0700 Subject: [PATCH 26/52] -> Downloader updates --- static/css/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/css/style.css b/static/css/style.css index 7ebd17c2b..21d90a751 100755 --- a/static/css/style.css +++ b/static/css/style.css @@ -4517,7 +4517,7 @@ d-topics-list iframe { #floating-message { position: fixed; top: 180px; - width: 300px; + width: 400px; right: 40px; z-index: 9999; } From a9283c3676c3f1ba1bdd044a7bb42d6d9296606c Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Mon, 21 Jul 2025 09:50:22 -0700 Subject: [PATCH 27/52] -> Typo --- static/js/tables.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/tables.js b/static/js/tables.js index 6f58f83da..a647ece8b 100644 --- a/static/js/tables.js +++ b/static/js/tables.js @@ -948,7 +948,7 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, let volView_element = '
  • VolView ' + '
  • ` } return `` From d9e924c72f0e69406ab746b9b79a1aecd7a2d5f7 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Wed, 30 Jul 2025 23:08:15 -0700 Subject: [PATCH 29/52] -> Study downloads --- idc/urls.py | 1 + idc/views.py | 72 +++++++++++++++++----------- static/js/downloader.js | 48 ++++++++++++------- static/js/tables.js | 27 +++++++++-- templates/idc/explore_data_core.html | 1 + 5 files changed, 100 insertions(+), 49 deletions(-) diff --git a/idc/urls.py b/idc/urls.py index 0d96df712..45905bf91 100644 --- a/idc/urls.py +++ b/idc/urls.py @@ -61,6 +61,7 @@ re_path(r'^explore/cart/$', views.cart_page, name='get_explore_cart'), re_path(r'^cart_data/$', views.cart_data, name='get_cart_data'), re_path(r'^cart_data_stats/$', views.cart_data_stats, name='get_cart_data_stats'), + re_path(r'^series_ids/(?P[0-9\.]+)/$', views.get_series_for_study, name='get_series_for_study'), re_path(r'^collaborators/', views.collaborators, name='collaborators'), re_path(r'^collections/', include('idc_collections.urls')), # re_path(r'^share/', include('sharing.urls')), diff --git a/idc/views.py b/idc/views.py index 3389bdcd0..4fd029fe1 100644 --- a/idc/views.py +++ b/idc/views.py @@ -44,6 +44,7 @@ from django.contrib.auth.signals import user_login_failed from django.dispatch import receiver from idc.models import User_Data +from solr_helpers import build_solr_query, query_solr_and_format_result @@ -54,8 +55,6 @@ WEBAPP_LOGIN_LOG_NAME = settings.WEBAPP_LOGIN_LOG_NAME - - # The site's homepage @cache_page(60 * 5) def landing_page(request): @@ -183,7 +182,6 @@ def user_login_failed_callback(sender, credentials, **kwargs): log_name, '[WEBAPP LOGIN] Login FAILED for: {credentials}'.format(credentials=credentials) ) - except Exception as e: logger.exception(e) @@ -241,24 +239,6 @@ def save_ui_hist(request): return JsonResponse({}, status=status) -# @login_required -# def getCartData(request): -# response = {} -# status = 200 -# sources = ImagingDataCommonsVersion.objects.get(active=True).get_data_sources( -# active=True, source_type=DataSource.SOLR, -# aggregate_level="StudyInstanceUID" -# ) - -#def compcartsets(carthist, sel): - -# def cartsets(carthist): -# cartsets = [] -# -# for cartfiltset in carhist: -# for selection in cartfiltset: -# sel = selection['sel'] - def cart(request): response={} @@ -286,15 +266,9 @@ def cart(request): return JsonResponse(response, status=status) -# Calculate the size and counts of a cart based on its current partitions -def calculate_cart(request): - pass - - # returns various metadata mappings for selected projects used in calculating cart selection # counts 'on the fly' client side def studymp(request): - response = {} status = 200 sources = ImagingDataCommonsVersion.objects.get(active=True).get_data_sources( @@ -789,8 +763,8 @@ def populate_tables_old(request): return JsonResponse(response, status=status) + # Data exploration and cohort creation page -#@login_required def explore_data_page(request, filter_path=False, path_filters=None): context = {'request': request} is_json = False @@ -946,6 +920,7 @@ def explorer_manifest(request): logger.exception(e) return redirect(reverse('cart')) + # Given a set of filters in a GET request, parse the filter set out into a filter set recognized # by the explore_data_page method and forward it on to that view, returning its response. def parse_explore_filters(request): @@ -1098,6 +1073,47 @@ def cart_data(request): return JsonResponse(response, status=status) +def get_series_for_study(request, study_uid): + try: + status = 200 + response = { "result": [] } + source = ImagingDataCommonsVersion.objects.get(active=True).get_data_sources( + active=True, source_type=DataSource.SOLR, + aggregate_level="SeriesInstanceUID" + ).first() + filter_query = build_solr_query( + {"StudyInstanceUID": [study_uid]}, + with_tags_for_ex=False, + search_child_records_by=None, solr_default_op='AND' + ) + result = query_solr_and_format_result( + { + "collection": source.name, + "fields": ["StudyInstanceUID", "Modality", "crdc_series_uuid", "SeriesInstanceUID", "aws_bucket", "instance_size"], + "query_string": None, + "fqs": [filter_query['full_query_str']], + "facets": None, "sort":None, "counts_only":False + } + ) + for doc in result['docs']: + response['result'].append({ + "series_id": doc['SeriesInstanceUID'], + "crdc_series_id": doc['crdc_series_uuid'], + "bucket": doc['aws_bucket'][0], + "series_size": doc['instance_size'][0], + "modality": doc['Modality'][0], + "study": doc['StudyInstanceUID'] + }) + + except Exception as e: + logger.error("[ERROR] While fetching series per study ID:") + logger.exception(e) + response['message'] = "Error while retrieving series IDs" + status = 400 + + return JsonResponse(response, status=status) + + # User dashboard, where saved cohorts (and, in the future, uploaded/indexed data) are listed @login_required def dashboard_page(request): diff --git a/static/js/downloader.js b/static/js/downloader.js index 1bc36a22d..58a31ae36 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -458,26 +458,42 @@ require([ startIn: 'downloads', mode: 'readwrite', }); - const bucket = clicked.attr('data-bucket'); - const crdc_series_id = clicked.attr('data-series'); - const series_id = clicked.attr('data-series-id'); const collection_id = clicked.attr('data-collection'); const study_id = clicked.attr('data-study'); - const modality = clicked.attr('data-modality'); const patient_id = clicked.attr('data-patient'); - const total_series_size = clicked.attr('data-series-size'); - downloader_manager.addRequest({ - 'directory': directoryHandle, - 'bucket': bucket, - 'crdc_series_id': crdc_series_id, - 'series_id': series_id, - 'collection_id': collection_id, - 'study_id': study_id, - 'modality': modality, - 'patient_id': patient_id, - 'series_size': total_series_size - }); + let series = []; + if(clicked.hasClass('download-study')) { + // This is a study row click + let response = await fetch(`http://localhost:8086/series_ids/${study_id}/`); + if (!response.ok) { + console.error(`[ERROR] Failed to retrieve series IDs for study ${study_id}: ${response.status}`); + return; + } + const series_data = await response.json(); + series.push(...series_data['result']); + } else { + series.push({ + "bucket": clicked.attr('data-bucket'), + "crdc_series_id": clicked.attr('data-series'), + "series_id": clicked.attr('data-series-id'), + "modality": clicked.attr('data-modality'), + "series_size": clicked.attr('data-series-size') + }); + } + series.forEach(series_request => { + downloader_manager.addRequest({ + 'directory': directoryHandle, + 'bucket': series_request['bucket'], + 'crdc_series_id': series_request['crdc_series_id'], + 'series_id': series_request['series_id'], + 'collection_id': collection_id, + 'study_id': study_id, + 'modality': series_request['modality'], + 'patient_id': patient_id, + 'series_size': series_request['series_size'] + }); + }); downloader_manager.beginDownloads(); }); }); \ No newline at end of file diff --git a/static/js/tables.js b/static/js/tables.js index e700a40cf..2dbce6778 100644 --- a/static/js/tables.js +++ b/static/js/tables.js @@ -849,7 +849,8 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, {className: "col1 study-description", "targets": [6]}, {className: "col1 numrows", "targets": [7]}, {className: "ohif open-viewer", "targets": [8]}, - {className: "download-col", "targets": [9]}, + {className: "manifest-col", "targets": [9]}, + {className: "download-col", "targets": [10]}, ], "columns": [ { @@ -978,10 +979,24 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, "type":"html", "orderable": false, data: 'StudyInstanceUID', render: function (data, type, row){ - return '' } - } + }, + { + "type":"html", + "orderable": false, + data: 'StudyInstanceUID', render: function (data, type, row) { + if ("showDirectoryPicker" in window) { + return ``; + } + return ``; + } + } ], "processing": true, "serverSide": true, @@ -1216,7 +1231,8 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, {className: "col1 body-part-examined", "targets": [5]}, {className: "series-description", "targets": [6]}, {className: "ohif open-viewer", "targets": [7]}, - {className: "download-col", "targets": [8]}, + {className: "manifest-col", "targets": [8]}, + {className: "download-col", "targets": [9]}, ], "columns": [ {"type": "html", "orderable": false, render: function () { @@ -2138,7 +2154,8 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, //do nothing. handled by triggers in base.js and explore.js to copy to clipboard and show a copy tooltip } else if ($(elem).hasClass('ohif') || $(elem).parentsUntil('tr').hasClass('ohif')) { //do nothing here. opening the viewer - } else if ($(elem).hasClass('download-col') || $(elem).parentsUntil('tr').hasClass('download-col')) { + } else if ($(elem).hasClass('download-col') || $(elem).hasClass('manifest-col') || $(elem).parentsUntil('tr').hasClass('download-col') + || $(elem).parentsUntil('tr').hasClass('manifest-col')) { //do nothing here. downloading a series or study manifest } else if ($(elem).hasClass('shopping-cart') || $(elem).hasClass('shopping-cart-holder')) { handleCartClick(tabletype, row, elem, ids); diff --git a/templates/idc/explore_data_core.html b/templates/idc/explore_data_core.html index 3008eae68..044c32571 100644 --- a/templates/idc/explore_data_core.html +++ b/templates/idc/explore_data_core.html @@ -893,6 +893,7 @@

    Study Description # of Series View + From 8abd631a402c9c184af89534002a3cefec5de049 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Wed, 30 Jul 2025 23:13:38 -0700 Subject: [PATCH 30/52] -> Study downloads --- static/js/downloader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index 58a31ae36..6deef9dc5 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -465,7 +465,7 @@ require([ let series = []; if(clicked.hasClass('download-study')) { // This is a study row click - let response = await fetch(`http://localhost:8086/series_ids/${study_id}/`); + let response = await fetch(`${BASE_URL}/series_ids/${study_id}/`); if (!response.ok) { console.error(`[ERROR] Failed to retrieve series IDs for study ${study_id}: ${response.status}`); return; From caa79748e608ba98ebb2996ce5cf1f4c1135870d Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Wed, 30 Jul 2025 23:33:01 -0700 Subject: [PATCH 31/52] -> Study download tooltips and table layout adjustments --- static/css/search.css | 5 +++++ static/js/explore.js | 32 +++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/static/css/search.css b/static/css/search.css index d832580b6..6ce3c7dc5 100644 --- a/static/css/search.css +++ b/static/css/search.css @@ -455,6 +455,11 @@ tr { #studies_table_head th.open-viewer, #studies_table td.open-viewer { width:4%; + /* This is necessary to make sure the 2 icons remain together, lined up. */ + min-width: 80px; +} + +#studies_table td.open-viewer { /* This is necessary to make sure the 2 icons remain together, lined up. */ min-width: 70px; } diff --git a/static/js/explore.js b/static/js/explore.js index 054ac08c7..97edd275d 100644 --- a/static/js/explore.js +++ b/static/js/explore.js @@ -546,8 +546,38 @@ require([ maxWidth: 85 }); + tippy.delegate('.studies-table', { + content: 'Download all of the image instances in this study.', + theme: 'dark', + placement: 'left', + arrow: false, + interactive:true, + target: '.download-all-instances', + maxWidth: 200 + }); + + tippy.delegate('.studies-table', { + content: 'Download a manifest file for this study.', + theme: 'dark', + placement: 'left', + arrow: false, + interactive:true, + target: '.export-button', + maxWidth: 200 + }); + + tippy.delegate('.studies-table', { + content: 'Direct download is only available in Chromium browsers.', + theme: 'dark', + placement: 'left', + arrow: false, + interactive:true, + target: '.download-all-disabled', + maxWidth: 200 + }); + tippy.delegate('.series-table', { - content: 'Download all of the image instances for this series.', + content: 'Download all of the image instances in this series.', theme: 'dark', placement: 'left', arrow: false, From e273258e0d312a42a01006a459b6f2bbd4cd7184 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Tue, 5 Aug 2025 12:26:46 -0700 Subject: [PATCH 32/52] -> Case downloads --- idc/urls.py | 3 ++- idc/views.py | 16 +++++++++++----- static/js/downloader.js | 11 ++++++----- static/js/tables.js | 14 ++++++++++++-- templates/idc/explore_data_core.html | 9 +++++---- 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/idc/urls.py b/idc/urls.py index 45905bf91..f49e56fb1 100644 --- a/idc/urls.py +++ b/idc/urls.py @@ -61,7 +61,8 @@ re_path(r'^explore/cart/$', views.cart_page, name='get_explore_cart'), re_path(r'^cart_data/$', views.cart_data, name='get_cart_data'), re_path(r'^cart_data_stats/$', views.cart_data_stats, name='get_cart_data_stats'), - re_path(r'^series_ids/(?P[0-9\.]+)/$', views.get_series_for_study, name='get_series_for_study'), + re_path(r'^series_ids/(?P[A-Za-z0-9\.\-_]+)/$', views.get_series, name='get_series_by_case'), + re_path(r'^series_ids/(?P[A-Za-z0-9\.\-_]+)/(?P[0-9\.]+)/$', views.get_series, name='get_series'), re_path(r'^collaborators/', views.collaborators, name='collaborators'), re_path(r'^collections/', include('idc_collections.urls')), # re_path(r'^share/', include('sharing.urls')), diff --git a/idc/views.py b/idc/views.py index 4fd029fe1..cf5540526 100644 --- a/idc/views.py +++ b/idc/views.py @@ -1073,7 +1073,7 @@ def cart_data(request): return JsonResponse(response, status=status) -def get_series_for_study(request, study_uid): +def get_series(request, patient_id, study_uid=None): try: status = 200 response = { "result": [] } @@ -1081,18 +1081,23 @@ def get_series_for_study(request, study_uid): active=True, source_type=DataSource.SOLR, aggregate_level="SeriesInstanceUID" ).first() + filters = { + "PatientID": [patient_id] + } + if study_uid: + filters["StudyInstanceUID"] = [study_uid] filter_query = build_solr_query( - {"StudyInstanceUID": [study_uid]}, + filters, with_tags_for_ex=False, search_child_records_by=None, solr_default_op='AND' ) result = query_solr_and_format_result( { "collection": source.name, - "fields": ["StudyInstanceUID", "Modality", "crdc_series_uuid", "SeriesInstanceUID", "aws_bucket", "instance_size"], + "fields": ["PatientID", "StudyInstanceUID", "Modality", "crdc_series_uuid", "SeriesInstanceUID", "aws_bucket", "instance_size"], "query_string": None, "fqs": [filter_query['full_query_str']], - "facets": None, "sort":None, "counts_only":False + "facets": None, "sort": None, "counts_only": False, "limit": 2000 } ) for doc in result['docs']: @@ -1102,7 +1107,8 @@ def get_series_for_study(request, study_uid): "bucket": doc['aws_bucket'][0], "series_size": doc['instance_size'][0], "modality": doc['Modality'][0], - "study": doc['StudyInstanceUID'] + "study_id": doc['StudyInstanceUID'], + "case": doc["PatientID"] }) except Exception as e: diff --git a/static/js/downloader.js b/static/js/downloader.js index 6deef9dc5..1d7f20812 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -463,9 +463,9 @@ require([ const patient_id = clicked.attr('data-patient'); let series = []; - if(clicked.hasClass('download-study')) { - // This is a study row click - let response = await fetch(`${BASE_URL}/series_ids/${study_id}/`); + if(clicked.hasClass('download-study') || clicked.hasClass('download-case')) { + let study_uri = (study_id !== undefined && study_id !== null) ? `${study_id}/` : ""; + let response = await fetch(`${BASE_URL}/series_ids/${patient_id}/${study_uri}`); if (!response.ok) { console.error(`[ERROR] Failed to retrieve series IDs for study ${study_id}: ${response.status}`); return; @@ -478,7 +478,8 @@ require([ "crdc_series_id": clicked.attr('data-series'), "series_id": clicked.attr('data-series-id'), "modality": clicked.attr('data-modality'), - "series_size": clicked.attr('data-series-size') + "series_size": clicked.attr('data-series-size'), + "study_id": study_id }); } series.forEach(series_request => { @@ -488,7 +489,7 @@ require([ 'crdc_series_id': series_request['crdc_series_id'], 'series_id': series_request['series_id'], 'collection_id': collection_id, - 'study_id': study_id, + 'study_id': series_request['study_id'], 'modality': series_request['modality'], 'patient_id': patient_id, 'series_size': series_request['series_size'] diff --git a/static/js/tables.js b/static/js/tables.js index 2dbce6778..d9cc0733b 100644 --- a/static/js/tables.js +++ b/static/js/tables.js @@ -490,7 +490,8 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, {className: "col1 project-name", "targets": [3]}, {className: "col1 case-id", "targets": [4]}, {className: "col1 numrows", "targets": [5]}, - {className: "col1", "targets": [6]}]; + {className: "col1 numseries", "targets": [6]}, + {className: "col1 download-case", "targets": [7]}]; }; const caseTableColumns = function() { @@ -548,7 +549,16 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, }}; const study_col = {"type": "num", "orderable": true, data: 'unique_studies'}; const series_col = {"type": "num", "orderable": true, data: 'unique_series'}; - return [caret_col, cart_col, cartnum_col, collection_col, case_col, study_col, series_col]; + const download_col = {"type": "htlp", "orderable": false, data: 'patient_id', render: function(data, type, row) { + if ("showDirectoryPicker" in window) { + return ``; + } + return ``; + }}; + return [caret_col, cart_col, cartnum_col, collection_col, case_col, study_col, series_col, download_col]; }; // recreates the cases table when a chevron is clicked in the projects table. Defines the chevron and cart selection actions. diff --git a/templates/idc/explore_data_core.html b/templates/idc/explore_data_core.html index 044c32571..a204fb3ba 100644 --- a/templates/idc/explore_data_core.html +++ b/templates/idc/explore_data_core.html @@ -864,10 +864,11 @@

    # of Series in Cart - Collection Name - Case ID - Total # of Studies - Total # of Series + Collection Name + Case ID + Total # of Studies + Total # of Series + From a0397ae9f375fa4162979ec8501e026d8e41f7b9 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Tue, 5 Aug 2025 12:29:39 -0700 Subject: [PATCH 33/52] -> Case download tooltip --- static/js/explore.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/static/js/explore.js b/static/js/explore.js index 97edd275d..75a39d6a6 100644 --- a/static/js/explore.js +++ b/static/js/explore.js @@ -546,6 +546,26 @@ require([ maxWidth: 85 }); + tippy.delegate('.cases-table', { + content: 'Download all of the image instances in this case.', + theme: 'dark', + placement: 'left', + arrow: false, + interactive:true, + target: '.download-all-instances', + maxWidth: 200 + }); + + tippy.delegate('.cases-table', { + content: 'Direct download is only available in Chromium browsers.', + theme: 'dark', + placement: 'left', + arrow: false, + interactive:true, + target: '.download-all-disabled', + maxWidth: 200 + }); + tippy.delegate('.studies-table', { content: 'Download all of the image instances in this study.', theme: 'dark', From 2969e6881d28023f00b3d8999f2e2fc912a2f5c3 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Tue, 5 Aug 2025 14:19:09 -0700 Subject: [PATCH 34/52] -> Download Messenger class --- static/js/base.js | 1 - static/js/downloader.js | 110 ++++++++++++++++++++++++++++++++-------- static/js/explore.js | 2 +- static/js/utils.js | 39 -------------- templates/base.html | 27 +++++++++- 5 files changed, 116 insertions(+), 63 deletions(-) diff --git a/static/js/base.js b/static/js/base.js index a2af11e60..9719b1079 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -263,7 +263,6 @@ define(['jquery', 'utils'], function($, utils) { // From http://www.regular-expressions.info/email.html email: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, showJsMessage: utils.showJsMessage, - showFloatingMessage: utils.showFloatingMessage, hideFloatingMessage: utils.hideFloatingMessage, // Simple method for standardizing storage of a message into sessionStorage so it can be retrieved and reloaded // at document load time diff --git a/static/js/downloader.js b/static/js/downloader.js index 1d7f20812..8f3334cb8 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -46,6 +46,53 @@ require([ return `${(Math.round((size/(Math.pow(10,log_level)))*100)/100)} ${byte_level[(byte_count/3)]}` ; } + class DownloadProgressDisplay { + msgBox = $('#floating-message'); + cancel_button = this.msgBox.find('.cancel-floating-message'); + close_button = this.msgBox.find('.close-floating-message'); + msgBoxContents = this.msgBox.find('.floating-message-content'); + msgBoxHeader = this.msgBox.find('.floating-message-header'); + + // Updates floating message text elements and/or icons + // messages: Object of DOM element contents to update of the form: + // { : , [: ...] } + // subclass should be the final term in the element's floating-message class + // (eg. floating-message-header has a subclass of header) + // A falsey valye will empty all text elements + // icon: string subclass of the icon to display. subclass should be the final term in the icon's + // floating-message-icon class (eg. floating-message-icon-download has a subclass of download) + update(type, messages, icon) { + let alert_box = this.msgBox.find('.alert'); + alert_box.removeClass(); + alert_box.addClass(`alert alert-dismissible alert-${type}`); + let new_icon = typeof icon === "string"; + (!icon || new_icon) && this.msgBox.find('.floating-message-icon').hide(); + (new_icon) && this.msgBox.find(`.floating-message-icon-${icon}`).show(); + if(messages) { + for (const [loc, msg] of Object.entries(messages)) { + let updateTo = this.msgBox.find(`.floating-message-${loc}`); + updateTo.empty(); + updateTo.append(`${msg}`); + } + } else { + this.msgBox.find('.floating-messages').empty(); + } + } + + // Sets up the download message box and displays it. Subsequent calls to this method + // will completely override the current contents of the entire display. To update only the type, text + // and/or icons, use update. + show(type, contents, icon, withCancel, withClose) { + withCancel ? this.cancel_button.show() : this.cancel_button.hide(); + withClose ? this.close_button.show() : this.close_button.hide(); + this.msgBoxContents.empty(); + this.msgBoxHeader.empty(); + type = type || "info"; + this.update(type, contents, icon) + this.msgBox.show(); + } + } + class DownloadRequest { region = "us-east-1"; @@ -202,15 +249,17 @@ require([ let cancellation = event.data.message === 'error' && (downloader_manager.pending_cancellation || event.data.error.name === "AbortError"); if (true_error) { console.error(`Worker Error: ${JSON.stringify(event)}`); - downloader_manager.statusMessage(`Encountered an error while downloading these files.`, 'error', null, true, false); + downloader_manager.statusMessage(`Encountered an error while downloading these files.`, 'error', "error", true, false); } if (event.data.message === 'done' || cancellation) { let msg = `Download status: ${downloader_manager.in_progress} file(s) in progress, ${downloader_manager.queues.pending} in queue...`; + let progType = "download"; if (downloader_manager.queues.isEmpty()) { // This means the remaining downloads are all in-progress, or we cancelled msg = cancellation ? "Cleaning up cancelled downloads..." : `Download status: ${downloader_manager.in_progress} file(s) in progress...`; + progType = cancellation ? "cancel" : "done"; } - downloader_manager.progressUpdate(msg); + downloader_manager.progressUpdate(msg, progType); } if (downloader_manager.queues.isEmpty() || (thisWorker.downloadCount > downloader_manager.workerDownloadThreshold) || downloader_manager.pending_cancellation) { downloader_manager.finalizeWorker(thisWorker); @@ -224,11 +273,13 @@ require([ function workerOnError(event) { let thisWorker = event.target console.error('[Main] Error in worker:', event.message || "No message given", event); - downloader_manager.statusMessage(`[Worker] Error in worker: ${event.message}`, 'error', null, true, false); + downloader_manager.statusMessage(`[Worker] Error in worker: ${event.message}`, 'error', "error", true, false); downloader_manager.finalizeWorker(thisWorker); } class DownloadManager { + progressDisplay = new DownloadProgressDisplay(); + // Text blobs workerCode = ` const abort_controller = new AbortController(); @@ -312,12 +363,7 @@ require([ } } `; - cancel_button = ` - - `; - close_button = ` - - `; + // Status pending_cancellation = false; @@ -345,19 +391,40 @@ require([ // Replaces the current floating message contents with a new message, including a new icon if provided statusMessage(message, type, icon, withClose, withCancel) { - let buttons = []; - withCancel && buttons.push(this.cancel_button); - withClose && buttons.push(this.close_button); - base.showFloatingMessage(type, message, true, null, icon, buttons); + let messages = { + "content": message + }; + if(!this.pending_cancellation && this.in_progress > 0) { + messages['header'] = this.overall_progress; + } + this.progressDisplay.show(type, messages, icon, withCancel, withClose); } // Updates the current floating message contents and display class - progressUpdate(message, type) { - let messages = [message]; + progressUpdate(message, progType) { + progType = progType || "download"; + let type = "info"; + let icon = progType || true; + switch(progType) { + case "cancel": + type = "warning"; + break; + case "done": + type = "success"; + break; + case "error": + type = "error"; + break; + default: + break; + } + let messages = { + "content": message + }; if(!this.pending_cancellation && this.in_progress > 0) { - messages.unshift(this.overall_progress); + messages['header'] = this.overall_progress; } - base.showFloatingMessage(type, messages, false); + this.progressDisplay.update(type, messages, icon); } finalizeWorker(worker) { @@ -384,8 +451,9 @@ require([ } let msg = this.pending_cancellation ? 'Download cancelled.' : `Download complete.`; this.queues.reset_queue_counts(); - let type = this.pending_cancellation ? 'warning' : 'info'; - this.statusMessage(msg, type, null, true, false); + let type = this.pending_cancellation ? 'warning' : 'success'; + let icon = this.pending_cancellation ? 'cancel' : 'done'; + this.statusMessage(msg, type, icon, true, false); this.pending_cancellation = false; } else { if (!this.workerObjectURL) { @@ -411,7 +479,7 @@ require([ }); let queue_msg = this.queues.pending > 0 ? `, ${this.queues.pending} in queue` : ""; let msg = `Download status: ${this.in_progress} file(s) in progress${queue_msg}...`; - this.progressUpdate(msg); + this.progressUpdate(msg, "download"); } else { break; } @@ -438,7 +506,7 @@ require([ if(this.in_progress <= 0) { $('.cancel-download').show(); $('.close-message-window').hide(); - this.statusMessage("Download underway.", 'message', '', false, true); + this.statusMessage("Download underway.", 'info', "download", false, true); } this.triggerWorkerDownloads(); } diff --git a/static/js/explore.js b/static/js/explore.js index 75a39d6a6..98c4e8024 100644 --- a/static/js/explore.js +++ b/static/js/explore.js @@ -555,7 +555,7 @@ require([ target: '.download-all-instances', maxWidth: 200 }); - + tippy.delegate('.cases-table', { content: 'Direct download is only available in Chromium browsers.', theme: 'dark', diff --git a/static/js/utils.js b/static/js/utils.js index c1d95dd0b..9baaa7e08 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -133,44 +133,6 @@ define(['jquery', 'jqueryui'], function($, jqueryui) { return uuid; } - // A method for displaying a special floating message box during worker thread activity. Note there is only - // ever one floating message box - // If withEmpty is true, the message is assumed to replace the currently visible contents of the message box proper. - // If withEmpty is false, the message is assumed to replace the .contents element - // If withEmpty is false and the type is not null and isn't found on the alert subelement, the subelement's classes - // will be changed to match the new type - function _showFloatingMessage(type, contents, withEmpty, add_classes, icon, controls) { - let msgBox = $('#floating-message'); - withEmpty && msgBox.empty(); - let msg = contents instanceof Array ? contents.join("
    ") : contents; - controls = controls instanceof Array ? controls.join(" ") : controls; - icon = icon || ""; - if(withEmpty || msgBox.find('.alert-dismissible').length <= 0) { - msgBox.append( - $('
    ') - .addClass(`alert alert-${type} alert-dismissible`) - .html( - ` -

    ${msg} ${icon}

    -

    ${controls}

    - ` - ) - .prepend( - '' - ) - ); - } else { - let alert_box = msgBox.find('.alert-dismissible'); - alert_box.find('.contents').html(msg); - if(type && !alert_box.hasClass(`alert-${type}`)) { - alert_box.removeClass(); - alert_box.addClass(`alert alert-dismissible alert-${type}`); - } - } - msgBox.show(); - } - function _hideFloatingMessage(withEmpty) { withEmpty && $('#floating-message').empty(); $('#floating-message').hide(); @@ -245,7 +207,6 @@ define(['jquery', 'jqueryui'], function($, jqueryui) { return { showJsMessage: _showJsMessage, - showFloatingMessage: _showFloatingMessage, hideFloatingMessage: _hideFloatingMessage, // Block re-requests of requests which can't be handled via AJAX (eg. file downloads) // Uses cookie polling diff --git a/templates/base.html b/templates/base.html index f129eda43..0514419ba 100755 --- a/templates/base.html +++ b/templates/base.html @@ -92,7 +92,32 @@ {% endif %}
    - +
    From 5b80f5abf3b2b18390d9d87a91c3affeceacda16 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Tue, 5 Aug 2025 15:13:23 -0700 Subject: [PATCH 35/52] -> Download Messenger class --- static/js/downloader.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index 8f3334cb8..d25aa9239 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -183,6 +183,8 @@ require([ queue_byte_size = 0; series_count = 0; + case_count = 0; + study_count = 0; get pending() { let pending_items = [ @@ -364,7 +366,6 @@ require([ } `; - // Status pending_cancellation = false; From c6eb99b0d54edc4d6b296607411a79ea9a12de58 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Thu, 7 Aug 2025 13:15:23 -0700 Subject: [PATCH 36/52] -> Queue emptying swapped to length=0 -> Better message flow --- static/js/downloader.js | 105 ++++++++++++++++++++++++++-------------- static/js/tables.js | 4 +- static/js/utils.js | 2 +- 3 files changed, 71 insertions(+), 40 deletions(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index d25aa9239..7e5490b6c 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -183,8 +183,11 @@ require([ queue_byte_size = 0; series_count = 0; - case_count = 0; - study_count = 0; + collections = new Set([]); + cases = new Set([]); + studies = new Set([]); + + cancellation_underway = false; get pending() { let pending_items = [ @@ -198,15 +201,24 @@ require([ return size_display_string(this.queue_byte_size); } - reset_queue_counts() { + reset_queue_manager() { this.queue_byte_size = 0; this.series_count = 0; + this.collections = new Set([]); + this.cases = new Set([]); + this.studies = new Set([]); + this.cancellation_underway = false; } get active_requests() { return (this.working_queue.length > 0); } + cancel() { + this.cancellation_underway = true; + this._emptyQueues(); + } + async _update_queue() { if(this.working_queue.length < this.WORKING_QUEUE_LIMIT && this.hopper.length > 0) { let request = this.hopper.pop(); @@ -214,14 +226,21 @@ require([ this.working_queue.push(...keys); }); } + // It's possible a cancellation came in while we were doing this. If so, re-empty the working_queue + if(this.cancellation_underway) { + this.working_queue.slice(0,this.working_queue.length); + } } load(request) { let request_success = false; - if(this.hopper.length < this.HOPPER_LIMIT) { + if(this.hopper.length < this.HOPPER_LIMIT && !this.cancellation_underway) { this.hopper.push(new DownloadRequest(request)); this.queue_byte_size += parseFloat(request['series_size']); this.series_count += 1; + this.studies.add(request['study_id']); + this.collections.add(request['collection_id']); + this.cases.add(request['patient_id']); request_success = true; } return request_success; @@ -233,22 +252,22 @@ require([ async get_download_item() { await this._update_queue(); - if(this.working_queue.length > 0) { + if(this.working_queue.length > 0 && !this.cancellation_underway) { return this.working_queue.pop(); } return null; } - emptyQueues() { - this.working_queue.slice(0,this.working_queue.length); - this.hopper.slice(0,this.hopper.length); + _emptyQueues() { + this.hopper.length = 0; + this.working_queue.length = 0; } } function workerOnMessage(event) { let thisWorker = event.target; let true_error = event.data.message === 'error' && event.data.error.name !== "AbortError"; - let cancellation = event.data.message === 'error' && (downloader_manager.pending_cancellation || event.data.error.name === "AbortError"); + let cancellation = downloader_manager.pending_cancellation || (event.data.message === 'error' && event.data.error.name === "AbortError"); if (true_error) { console.error(`Worker Error: ${JSON.stringify(event)}`); downloader_manager.statusMessage(`Encountered an error while downloading these files.`, 'error', "error", true, false); @@ -383,7 +402,11 @@ require([ } get overall_progress() { - return `${this.queues.total_downloads_requested} of data over ${this.queues.series_count} series requested`; + return `${this.queues.total_downloads_requested} of data in ` + + `${this.queues.collections.size} collection(s) / ` + + `${this.queues.cases.size} case(s) / ` + + `${this.queues.studies.size} ${this.queues.studies.size <= 1 ? "study" : "studies"} / ` + + `${this.queues.series_count} series`; } get in_progress() { @@ -443,46 +466,54 @@ require([ } async triggerWorkerDownloads() { - // One way or another, there's nothing left to download if (this.queues.isEmpty() && this.downloadWorkers.length <= 0) { + // One way or another, we're stopping // cleanup our worker object URL for now if (this.workerObjectURL) { URL.revokeObjectURL(this.workerObjectURL); this.workerObjectURL = null; } let msg = this.pending_cancellation ? 'Download cancelled.' : `Download complete.`; - this.queues.reset_queue_counts(); + this.queues.reset_queue_manager(); let type = this.pending_cancellation ? 'warning' : 'success'; let icon = this.pending_cancellation ? 'cancel' : 'done'; this.statusMessage(msg, type, icon, true, false); this.pending_cancellation = false; } else { - if (!this.workerObjectURL) { - this.workerObjectURL = URL.createObjectURL(this.workerCodeBlob); - } - while (!this.queues.isEmpty() && !this.pending_cancellation) { - let targetWorker = null; - if (this.availableWorkers.length > 0) { - targetWorker = this.availableWorkers.pop(); - } else { - if (this.downloadWorkers.length <= this.workerLimit) { - targetWorker = this.allocateWorker(); + if(!this.pending_cancellation) { + if (!this.workerObjectURL) { + this.workerObjectURL = URL.createObjectURL(this.workerCodeBlob); + } + while (!this.queues.isEmpty() && !this.pending_cancellation) { + let targetWorker = null; + if (this.availableWorkers.length > 0) { + targetWorker = this.availableWorkers.pop(); } else { - break; // all workers busy and we can't add more + if (this.downloadWorkers.length <= this.workerLimit) { + targetWorker = this.allocateWorker(); + } else { + break; // all workers busy and we can't add more + } + } + if(targetWorker) { + let item_to_download = await this.queues.get_download_item(); + if(item_to_download) { + targetWorker.postMessage({ + 'url': item_to_download['url'], + 'metadata': item_to_download, + 'directoryHandle': item_to_download['directory'] + }); + let queue_msg = this.queues.pending > 0 ? `, ${this.queues.pending} in queue` : ""; + let msg = `Download status: ${this.in_progress} file(s) in progress${queue_msg}...`; + this.progressUpdate(msg, "download"); + } else { + // For whatever reason, we didn't get an item to work on; put this worker back on the + // available set + this.availableWorkers.push(targetWorker); + } + } else { + break; } - } - if(targetWorker) { - let item_to_download = await this.queues.get_download_item(); - targetWorker.postMessage({ - 'url': item_to_download['url'], - 'metadata': item_to_download, - 'directoryHandle': item_to_download['directory'] - }); - let queue_msg = this.queues.pending > 0 ? `, ${this.queues.pending} in queue` : ""; - let msg = `Download status: ${this.in_progress} file(s) in progress${queue_msg}...`; - this.progressUpdate(msg, "download"); - } else { - break; } } } @@ -496,7 +527,7 @@ require([ this.pending_cancellation = true; $('.cancel-download').hide(); $('.close-message-window').show(); - this.queues.emptyQueues(); + this.queues.cancel(); this.downloadWorkers.forEach(worker => { worker.postMessage({'abort': true, 'reason': 'User cancelled download.'}); }); diff --git a/static/js/tables.js b/static/js/tables.js index d9cc0733b..2a87cfc37 100644 --- a/static/js/tables.js +++ b/static/js/tables.js @@ -491,7 +491,7 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, {className: "col1 case-id", "targets": [4]}, {className: "col1 numrows", "targets": [5]}, {className: "col1 numseries", "targets": [6]}, - {className: "col1 download-case", "targets": [7]}]; + {className: "col1 download-case download-col", "targets": [7]}]; }; const caseTableColumns = function() { @@ -2166,7 +2166,7 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, //do nothing here. opening the viewer } else if ($(elem).hasClass('download-col') || $(elem).hasClass('manifest-col') || $(elem).parentsUntil('tr').hasClass('download-col') || $(elem).parentsUntil('tr').hasClass('manifest-col')) { - //do nothing here. downloading a series or study manifest + //do nothing here. downloading a manifest or instances } else if ($(elem).hasClass('shopping-cart') || $(elem).hasClass('shopping-cart-holder')) { handleCartClick(tabletype, row, elem, ids); } diff --git a/static/js/utils.js b/static/js/utils.js index 9baaa7e08..fbbbf0605 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -134,7 +134,7 @@ define(['jquery', 'jqueryui'], function($, jqueryui) { } function _hideFloatingMessage(withEmpty) { - withEmpty && $('#floating-message').empty(); + withEmpty && $('.floating-messages').empty(); $('#floating-message').hide(); } From 86272f52d66c1ac30486a5a3dd63d01c47039d0b Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Wed, 13 Aug 2025 14:12:57 -0700 Subject: [PATCH 37/52] -> Better status display --- static/js/downloader.js | 66 ++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index 7e5490b6c..d9da43fdb 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -38,12 +38,16 @@ require([ }; function size_display_string(size) { + if(size <= 0) { + return "0 MB"; + } let log_level = Math.floor(Math.log10(size)); let byte_count = 12; while(log_level%byte_count >= log_level) { byte_count-=3; } - return `${(Math.round((size/(Math.pow(10,log_level)))*100)/100)} ${byte_level[(byte_count/3)]}` ; + let bytes = (Math.round((size/(Math.pow(10,log_level)))*100)/100).toFixed(2); + return `${bytes} ${byte_level[(byte_count/3)]}` ; } class DownloadProgressDisplay { @@ -65,7 +69,7 @@ require([ let alert_box = this.msgBox.find('.alert'); alert_box.removeClass(); alert_box.addClass(`alert alert-dismissible alert-${type}`); - let new_icon = typeof icon === "string"; + let new_icon = (typeof icon === "string"); (!icon || new_icon) && this.msgBox.find('.floating-message-icon').hide(); (new_icon) && this.msgBox.find(`.floating-message-icon-${icon}`).show(); if(messages) { @@ -182,6 +186,7 @@ require([ working_queue = []; queue_byte_size = 0; + bytes_downloaded = 0; series_count = 0; collections = new Set([]); cases = new Set([]); @@ -190,19 +195,26 @@ require([ cancellation_underway = false; get pending() { - let pending_items = [ - this.hopper.length > 0 ? `${this.hopper.length} series` : "", - this.working_queue.length > 0 ? `${this.working_queue.length} file(s)` : "" - ]; - return `${pending_items.join(" and ")}`; + if(this.hopper.length <= 0 && this.working_queue.length <= 0) { + return ""; + } + let pending_items = []; + this.hopper.length > 0 && pending_items.push(`${this.hopper.length} series`); + this.working_queue.length > 0 && pending_items.push(`${this.working_queue.length} file(s)`); + return pending_items.join(" and "); } get total_downloads_requested() { return size_display_string(this.queue_byte_size); } + get total_bytes_downloaded() { + return size_display_string(this.bytes_downloaded); + } + reset_queue_manager() { this.queue_byte_size = 0; + this.bytes_downloaded = 0; this.series_count = 0; this.collections = new Set([]); this.cases = new Set([]); @@ -273,12 +285,13 @@ require([ downloader_manager.statusMessage(`Encountered an error while downloading these files.`, 'error', "error", true, false); } if (event.data.message === 'done' || cancellation) { - let msg = `Download status: ${downloader_manager.in_progress} file(s) in progress, ${downloader_manager.queues.pending} in queue...`; + let msg = `Download status: ${downloader_manager.in_progress} file(s) downloading${downloader_manager.pending_msg}...`; let progType = "download"; + !cancellation && downloader_manager.add_to_done(parseFloat(event.data.size)); if (downloader_manager.queues.isEmpty()) { // This means the remaining downloads are all in-progress, or we cancelled - msg = cancellation ? "Cleaning up cancelled downloads..." : `Download status: ${downloader_manager.in_progress} file(s) in progress...`; - progType = cancellation ? "cancel" : "done"; + msg = cancellation ? "Cleaning up cancelled downloads..." : msg; + progType = cancellation ? "cancel" : (downloader_manager.in_progress <= 0 ? "done" : progType); } downloader_manager.progressUpdate(msg, progType); } @@ -371,7 +384,7 @@ require([ const fileHandle = await subDirectoryHandle.getFileHandle(fileName, {create: true}); const outputStream = await fileHandle.createWritable(); await inputStream.pipeTo(outputStream, {signal: abort_controller.signal}); - self.postMessage({message: "done", path: s3_url, localFilePath: filePath}); + self.postMessage({message: "done", path: s3_url, localFilePath: filePath, size: response.headers.get('content-length')}); } catch (error) { let msg = error.name || "Unnamed Error" + " when attempting to fetch URL " + s3_url; if(error.name === "AbortError" || (error.name === undefined && pending_abort)) { @@ -402,24 +415,40 @@ require([ } get overall_progress() { - return `${this.queues.total_downloads_requested} of data in ` + + return `${this.queues.total_bytes_downloaded} / ${this.queues.total_downloads_requested} downloaded `; + } + + get all_requested() { + return `${this.queues.total_downloads_requested} requested in ` + `${this.queues.collections.size} collection(s) / ` + `${this.queues.cases.size} case(s) / ` + `${this.queues.studies.size} ${this.queues.studies.size <= 1 ? "study" : "studies"} / ` + `${this.queues.series_count} series`; } + get pending_msg() { + let pending = this.queues.pending; + if(pending.length > 0) { + return `, ${pending} pending`; + } + return ""; + } + get in_progress() { return this.downloadWorkers.length - this.availableWorkers.length; } + add_to_done(downloaded_amount) { + this.queues.bytes_downloaded += downloaded_amount; + } + // Replaces the current floating message contents with a new message, including a new icon if provided statusMessage(message, type, icon, withClose, withCancel) { let messages = { "content": message }; if(!this.pending_cancellation && this.in_progress > 0) { - messages['header'] = this.overall_progress; + messages['header'] = this.all_requested; } this.progressDisplay.show(type, messages, icon, withCancel, withClose); } @@ -429,24 +458,28 @@ require([ progType = progType || "download"; let type = "info"; let icon = progType || true; + let prog = `${this.overall_progress}
    `; switch(progType) { case "cancel": type = "warning"; + prog = ""; break; case "done": type = "success"; break; case "error": + prog = ""; type = "error"; break; default: break; } + let messages = { - "content": message + "content": prog + message }; if(!this.pending_cancellation && this.in_progress > 0) { - messages['header'] = this.overall_progress; + messages['header'] = this.all_requested; } this.progressDisplay.update(type, messages, icon); } @@ -503,8 +536,7 @@ require([ 'metadata': item_to_download, 'directoryHandle': item_to_download['directory'] }); - let queue_msg = this.queues.pending > 0 ? `, ${this.queues.pending} in queue` : ""; - let msg = `Download status: ${this.in_progress} file(s) in progress${queue_msg}...`; + let msg = `Download status: ${this.in_progress} file(s) in progress${this.pending_msg}...`; this.progressUpdate(msg, "download"); } else { // For whatever reason, we didn't get an item to work on; put this worker back on the From 72d1efa320d36b9e3f5419aacc06165857d0e03e Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Wed, 13 Aug 2025 14:36:39 -0700 Subject: [PATCH 38/52] -> Widen display --- static/css/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/css/style.css b/static/css/style.css index 21d90a751..c29541dce 100755 --- a/static/css/style.css +++ b/static/css/style.css @@ -4517,7 +4517,7 @@ d-topics-list iframe { #floating-message { position: fixed; top: 180px; - width: 400px; + width: 500px; right: 40px; z-index: 9999; } From a85f3da2e4e33d1ee96b45634bc1869869a153a4 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Wed, 20 Aug 2025 13:27:17 -0700 Subject: [PATCH 39/52] -> #1443 --- static/js/collections_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/collections_list.js b/static/js/collections_list.js index 7460df51a..6c05e4646 100644 --- a/static/js/collections_list.js +++ b/static/js/collections_list.js @@ -18,7 +18,7 @@ require.config({ require(['jquery', 'datatables.net','jqueryui', 'bootstrap', 'base'], function($) { var collex_data_table = $('#collections-table').DataTable({ - "dom": '<"dataTables_controls"i<>lpf>rt<"bottom"><"clear">', + "dom": '<"dataTables_controls"i<>lpf>rt<"dataTables_controls"i<>lpf><"bottom"><"clear">', "pageLength": 100, 'order': [[ 2, 'desc' ], [ 1, 'asc' ]] }); From d50f5ef3c4679b4de52c27859cda6f2c7edb5979 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Wed, 20 Aug 2025 14:38:03 -0700 Subject: [PATCH 40/52] -> #1455 --- static/js/tables.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/static/js/tables.js b/static/js/tables.js index 2a87cfc37..c8b981c40 100644 --- a/static/js/tables.js +++ b/static/js/tables.js @@ -436,6 +436,10 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, "columns":[...columns] } ); + let collex_input = $('#proj_table_filter input'); + collex_input.after(``); + collex_input.addClass("table-search-box"); + collex_input.attr("data-search-type", "collection"); $('#proj_table').children('tbody').attr('id', 'projects_table'); $('#projects_table_head').find('th').each(function() { this.style.width = null; @@ -1520,6 +1524,10 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, // stops long enough for the callback to begin. Any calls sent in after the callback fires will see one call // queued and the others ignored, so that only a single call fires once the first one is completed. function filterTable(wrapper, type, input){ + // Ignore unsupported delay methods + if(update_methods[type] === undefined || update_methods[type] === null){ + return; + } if(SEARCH_PENDING) { if(SEARCH_QUEUE.length <= 0) { enqueueSearch(wrapper, type, input); @@ -1568,7 +1576,15 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, $('#rh_panel').on('click', '.dataTables_filter button.clear', function(){ let search_box = $(this).siblings('.table-search-box'); search_box.val(''); - search_box.trigger("change"); + let type = search_box.attr("data-search-type"); + if(type !== "collection") { + search_box.trigger("change"); + } else { + $('#proj_table').DataTable().search("").draw(); + setTimeout(function(){ + search_box.focus(); + }, 200); + } }); window.resetCartInTables = function(projArr){ From 2495a82d2c3e0a194995be053708d6fa6fb63aee Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Wed, 20 Aug 2025 21:23:13 -0700 Subject: [PATCH 41/52] -> #1409 --- idc/views.py | 401 --------------------------- static/css/search.css | 8 +- static/css/style.css | 10 +- static/js/citations_modal.js | 121 ++++++++ static/js/explore.js | 11 + static/js/image_search.js | 3 +- static/js/tables.js | 9 +- templates/idc/citations-modal.html | 26 ++ templates/idc/explore.html | 3 +- templates/idc/explore_data_core.html | 4 +- 10 files changed, 184 insertions(+), 412 deletions(-) create mode 100644 static/js/citations_modal.js create mode 100644 templates/idc/citations-modal.html diff --git a/idc/views.py b/idc/views.py index cf5540526..31da69246 100644 --- a/idc/views.py +++ b/idc/views.py @@ -240,32 +240,6 @@ def save_ui_hist(request): return JsonResponse({}, status=status) -def cart(request): - response={} - status=200 - field_list=['collection_id', 'PatientID', 'StudyInstanceUID', 'SeriesInstanceUID'] - try: - req = request.GET if request.GET else request.POST - partitions = json.loads(req.get('partitions', '{}')) - filtlist = json.loads(req.get('filtlist', '{}')) - limit = json.loads(req.get(limit, 1000)) - offset = json.loads(req.get(offset, 0)) - - get_cart_data(filtlist,partitions,limit, offset) - i=1 - - except Exception as e: - logger.error("[ERROR] While attempting to populate the table:") - logger.exception(e) - messages.error( - request, - "Encountered an error when attempting to populate the page - please contact the administrator." - ) - status = 400 - - return JsonResponse(response, status=status) - - # returns various metadata mappings for selected projects used in calculating cart selection # counts 'on the fly' client side def studymp(request): @@ -389,381 +363,6 @@ def populate_tables(request): return JsonResponse(response, status=status) -def populate_tables_old(request): - response = {} - status = 200 - tableRes = [] - studymp={} - - try: - req = request.GET if request.GET else request.POST - path_arr = [nstr for nstr in request.path.split('/') if nstr] - table_type = path_arr[len(path_arr)-1] - fields = None - collapse_on = None - filters = json.loads(req.get('filters', '{}')) - - offset = int(req.get('offset', '0')) - limit = int(req.get('limit', '500')) - serieslimit = int(req.get('serieslimit', '500')) - studylimit = int(req.get('studylimit', '500')) - if limit > settings.MAX_SOLR_RECORD_REQUEST: - logger.warning("[WARNING] Attempt to request more than MAX_SOLR_RECORD_REQUEST! ({})".format(limit)) - limit = settings.MAX_SOLR_RECORD_REQUEST - sort = req.get('sort', 'PatientID') - sortdir = req.get('sortdir', 'asc') - checkIds = json.loads(req.get('checkids', '[]')) - - diffA = [] - versions=[] - versions = ImagingDataCommonsVersion.objects.filter( - version_number__in=versions - ).get_data_versions(active=True) if len(versions) else ImagingDataCommonsVersion.objects.filter( - active=True - ).get_data_versions(active=True) - - aggregate_level = "SeriesInstanceUID" if table_type == 'series' else "StudyInstanceUID" - - data_types = [DataSetType.IMAGE_DATA,DataSetType.ANCILLARY_DATA,DataSetType.DERIVED_DATA] - data_sets = DataSetType.objects.filter(data_type__in=data_types) - aux_sources = data_sets.get_data_sources().filter( - source_type=DataSource.SOLR, - aggregate_level__in=["case_barcode", "sample_barcode", aggregate_level], - id__in=versions.get_data_sources().filter(source_type=DataSource.SOLR).values_list("id", flat=True) - ).distinct() - - sources = ImagingDataCommonsVersion.objects.get(active=True).get_data_sources( - active=True, source_type=DataSource.SOLR, - aggregate_level=aggregate_level - ) - - sortByField = True - custom_facets = None - custom_facets_order = None - - custom_facets_ex = {"tot_series": {"type": "terms", "field": "PatientID", "limit": limit, - "facet": {"unique_series": "unique(SeriesInstanceUID)"}, "domain": {"query": "*.*"}}} - - if table_type =="collections": - custom_facets = {"per_id": {"type": "terms", "field": "collection_id", "limit": limit, - "facet": {"unique_patient":"unique(PatientID)", "unique_study": "unique(StudyInstanceUID)", - "unique_series": "unique(SeriesInstanceUID)"}}, - "tot_series":{"type": "terms", "field": "collection_id", "limit": limit, - "facet": {"unique_series": "unique(SeriesInstanceUID)"}, "domain": {"query": "*.*"} } - } - tableIndex = 'PatientID' - fields = ['collection_id', 'access'] - facetfields = ['unique_patient','unique_study', 'unique_series'] - sort_arg = 'collection_id asc' - sortByField= True - sort = "collection_id" - - if table_type == 'cases': - custom_facets = {"per_id": {"type": "terms", "field": "PatientID", "limit": limit, - "facet": {"unique_study": "unique(StudyInstanceUID)", - "unique_series": "unique(SeriesInstanceUID)", - "sz":"sum(instance_size)"}}, - "per_id2": {"type": "terms", "field": "collection_id", "limit": limit, - "facet": {"unique_study": "unique(StudyInstanceUID)", - "unique_series": "unique(SeriesInstanceUID)", - "sz": "sum(instance_size)"}}, - "per_study": {"type": "terms", "field": "StudyInstanceUID", "limit": studylimit, - "facet": { - "unique_series": "unique(SeriesInstanceUID)", - "sz": "sum(instance_size)", "id": {"type":"terms", "field":"PatientID", "limit":4}} - }, - - "tot_series": {"type": "terms", "field": "PatientID", "limit": limit, - "facet": {"unique_series": "unique(SeriesInstanceUID)"}, "domain": {"query": "*.*"}} - - } - custom_facets_ex = { - "per_study": {"type": "terms", "field": "StudyInstanceUID", "limit": studylimit, - "facet": { - "unique_series": "unique(SeriesInstanceUID)", - "sz": "sum(instance_size)", "id": {"type": "terms", "field": "PatientID", "limit": 4}} - }, - "tot_series": {"type": "terms", "field": "PatientID", "limit": limit, - "facet": {"unique_series": "unique(SeriesInstanceUID)"}, "domain": {"query": "*.*"}} - } - tableIndex = 'PatientID' - fields = ['collection_id', 'PatientID','access'] - facetfields=['unique_study', 'unique_series'] - - if sort == 'collection_id': - sortByField = True - sort_arg = 'collection_id '+sortdir - - elif sort == 'PatientID': - sort_arg = 'PatientID ' + sortdir - - elif sort == 'StudyInstanceUID': - sortByField = False - sort_arg = 'unique_study ' + sortdir - custom_facets_order = { - "tot": "unique(PatientID)", - "per_id": { - "type": "terms", - "field": "PatientID", - "sort": sort_arg, - "offset": offset, - "limit": limit, - "facet": { - "unique_study": "unique(StudyInstanceUID)", - "unique_series": "unique(SeriesInstanceUID)" - } - } - } - elif sort == 'SeriesInstanceUID': - sortByField=False - sort_arg = 'unique_series '+sortdir - custom_facets_order = { - "tot": "unique(PatientID)", - "per_id": { - "type": "terms", "field": "PatientID", "sort": sort_arg, "offset": offset, "limit": limit, - "facet": { - "unique_study": "unique(StudyInstanceUID)", - "unique_series": "unique(SeriesInstanceUID)" - } - } - } - - if table_type == 'studies': - custom_facets = {"per_id": {"type": "terms", "field": "StudyInstanceUID", "limit": limit, - "facet": {"unique_series": "unique(SeriesInstanceUID)"}}, - "tot_series": {"type": "terms", "field": "PatientID", "limit": limit, - "facet": {"unique_series": "unique(SeriesInstanceUID)"}, - "domain": {"query": "*.*"}} - } - custom_facets_ex = { - "tot_series": {"type": "terms", "field": "PatientID", "limit": limit, - "facet": {"unique_series": "unique(SeriesInstanceUID)"}, "domain": {"query": "*.*"}} - } - - tableIndex = 'StudyInstanceUID' - fields = ['collection_id','PatientID','StudyInstanceUID','StudyDescription','Modality','StudyDate','access','crdc_series_uuid','gcs_bucket','aws_bucket'] - facetfields = ['unique_series'] - sort_arg = 'PatientID asc, StudyDate asc' - - if sort in ['PatientID','StudyInstanceUID', 'StudyDescription', 'StudyDate']: - sortByField = True - sort_arg = "{} {}".format(sort, sortdir) - if sort == 'PatientID': - sort_arg = sort_arg+', StudyDate asc' - - elif sort == 'SeriesInstanceUID': - sortByField = False - sort_arg = 'unique_series '+sortdir - - custom_facets_order = {"tot": "unique(SeriesInstanceUID)", - "per_id": {"type": "terms", "field": "StudyInstanceUID", - "sort": sort_arg,"offset": offset, "limit": limit, - "facet": {"unique_series": "unique(SeriesInstanceUID)"} - } - } - - if table_type == 'series': - custom_facets = {} - tableIndex = 'SeriesInstanceUID' - - fields = ['collection_id', 'PatientID', 'SeriesInstanceUID', 'StudyInstanceUID', 'SeriesDescription', 'SeriesNumber', - 'BodyPartExamined', 'Modality', 'access', 'crdc_series_uuid','gcs_bucket','aws_bucket', 'SOPClassUID'] - facetfields = [] - sortByField = True - sort_arg = 'StudyInstanceUID asc, SeriesNumber asc' if not sort else "{} {}, SeriesNumber asc".format( - sort, sortdir - ) - if sort == 'SeriesDescription': - custom_facets_order = {} - - order = {} - curInd = 0 - idsFilt = [] - - # check that any selected ids are still valid after the filter is updated. ids that are no longer valid are - # then deselected on the front end - if len(checkIds)>0: - selFilters=copy.deepcopy(filters) - selFilters[tableIndex] = checkIds - newCheckIds = get_collex_metadata( - selFilters, [tableIndex], record_limit=len(checkIds)+1,sources=sources, records_only=True, - collapse_on=tableIndex, counts_only=False, filtered_needed=False, aux_sources=aux_sources, sort=tableIndex+' asc', default_facets=False - ) - - nset = set([x[tableIndex] for x in newCheckIds['docs']]) - diffA = [x for x in checkIds if x not in nset] - - if sortByField: - idsReq = get_collex_metadata( - filters, fields, record_limit=limit, sources=sources, offset=offset, records_only=True, raw_format = True, - collapse_on=tableIndex, counts_only=False, filtered_needed=False, aux_sources=aux_sources, sort=sort_arg, default_facets=False - - ) - - cntTotal = idsReq['total'] - for rec in idsReq['docs']: - id = rec[tableIndex] - idsFilt.append(id) - order[id] = curInd - newRow = {} - for field in fields: - if field in rec: - newRow[field] = rec[field] - else: - newRow[field] = '' - tableRes.append(newRow) - curInd = curInd + 1 - filters[tableIndex]=idsFilt - - if not table_type == 'series': - #custom_facets["tot_series"]["domain"]["query"] = tableIndex + ": (" + " ".join(idsFilt) + ")" - - cntRecs = get_collex_metadata( - filters, fields, record_limit=limit, sources=sources, collapse_on=tableIndex, counts_only=True, - records_only=False, filtered_needed=False, custom_facets=custom_facets, raw_format=True, aux_sources=aux_sources, default_facets = False - ) - - - if table_type =='cases': - for rec in cntRecs['facets']['tot_series']['buckets']: - id = rec['val'] - tableRow = tableRes[order[id]] - totser = rec['unique_series'] - tableRow['maxseries'] = totser - - - for rec in cntRecs['facets']['per_id']['buckets']: - id = rec['val'] - tableRow = tableRes[order[id]] - for facet in facetfields: - if facet in rec: - tableRow[facet] = rec[facet] - else: - tableRow[facet] = 0 - - else: - idsReq = get_collex_metadata( - filters, fields, record_limit=limit, sources=sources, offset=offset, records_only=False, - collapse_on=tableIndex, counts_only=True, filtered_needed=False, custom_facets=custom_facets_order, - raw_format=True, aux_sources=aux_sources, default_facets=False - - ) - cntTotal = idsReq['facets']['tot'] - for rec in idsReq['facets']['per_id']['buckets']: - id = rec['val'] - idsFilt.append(id) - order[id] = curInd - newRow = {tableIndex: id} - for facet in facetfields: - if facet in rec: - newRow[facet]=rec[facet] - else: - newRow[facet] = 0 - tableRes.append(newRow) - curInd = curInd + 1 - filters[tableIndex] = idsFilt - custom_facets_ex["tot_series"]["domain"]["query"] = tableIndex + ": (" + " ".join(idsFilt) + ")" - - fieldRecs = get_collex_metadata( - filters, fields, record_limit=limit, sources=sources, records_only=False, collapse_on=tableIndex, raw_format = True, - counts_only=False, custom_facets=custom_facets_ex, filtered_needed=False, aux_sources=aux_sources, default_facets=False - ) - - if table_type == 'cases': - for rec in fieldRecs['facets']['tot_series']['buckets']: - id = rec['val'] - tableRow = tableRes[order[id]] - totser = rec['unique_series'] - tableRow['maxseries'] = totser - - for rec in fieldRecs['docs']: - id = rec[tableIndex] - tableRow = tableRes[order[id]] - for field in fields: - if not field == tableIndex: - if field in rec: - tableRow[field] = rec[field] - else: - tableRow[field] = '' - - - - if (table_type == 'cases'): - if sortByField: - extbl = cntRecs - else: - extbl = fieldRecs - for rec in extbl['facets']['per_study']['buckets']: - PatientID = rec['id']['buckets'][0]['val'] - tableRow = tableRes[order[PatientID]] - collection_id = tableRow['collection_id'][0] - studyid = rec['val'] - cnt = rec['unique_series'] - studymp[studyid] = {} - studymp[studyid]['val'] = [] - studymp[studyid]['proj'] = collection_id - studymp[studyid]['PatientID'] = PatientID - studymp[studyid]['cnt'] = cnt - if not 'studymp' in tableRow: - tableRow['studymp'] = {} - tableRow['studymp'][studyid] = cnt - - elif (table_type == 'studies'): - osources = ImagingDataCommonsVersion.objects.get(active=True).get_data_sources( - active=True, source_type=DataSource.SOLR, - aggregate_level="SeriesInstanceUID" - ) - - clist = [x['StudyInstanceUID'] for x in tableRes] - sfilters={} - sfilters['StudyInstanceUID'] = clist - idsEx = get_collex_metadata( - sfilters, ['collection_id', 'PatientID', 'SeriesInstanceUID', 'StudyInstanceUID'], - record_limit=serieslimit, sources=osources, offset=0, - records_only=True, - collapse_on='SeriesInstanceUID', counts_only=False, filtered_needed=False, - raw_format=True, default_facets=False - ) - - for res in idsEx['docs']: - collection_id = res['collection_id'] - patientid = res['PatientID'] - studyid = res['StudyInstanceUID'] - seriesid = res['SeriesInstanceUID'] - if not (studyid in studymp): - studymp[studyid] = {} - studymp[studyid]['val'] = [] - studymp[studyid]['proj'] = collection_id[0] - studymp[studyid]['PatientID'] = patientid - studymp[studyid]['val'].append(seriesid) - - for row in tableRes: - id = row['StudyInstanceUID'] - if id in studymp: - row['studymp'] = {} - row['studymp'][id] =studymp[id] - - - response["res"] = tableRes - response["cnt"] = cntTotal - response["diff"] = diffA - - if (table_type == 'cases') or (table_type == 'studies'): - response["studymp"]=studymp - - - except Exception as e: - logger.error("[ERROR] While attempting to populate the table:") - logger.exception(e) - messages.error( - request, - "Encountered an error when attempting to populate the page - please contact the administrator." - ) - status = 400 - - return JsonResponse(response, status=status) - - # Data exploration and cohort creation page def explore_data_page(request, filter_path=False, path_filters=None): context = {'request': request} diff --git a/static/css/search.css b/static/css/search.css index 6ce3c7dc5..ece052ebb 100644 --- a/static/css/search.css +++ b/static/css/search.css @@ -395,16 +395,14 @@ tr { background-color: #b7cfe5; } -.export-button:hover, .btnGroupDropViewers:hover { +.export-button:hover, .citations-button:hover, .btnGroupDropViewers:hover { cursor: pointer; } #projects_table td.cartnumholder, #projects_table_head th.cartnumholder, #cases_table td.cartnumholder, #cases_table_head th.cartnumholder, -#studies_table td.cartnumholder, #studies_table_head th.cartnumholder -{ +#studies_table td.cartnumholder, #studies_table_head th.cartnumholder { width: 10%; - } #projects_table td.ckbx:not(.cartnumholder), #projects_table_head th.ckbx:not(.cartnumholder), @@ -412,7 +410,7 @@ tr { #studies_table_head th.ckbx:not(.cartnumholder), #studies_table td.ckbx:not(.cartnumholder), #series_table_head th.ckbx, #series_table td.ckbx { width: 5%; - } +} #search_def_stats { padding-top: 8px; } diff --git a/static/css/style.css b/static/css/style.css index c29541dce..dcd2d1db8 100755 --- a/static/css/style.css +++ b/static/css/style.css @@ -2543,10 +2543,18 @@ section .min-max { border-radius: 4px; } +.modal pre { + margin-top: 10px; +} + pre.bq-string { max-height: 600px; width: 570px; - margin-top: 10px; +} + +pre.citations-list { + white-space: pre-wrap; + word-break: break-word; } .bq-string-display img { diff --git a/static/js/citations_modal.js b/static/js/citations_modal.js new file mode 100644 index 000000000..53cc14de9 --- /dev/null +++ b/static/js/citations_modal.js @@ -0,0 +1,121 @@ +/** + * + * Copyright 2020-2025, Institute for Systems Biology + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +require.config({ + baseUrl: STATIC_FILES_URL + 'js/', + paths: { + jquery: 'libs/jquery-3.7.1.min', + bootstrap: 'libs/bootstrap.min', + jqueryui: 'libs/jquery-ui.min', + underscore: 'libs/underscore-min', + tablesorter: 'libs/jquery.tablesorter.min', + assetscore: 'libs/assets.core', + assetsresponsive: 'libs/assets.responsive', + jquerydt: 'libs/jquery.dataTables.min', + base: 'base', + tippy: 'libs/tippy-bundle.umd.min', + '@popperjs/core': 'libs/popper.min', + session_security: 'session_security/script' + + }, + shim: { + '@popperjs/core': { + exports: "@popperjs/core" + }, + 'tippy': { + exports: 'tippy', + deps: ['@popperjs/core'] + }, + 'bootstrap': ['jquery'], + 'jqueryui': ['jquery'], + 'jquerydt': ['jquery'], + 'underscore': {exports: '_'}, + 'tablesorter': ['jquery'], + 'assetscore': ['jquery', 'bootstrap', 'jqueryui'], + 'session_security': ['jquery'], + 'assetsresponsive': ['jquery', 'bootstrap', 'jqueryui'] + } +}); + +require([ + 'jquery', + 'jqueryui', + 'tippy', + 'base', // Do not remove + 'bootstrap', + 'assetscore', + 'assetsresponsive', +], function($, jqueryui, tippy, base) { + + A11y.Core(); + + var downloadToken = new Date().getTime(); + + $('#citations-modal').on('show.bs.modal', async function(event) { + let button = $(event.relatedTarget); + let dois = button.attr('data-dois').split("||"); + let cites_list = $("#citations-modal .citations-list"); + let copy_cites = $("#citations-modal .copy-this"); + cites_list.html(` + Formatting citation(s)... + `); + let citations = []; + await Promise.all(dois.map(async function(cite){ + let response = await fetch(`https://doi.org/${cite}`, { + headers: { + "Accept": "text/x-bibliography; style=mla" + } + }); + if (!response.ok) { + citations.push(`Encountered an error requesting DOI ${cite}`); + } else { + citations.push(await response.text()); + } + })); + cites_list.html(citations.join("\n\n")); + copy_cites.attr('content', citations.join("\n\n")); + }); + + $('#citations-modal').on('hide.bs.modal', function() { + + }); + + $('#export-manifest-modal').on('hidden.bs.modal', function() { + $(".citations-list").empty(); + }); + + $('.copy-this').on('click', function(e) { + // copy the entire set + }); + + tippy.delegate('#citations-modal', { + content: 'Copied!', + theme: 'blue', + placement: 'right', + arrow: true, + interactive: true, // This is required for any table tooltip to show at the appropriate spot! + target: '.copy-this', + onShow(instance) { + setTimeout(function() { + instance.hide(); + }, 1000); + }, + trigger: "click", + maxWidth: 85 + }); +}); diff --git a/static/js/explore.js b/static/js/explore.js index 98c4e8024..17ac294e1 100644 --- a/static/js/explore.js +++ b/static/js/explore.js @@ -546,6 +546,17 @@ require([ maxWidth: 85 }); + + tippy.delegate('.series-table', { + content: 'Get the citation list for this series.', + theme: 'dark', + placement: 'left', + arrow: false, + interactive:true, + target: '.citations-button', + maxWidth: 200 + }); + tippy.delegate('.cases-table', { content: 'Download all of the image instances in this case.', theme: 'dark', diff --git a/static/js/image_search.js b/static/js/image_search.js index 3476340ce..7ca2853ff 100644 --- a/static/js/image_search.js +++ b/static/js/image_search.js @@ -73,7 +73,8 @@ require([ 'jquerydt', 'jqueryui', 'bootstrap', - 'downloader' + 'downloader', + 'citations_modal' ], function(plotutils,filterutils,sliderutils, tables, cartutils, tippy,$, _, base) { diff --git a/static/js/tables.js b/static/js/tables.js index c8b981c40..73d902c62 100644 --- a/static/js/tables.js +++ b/static/js/tables.js @@ -1245,8 +1245,9 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, {className: "col1 body-part-examined", "targets": [5]}, {className: "series-description", "targets": [6]}, {className: "ohif open-viewer", "targets": [7]}, - {className: "manifest-col", "targets": [8]}, - {className: "download-col", "targets": [9]}, + {className: "series-citations", "targets": [8]}, + {className: "manifest-col", "targets": [9]}, + {className: "download-col", "targets": [10]} ], "columns": [ {"type": "html", "orderable": false, render: function () { @@ -1345,6 +1346,10 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, ''; } } + }, { + "type": "html", "orderable": false, data: 'source_DOI', render: function (data) { + return '' + } }, { "type":"html", "orderable": false, diff --git a/templates/idc/citations-modal.html b/templates/idc/citations-modal.html new file mode 100644 index 000000000..7a088626c --- /dev/null +++ b/templates/idc/citations-modal.html @@ -0,0 +1,26 @@ + + diff --git a/templates/idc/explore.html b/templates/idc/explore.html index 51113c937..3cdf9fdb6 100644 --- a/templates/idc/explore.html +++ b/templates/idc/explore.html @@ -89,7 +89,7 @@

    Explore Image Data

    {% with user_is_social=request.user|has_social is_cohort=False %} {% include "cohorts/export-manifest-modal.html" with export_uri=export_uri user_is_social=user_is_social is_cohort=is_cohort %} {% endwith %} - +{% include "idc/citations-modal.html" %} {% include "idc/explore_data_core.html" %} {% endblock %} @@ -156,6 +156,7 @@

    Explore Image Data

    + {% endblock %} \ No newline at end of file diff --git a/templates/idc/explore_data_core.html b/templates/idc/explore_data_core.html index a204fb3ba..f25138af4 100644 --- a/templates/idc/explore_data_core.html +++ b/templates/idc/explore_data_core.html @@ -917,7 +917,9 @@

    Body Part Examined Series Description View - + + + From e461c6c5d656a31ccf08ced988f29e41642d47d5 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Thu, 21 Aug 2025 11:31:53 -0700 Subject: [PATCH 42/52] -> #1186 --- static/css/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/css/style.css b/static/css/style.css index dcd2d1db8..098f96e50 100755 --- a/static/css/style.css +++ b/static/css/style.css @@ -4524,7 +4524,7 @@ d-topics-list iframe { #floating-message { position: fixed; - top: 180px; + bottom: 50px; width: 500px; right: 40px; z-index: 9999; From f10543471274bc6ffef7e2018fc681c4e8e77fa5 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Thu, 21 Aug 2025 11:41:02 -0700 Subject: [PATCH 43/52] -> #1476 --- static/js/cohorts/export-manifest.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/cohorts/export-manifest.js b/static/js/cohorts/export-manifest.js index b0843ccf6..2cbdb3313 100644 --- a/static/js/cohorts/export-manifest.js +++ b/static/js/cohorts/export-manifest.js @@ -394,8 +394,8 @@ require([ file_name.attr("name-base", "cohort_" + cohort_ids.join("_") + export_manifest_modal.data('file-timestamp')); } } - let s5cmd_manifest_filename = ""; - let idc_index_manifest_filename = ""; + let s5cmd_manifest_filename = ""; + let idc_index_manifest_filename = ""; let s5cmd_endpoint_url = $('input[name="loc_type_s5cmd"]:checked').attr('data-endpoint-url'); let s5cmd_text = `s5cmd --no-sign-request --endpoint-url ${s5cmd_endpoint_url} run ${s5cmd_manifest_filename}`; From 0f1052660bc11b60dcf163d8151e8c09c39a34bd Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Thu, 21 Aug 2025 13:21:09 -0700 Subject: [PATCH 44/52] -> #1409 swap to vancouver, remove button from series table in prep to add it to filter panel --- static/js/citations_modal.js | 21 +++++++++++++-------- static/js/explore.js | 4 ++-- static/js/tables.js | 9 ++------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/static/js/citations_modal.js b/static/js/citations_modal.js index 53cc14de9..d794b524a 100644 --- a/static/js/citations_modal.js +++ b/static/js/citations_modal.js @@ -62,6 +62,8 @@ require([ 'assetsresponsive', ], function($, jqueryui, tippy, base) { + var DOI_CACHE = {}; + A11y.Core(); var downloadToken = new Date().getTime(); @@ -76,16 +78,19 @@ require([ `); let citations = []; await Promise.all(dois.map(async function(cite){ - let response = await fetch(`https://doi.org/${cite}`, { - headers: { - "Accept": "text/x-bibliography; style=mla" + if(!DOI_CACHE[cite]) { + let response = await fetch(`https://doi.org/${cite}`, { + headers: { + "Accept": "text/x-bibliography; style=elsevier-vancouver-no-et-al" + } + }); + if (!response.ok) { + citations.push(`Encountered an error requesting DOI ${cite}`); + } else { + DOI_CACHE[cite] = await response.text(); } - }); - if (!response.ok) { - citations.push(`Encountered an error requesting DOI ${cite}`); - } else { - citations.push(await response.text()); } + citations.push(DOI_CACHE[cite]); })); cites_list.html(citations.join("\n\n")); copy_cites.attr('content', citations.join("\n\n")); diff --git a/static/js/explore.js b/static/js/explore.js index 17ac294e1..97225d44f 100644 --- a/static/js/explore.js +++ b/static/js/explore.js @@ -547,8 +547,8 @@ require([ }); - tippy.delegate('.series-table', { - content: 'Get the citation list for this series.', + tippy.delegate('.filter-display-panel', { + content: 'Get the citation list for this cohort.', theme: 'dark', placement: 'left', arrow: false, diff --git a/static/js/tables.js b/static/js/tables.js index 73d902c62..95862e361 100644 --- a/static/js/tables.js +++ b/static/js/tables.js @@ -1245,9 +1245,8 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, {className: "col1 body-part-examined", "targets": [5]}, {className: "series-description", "targets": [6]}, {className: "ohif open-viewer", "targets": [7]}, - {className: "series-citations", "targets": [8]}, - {className: "manifest-col", "targets": [9]}, - {className: "download-col", "targets": [10]} + {className: "manifest-col", "targets": [8]}, + {className: "download-col", "targets": [9]} ], "columns": [ {"type": "html", "orderable": false, render: function () { @@ -1346,10 +1345,6 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, ''; } } - }, { - "type": "html", "orderable": false, data: 'source_DOI', render: function (data) { - return '' - } }, { "type":"html", "orderable": false, From 243c5ab6e0b051e10b040bd1a414243faf4aeeac Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Thu, 21 Aug 2025 16:45:02 -0700 Subject: [PATCH 45/52] -> #1186, 3 significant digits on file sizes --- static/js/downloader.js | 7 +++++-- static/js/tables.js | 17 ++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index d9da43fdb..fccd32ec3 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -46,7 +46,7 @@ require([ while(log_level%byte_count >= log_level) { byte_count-=3; } - let bytes = (Math.round((size/(Math.pow(10,log_level)))*100)/100).toFixed(2); + let bytes = (Math.round((size/(Math.pow(10,log_level)))*100)/100).toFixed(3); return `${bytes} ${byte_level[(byte_count/3)]}` ; } @@ -583,7 +583,10 @@ require([ downloader_manager.cancel(); }); - $('.container-fluid').on('click', '.download-all-instances', async function () { + $('.container-fluid').on('click', '.download-all-instances', async function (event) { + // Don't let the row handler do anything from here on out or the FileSystem Access API will get ornery + event.preventDefault(); + event.stopImmediatePropagation(); const clicked = $(this); let directoryHandle = await window.showDirectoryPicker({ id: 'idc-downloads', diff --git a/static/js/tables.js b/static/js/tables.js index 95862e361..31b16fba1 100644 --- a/static/js/tables.js +++ b/static/js/tables.js @@ -852,6 +852,10 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, $(row).on('click', function(event) { handleRowClick("studies", row, event, ids); }); + let row_id = $(row).attr("id"); + $(`[id="${row_id}"] .download-all-instances`).on('click',async function(){ + + }); }, "columnDefs": [ {className: "ckbx seriesview", "targets": [0]}, @@ -1142,8 +1146,6 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, } else { pageRows = 10; } - console.debug($('#series_tab')); - console.debug($('#series_tab').DataTable()); $('#series_tab').DataTable().destroy(); try { $('#series_tab').DataTable({ @@ -2172,6 +2174,15 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, } } + async function bob() { + let directoryHandle = await window.showDirectoryPicker({ + id: 'idc-downloads', + startIn: 'downloads', + mode: 'readwrite', + }); + return directorHandle; + } + const handleRowClick = function(tabletype, row, event, ids){ let elem = event.target; if ($(elem).hasClass('collection_info')) { @@ -2182,7 +2193,7 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, //do nothing here. opening the viewer } else if ($(elem).hasClass('download-col') || $(elem).hasClass('manifest-col') || $(elem).parentsUntil('tr').hasClass('download-col') || $(elem).parentsUntil('tr').hasClass('manifest-col')) { - //do nothing here. downloading a manifest or instances + bob(); } else if ($(elem).hasClass('shopping-cart') || $(elem).hasClass('shopping-cart-holder')) { handleCartClick(tabletype, row, elem, ids); } From f4d1bcae94eeba66bfd5403a2bd74891d15d089d Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Thu, 21 Aug 2025 18:02:14 -0700 Subject: [PATCH 46/52] -> #1186, progress fixes, get rid of the annoying error --- static/js/downloader.js | 35 +++++++++++++++++++++++++---------- static/js/tables.js | 6 ++---- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index d9da43fdb..bf387826c 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -46,7 +46,7 @@ require([ while(log_level%byte_count >= log_level) { byte_count-=3; } - let bytes = (Math.round((size/(Math.pow(10,log_level)))*100)/100).toFixed(2); + let bytes = (Math.round((size/(Math.pow(10,byte_count)))*100)/100).toFixed(3); return `${bytes} ${byte_level[(byte_count/3)]}` ; } @@ -277,6 +277,10 @@ require([ } function workerOnMessage(event) { + if(event.data.message === 'update') { + downloader_manager.add_to_done(parseFloat(event.data.size)); + return; + } let thisWorker = event.target; let true_error = event.data.message === 'error' && event.data.error.name !== "AbortError"; let cancellation = downloader_manager.pending_cancellation || (event.data.message === 'error' && event.data.error.name === "AbortError"); @@ -287,7 +291,6 @@ require([ if (event.data.message === 'done' || cancellation) { let msg = `Download status: ${downloader_manager.in_progress} file(s) downloading${downloader_manager.pending_msg}...`; let progType = "download"; - !cancellation && downloader_manager.add_to_done(parseFloat(event.data.size)); if (downloader_manager.queues.isEmpty()) { // This means the remaining downloads are all in-progress, or we cancelled msg = cancellation ? "Cleaning up cancelled downloads..." : msg; @@ -365,7 +368,12 @@ require([ const fileName = metadata['instance']; try { const directoryHandle = event.data['directoryHandle']; - response = await fetch(s3_url, { + const seriesDirectory = modality + "_" + seriesInstanceUID; + const filePath = [collection_id, patientID, studyInstanceUID, seriesDirectory].join("/"); + const subDirectoryHandle = await createNestedDirectories(directoryHandle, filePath); + const fileHandle = await subDirectoryHandle.getFileHandle(fileName, {create: true}); + const outputStream = await fileHandle.createWritable(); + let response = await fetch(s3_url, { signal: abort_controller.signal }); if (!response.ok) { @@ -377,13 +385,20 @@ require([ } return; } - const inputStream = await response.body; - const seriesDirectory = modality + "_" + seriesInstanceUID; - const filePath = [collection_id, patientID, studyInstanceUID, seriesDirectory].join("/"); - const subDirectoryHandle = await createNestedDirectories(directoryHandle, filePath); - const fileHandle = await subDirectoryHandle.getFileHandle(fileName, {create: true}); - const outputStream = await fileHandle.createWritable(); - await inputStream.pipeTo(outputStream, {signal: abort_controller.signal}); + let counts = 0; + let read = 0; + for await(const chunk of response.body) { + if(abort_controller.signal.aborted) break; + outputStream.write(chunk); + counts += 1; + read += chunk.length; + if(counts%20 === 0) { + self.postMessage({message: "update", size: read}); + read = 0; + } + } + read > 0 && self.postMessage({message: "update", size: read}); + await outputStream.close(); self.postMessage({message: "done", path: s3_url, localFilePath: filePath, size: response.headers.get('content-length')}); } catch (error) { let msg = error.name || "Unnamed Error" + " when attempting to fetch URL " + s3_url; diff --git a/static/js/tables.js b/static/js/tables.js index 95862e361..7f71f39e2 100644 --- a/static/js/tables.js +++ b/static/js/tables.js @@ -1142,8 +1142,6 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, } else { pageRows = 10; } - console.debug($('#series_tab')); - console.debug($('#series_tab').DataTable()); $('#series_tab').DataTable().destroy(); try { $('#series_tab').DataTable({ @@ -2177,12 +2175,12 @@ define(['cartutils','filterutils','tippy','jquery', 'base'], function(cartutils, if ($(elem).hasClass('collection_info')) { displayInfo($(elem)); } else if ($(elem).hasClass('copy-this') || $(elem).hasClass('fa-copy')) { - //do nothing. handled by triggers in base.js and explore.js to copy to clipboard and show a copy tooltip + //do nothing. handled by triggers in base.js and explore.js to copy to clipboard and show a copy tooltip } else if ($(elem).hasClass('ohif') || $(elem).parentsUntil('tr').hasClass('ohif')) { //do nothing here. opening the viewer } else if ($(elem).hasClass('download-col') || $(elem).hasClass('manifest-col') || $(elem).parentsUntil('tr').hasClass('download-col') || $(elem).parentsUntil('tr').hasClass('manifest-col')) { - //do nothing here. downloading a manifest or instances + // Handled by delegates } else if ($(elem).hasClass('shopping-cart') || $(elem).hasClass('shopping-cart-holder')) { handleCartClick(tabletype, row, elem, ids); } From 27aef1f8ec4fb59d0fa3928153a4c9f3facb512d Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Fri, 22 Aug 2025 11:25:06 -0700 Subject: [PATCH 47/52] -> #1186, progress fix -> New Privacy policy --- static/js/downloader.js | 2 +- templates/idc/privacy.html | 134 +++++++++++++++---------------------- 2 files changed, 56 insertions(+), 80 deletions(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index bf387826c..c9afc02c1 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -46,7 +46,7 @@ require([ while(log_level%byte_count >= log_level) { byte_count-=3; } - let bytes = (Math.round((size/(Math.pow(10,byte_count)))*100)/100).toFixed(3); + let bytes = (Math.round((size/(Math.pow(10,byte_count)))*1000)/1000).toFixed(3); return `${bytes} ${byte_level[(byte_count/3)]}` ; } diff --git a/templates/idc/privacy.html b/templates/idc/privacy.html index ce0922b8b..610ba6526 100644 --- a/templates/idc/privacy.html +++ b/templates/idc/privacy.html @@ -22,18 +22,19 @@

    Privacy Policy

    - Protecting your privacy is very important to us. Our Web site links to other National Institutes of Health (NIH) sites, - federal agency sites, and private organizations. Once you leave the primary IDC site, you are subject to the privacy - policy for the site(s) you are visiting. In addition to name and email address, we collect some data about your visit to - our Web site to help us better understand how the public uses the site and how to make it more helpful. We collect - information from visitors who read, browse, and/or download information from our Web site. We never collect information - for commercial marketing or any purpose unrelated to the IDC mission and goals. + Protecting your privacy is very important to us. Our website links to other National Institutes of + Health (NIH) sites, federal agency sites, and private organizations. Once you leave the primary IDC + site, you are subject to the privacy policy for the site(s) you are visiting. We collect some data + about your visit to our website to help us better understand how the public uses the site and how to + make it more helpful. We collect information from visitors who read, browse, and/or download + information from our website. We never collect information for commercial marketing or any purpose + unrelated to the IDC mission and goals.

    Types of Information Collected

    - When you browse through any Web site, certain information about your visit can be collected. We automatically collect and - store the following type of information about your visit: + When you browse through our website, certain information about your visit can be collected. We + automatically collect and store the following types of information about your visit:

    • Domain from which you access the Internet
    • @@ -41,25 +42,32 @@

      Types of Information Collected

    • Operating system and information about the browser used when visiting the site
    • Date and time of your visit
    • Pages you visited
    • -
    • Address of the Web site that connected you to the IDC site (such as google.com or bing.com)
    • -
    • Information about what datasets are visited, linked to the IP address used to visit it
    • +
    • Address of the website that connected you to the IDC site (such as google.com or bing.com)
    • +
    • Information about which datasets are visited, linked to the IP address used to visit it

    - We use this information to measure the number of visitors to our site and its various sections and datasets to help make our site more useful to visitors. + We use this information to measure the number of visitors to our site and its various sections and + datasets to help make our site more useful to visitors. +

    +

    + When you download IDC-maintained data, you are interacting with Google Cloud Platform (GCP) and/or + Amazon Web Services (AWS) cloud storage buckets. While sign-in is not required to download data from + those public buckets, data may be collected if you are signed in. On AWS, your email ID may be + logged when downloading. On GCP, if you use the optional “requester pays”, your billing project ID + will be logged when downloading.

    How IDC Collects Information

    - IDC uses Google Analytics, Google Cloud Platform StackDriver Logging, and the IDC login process to collect - the information in the bulleted list in the Types of Information Collected section above. Google Analytics and - StackDriver gather information automatically and continuously. No Personally Identifiable Information (PII) is - collected from these two systems. When you log in to our IDC site using a Google Account or a personal email, - we collect your email address and name (as stored in your Google Account profile if applicable). IDC staff - conducts analyses and reports on the aggregated data, and those reports are only available to the NCI and NIH. + IDC uses Google Analytics and Google Cloud Platform StackDriver Logging to collect the information + in the Types of Information Collected section above. Google Analytics and StackDriver gather + information automatically and continuously. No Personally Identifiable Information (PII) is + collected by these two systems. IDC staff conducts analyses and reports on the aggregated data, and + those reports are only available to the NCI and NIH.

    - IDC retains the data from Google Analytics, StackDriver, and the login process as long as needed to support - the mission of the IDC website and the NCI. + IDC retains the data from Google Analytics and StackDriver as long as needed to support the mission + of the IDC website and the NCI.

    How IDC Uses Cookies

    @@ -68,88 +76,56 @@

    How IDC Uses Cookies

    M-10-22, Guidance for Online Use of Web Measurement and Customization Technologies (link is external) allows Federal agencies to use session and persistent cookies. -

    +

    - When you visit any Web site, its server may generate a piece of text known as a "cookie" to place on your - computer. The cookie allows the server to "remember" specific information about your visit while you are - connected. + When you visit any website, its server may generate a piece of text known as a "cookie" to place on + your computer. The cookie allows the server to "remember" specific information about your visit + while you are connected.

    - The cookie makes it easier for you to use the dynamic features of Web pages. Cookies from IDC pages only - collect information about your browser’s visit to the site; they do not collect personal information about you. + The cookie makes it easier for you to use the dynamic features of web pages. Cookies from IDC pages + only collect information about your browser’s visit to the site; they do not collect personal + information about you.

    - There are two types of cookies, single session (temporary), and multi-session (persistent). Session cookies last - only as long as your Web browser is open. Once you close your browser, the cookie disappears. Persistent cookies - are stored on your computer for longer periods. + There are two types of cookies: single-session (temporary) and multi-session (persistent). Session + cookies last only as long as your web browser is open. Once you close your browser, the cookie + disappears. Persistent cookies are stored on your computer for longer periods.

    Session Cookies

    - IDC uses session cookies for technical purposes such as to enable better navigation through our site. These - cookies let our server know that you are continuing a visit to our site. The OMB Memo 10-22 Guidance defines our - use of session cookies as "Usage Tier 1 — Single Session.” The policy says, "This tier encompasses any use of - single session web measurement and customization technologies." + IDC uses session cookies for technical purposes, such as to enable better navigation through our + site. These cookies let our server know that you are continuing a visit to our site. The OMB Memo + 10-22 Guidance defines our use of session cookies as "Usage Tier 1 — Single Session.” The policy + says, "This tier encompasses any use of single session web measurement and customization + technologies."

    Persistent Cookies

    - IDC uses persistent cookies to enable Google Analytics to differentiate between new and returning IDC - visitors. Persistent cookies remain on your computer between visits to IDC until they expire. The OMB Memo - 10-22 Guidance defines our use of persistent cookies as "Usage Tier 2 — Multi-session without Personally - Identifiable Information (PII).” The policy says, "This tier encompasses any use of multi-session Web - measurement and customization technologies when no PII is collected." -

    - -

    How Personal Information Is Protected

    -

    - If you choose to log in to our site with your Google Account we will receive your email address and name as - listed in your Google Account profile. If you choose to make an account on our site using a personal email - address, we will receive only that email address; any other profile information is optional and must be entered - by you. We will maintain this information for security purposes only. We will safeguard the information you - provide to us in accordance with the Privacy Act of 1974, as amended (5 U.S.C. Section 552a). + IDC uses persistent cookies to enable Google Analytics to differentiate between new and returning + IDC visitors. Persistent cookies remain on your computer between visits to IDC until they expire. + The OMB Memo 10-22 Guidance defines our use of persistent cookies as "Usage Tier 2 — Multi-session + without Personally Identifiable Information (PII).” The policy says, "This tier encompasses any use + of multi-session web measurement and customization technologies when no PII is collected."

    Data Safeguarding and Privacy

    - -

    - IDC uses web measurement and customization technologies to help our Web sites function better for visitors - and to better understand how the public uses the online resources we provide. All uses of web-based technologies - comply with existing policies with respect to privacy and data safeguarding standards. Information Technology - (IT) systems operated by IDC are assessed using Privacy Impact Assessments (PIAs) posted for public view on - the Department of Health and Human Services (DHHS) Web site - (link is external). IDC conducts and publishes a PIA for each use of a third-party website and application - (TPWA) as they may have a different functionality or practice. TPWA PIAs are posted for public view on the - DHHS Web site (link is external). -

    -

    - Groups of records that contain information about an individual and are designed to be retrieved by the - individual’s name or other personal identifier linked to the individual are covered by the Privacy Act of 1974, - as amended (5 U.S.C. Section 552a). For these records, NIH Systems of Record Notices are published in the - Federal Register and posted on the - NIH Senior Official for Privacy Website (link is external). When web measurement and customization - technologies are used, the Privacy Policy/Notice must provide: + IDC uses web measurement and customization technologies to help our websites function better for + visitors and to better understand how the public uses the online resources we provide. All uses of + web-based technologies comply with existing policies with respect to privacy and data safeguarding + standards.

    -
      -
    • Purpose of the web measurement and/or customization technology
    • -
    • Usage tier, session type, and technology used
    • -
    • Nature of the information collected
    • -
    • Purpose and use of the information
    • -
    • Whether and to whom the information will be disclosed
    • -
    • Privacy safeguards applied to the information
    • -
    • Data retention policy for the information
    • -
    • Whether the technology is enabled by default or not and why
    • -
    • Statement that opting-out still permits users to access comparable information or services
    • -
    • Identities of all third-party vendors involved in the measurement and customization process
    • -

    Data Retention and Access Limits

    IDC will retain data collected using the following technologies long enough to achieve the specified - objective for which they were collected. The data generated from these activities falls under the National - Archives and Records Administration (NARA) General Records Schedule (GRS) 20-item IC 'Electronic Records,' and - will be handled per the + objective for which they were collected. The data generated from these activities falls under the + National Archives and Records Administration (NARA) General Records Schedule (GRS) 20-item IC + 'Electronic Records,' and will be handled per the + requirements of that schedule (link is external).

    From b76133338d1cf352ca4322db80659b6bb2d0eae5 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Tue, 26 Aug 2025 12:26:17 -0700 Subject: [PATCH 48/52] -> #1835 --- templates/idc/privacy.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/idc/privacy.html b/templates/idc/privacy.html index 610ba6526..6c10cb9c7 100644 --- a/templates/idc/privacy.html +++ b/templates/idc/privacy.html @@ -121,7 +121,7 @@

    Data Safeguarding and Privacy

    Data Retention and Access Limits

    - IDC will retain data collected using the following technologies long enough to achieve the specified + IDC will retain data collected long enough to achieve the specified objective for which they were collected. The data generated from these activities falls under the National Archives and Records Administration (NARA) General Records Schedule (GRS) 20-item IC 'Electronic Records,' and will be handled per the From cac83477490a6c26d75d0763db6ce98042047454 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Wed, 27 Aug 2025 17:50:53 -0700 Subject: [PATCH 49/52] -> Better download estimates --- static/js/downloader.js | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index c9afc02c1..86e3a317f 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -188,6 +188,7 @@ require([ queue_byte_size = 0; bytes_downloaded = 0; series_count = 0; + start_time = -1; collections = new Set([]); cases = new Set([]); studies = new Set([]); @@ -212,6 +213,14 @@ require([ return size_display_string(this.bytes_downloaded); } + get total_duration_estimate() { + if(this.start_time < 0 || this.bytes_downloaded <=0) + return "Calculating..." + let rate = this.bytes_downloaded/((Date.now()-this.start_time)/1000); + let sec_remaining = Math.round((this.queue_byte_size-this.bytes_downloaded)/rate).toFixed(0); + return `~${((sec_remaining-(sec_remaining%60))/60)}m${(sec_remaining%60) > 9 ? ':' : ':0'}${sec_remaining%60}s remaining @ ${size_display_string(rate)}ps`; + } + reset_queue_manager() { this.queue_byte_size = 0; this.bytes_downloaded = 0; @@ -220,12 +229,17 @@ require([ this.cases = new Set([]); this.studies = new Set([]); this.cancellation_underway = false; + this.start_time = -1; } get active_requests() { return (this.working_queue.length > 0); } + set_start_time() { + if(this.start_time < 0) this.start_time = Date.now(); + } + cancel() { this.cancellation_underway = true; this._emptyQueues(); @@ -263,6 +277,7 @@ require([ } async get_download_item() { + this.set_start_time(); await this._update_queue(); if(this.working_queue.length > 0 && !this.cancellation_underway) { return this.working_queue.pop(); @@ -279,6 +294,9 @@ require([ function workerOnMessage(event) { if(event.data.message === 'update') { downloader_manager.add_to_done(parseFloat(event.data.size)); + let msg = `Download status: ${downloader_manager.in_progress} file(s) downloading${downloader_manager.pending_msg}...`; + let progType = "download"; + downloader_manager.progressUpdate(msg, progType); return; } let thisWorker = event.target; @@ -385,14 +403,16 @@ require([ } return; } - let counts = 0; let read = 0; + let lastReportTime = -1; + let thisChunkTime = -1; for await(const chunk of response.body) { - if(abort_controller.signal.aborted) break; + if(abort_controller.signal.aborted) break; outputStream.write(chunk); - counts += 1; + thisChunkTime = Date.now(); read += chunk.length; - if(counts%20 === 0) { + if(lastReportTime < 0 || ((thisChunkTime-lastReportTime) > 2000)) { + lastReportTime = Date.now(); self.postMessage({message: "update", size: read}); read = 0; } @@ -430,7 +450,7 @@ require([ } get overall_progress() { - return `${this.queues.total_bytes_downloaded} / ${this.queues.total_downloads_requested} downloaded `; + return `${this.queues.total_bytes_downloaded} / ${this.queues.total_downloads_requested} downloaded (${this.queues.total_duration_estimate})`; } get all_requested() { From ebd9807503475bf0887230e305c5114f449a2a78 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Wed, 27 Aug 2025 18:17:22 -0700 Subject: [PATCH 50/52] -> Report rate is 2s from thread and 1s from display --- static/js/downloader.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/static/js/downloader.js b/static/js/downloader.js index 86e3a317f..e77771982 100644 --- a/static/js/downloader.js +++ b/static/js/downloader.js @@ -41,13 +41,14 @@ require([ if(size <= 0) { return "0 MB"; } - let log_level = Math.floor(Math.log10(size)); - let byte_count = 12; - while(log_level%byte_count >= log_level) { - byte_count-=3; - } - let bytes = (Math.round((size/(Math.pow(10,byte_count)))*1000)/1000).toFixed(3); - return `${bytes} ${byte_level[(byte_count/3)]}` ; + let byte_count = 0; + let converted_size = size; + while(converted_size > 1024) { + byte_count += 1; + converted_size /= 1024; + } + let bytes = (Math.round(converted_size*1000)/1000).toFixed(3); + return `${bytes} ${byte_level[byte_count]}` ; } class DownloadProgressDisplay { @@ -444,6 +445,8 @@ require([ workerObjectURL = null; workerLimit = navigator.hardwareConcurrency; + lastProgressUpdate = -1; + constructor() { this.queues = new DownloadQueueManager(); this.workerCodeBlob = new Blob([this.workerCode], {type: 'application/javascript'}); @@ -489,7 +492,11 @@ require([ } // Updates the current floating message contents and display class + // Will abort out of an update if there isn't a pending cancellation and the last update time was < 2 seconds + // ago progressUpdate(message, progType) { + if(this.lastProgressUpdate > 0 && Date.now()-this.lastProgressUpdate < 1000 && !this.pending_cancellation) + return; progType = progType || "download"; let type = "info"; let icon = progType || true; @@ -516,6 +523,7 @@ require([ if(!this.pending_cancellation && this.in_progress > 0) { messages['header'] = this.all_requested; } + this.lastProgressUpdate = Date.now(); this.progressDisplay.update(type, messages, icon); } From 828169f7f61b3130e91b475b6a9eb1f55a72a3c2 Mon Sep 17 00:00:00 2001 From: "S. Paquette" Date: Wed, 27 Aug 2025 18:23:10 -0700 Subject: [PATCH 51/52] -> jquery UI update --- static/css/jquery-ui.min.css | 12 ++++++------ static/css/jquery-ui.structure.min.css | 8 ++++---- static/css/jquery-ui.theme.min.css | 8 ++++---- static/js/libs/jquery-ui.min.js | 10 +++++----- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/static/css/jquery-ui.min.css b/static/css/jquery-ui.min.css index 24a505981..b616c9c1e 100644 --- a/static/css/jquery-ui.min.css +++ b/static/css/jquery-ui.min.css @@ -1,7 +1,7 @@ -/*! jQuery UI - v1.13.1 - 2022-07-10 -* http://jqueryui.com -* Includes: draggable.css, core.css, resizable.css, selectable.css, sortable.css, accordion.css, autocomplete.css, menu.css, button.css, controlgroup.css, checkboxradio.css, datepicker.css, dialog.css, progressbar.css, selectmenu.css, slider.css, spinner.css, tabs.css, tooltip.css, theme.css -* To view and modify this theme, visit http://jqueryui.com/themeroller/?scope=&folderName=base&cornerRadiusShadow=8px&offsetLeftShadow=0px&offsetTopShadow=0px&thicknessShadow=5px&opacityShadow=30&bgImgOpacityShadow=0&bgTextureShadow=flat&bgColorShadow=666666&opacityOverlay=30&bgImgOpacityOverlay=0&bgTextureOverlay=flat&bgColorOverlay=aaaaaa&iconColorError=cc0000&fcError=5f3f3f&borderColorError=f1a899&bgTextureError=flat&bgColorError=fddfdf&iconColorHighlight=777620&fcHighlight=777620&borderColorHighlight=dad55e&bgTextureHighlight=flat&bgColorHighlight=fffa90&iconColorActive=ffffff&fcActive=ffffff&borderColorActive=003eff&bgTextureActive=flat&bgColorActive=007fff&iconColorHover=555555&fcHover=2b2b2b&borderColorHover=cccccc&bgTextureHover=flat&bgColorHover=ededed&iconColorDefault=777777&fcDefault=454545&borderColorDefault=c5c5c5&bgTextureDefault=flat&bgColorDefault=f6f6f6&iconColorContent=444444&fcContent=333333&borderColorContent=dddddd&bgTextureContent=flat&bgColorContent=ffffff&iconColorHeader=444444&fcHeader=333333&borderColorHeader=dddddd&bgTextureHeader=flat&bgColorHeader=e9e9e9&cornerRadius=3px&fwDefault=normal&fsDefault=1em&ffDefault=Arial%2CHelvetica%2Csans-serif -* Copyright jQuery Foundation and other contributors; Licensed MIT */ +/*! jQuery UI - v1.14.1 - 2024-10-30 +* https://jqueryui.com +* Includes: core.css, accordion.css, autocomplete.css, menu.css, button.css, controlgroup.css, checkboxradio.css, datepicker.css, dialog.css, draggable.css, resizable.css, progressbar.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css +* To view and modify this theme, visit https://jqueryui.com/themeroller/?bgColorDefault=%23f6f6f6&borderColorDefault=%23c5c5c5&fcDefault=%23454545&bgColorHover=%23ededed&borderColorHover=%23cccccc&fcHover=%232b2b2b&bgColorActive=%23007fff&borderColorActive=%23003eff&fcActive=%23ffffff&bgColorHeader=%23e9e9e9&borderColorHeader=%23dddddd&fcHeader=%23333333&bgColorContent=%23ffffff&borderColorContent=%23dddddd&fcContent=%23333333&bgColorHighlight=%23fffa90&borderColorHighlight=%23dad55e&fcHighlight=%23777620&bgColorError=%23fddfdf&borderColorError=%23f1a899&fcError=%235f3f3f&bgColorOverlay=%23aaaaaa&opacityOverlay=.3&bgColorShadow=%23666666&opacityShadow=.3&offsetTopShadow=0px&offsetLeftShadow=0px&thicknessShadow=5px&cornerRadiusShadow=8px&fsDefault=1em&ffDefault=Arial%2CHelvetica%2Csans-serif&fwDefault=normal&cornerRadius=3px&bgTextureDefault=flat&bgTextureHover=flat&bgTextureActive=flat&bgTextureHeader=flat&bgTextureContent=flat&bgTextureHighlight=flat&bgTextureError=flat&bgTextureOverlay=flat&bgTextureShadow=flat&bgImgOpacityDefault=75&bgImgOpacityHover=75&bgImgOpacityActive=65&bgImgOpacityHeader=75&bgImgOpacityContent=75&bgImgOpacityHighlight=55&bgImgOpacityError=95&bgImgOpacityOverlay=0&bgImgOpacityShadow=0&iconColorActive=%23ffffff&iconColorContent=%23444444&iconColorDefault=%23777777&iconColorError=%23cc0000&iconColorHeader=%23444444&iconColorHighlight=%23777620&iconColorHover=%23555555&opacityOverlayPerc=30&opacityShadowPerc=30&bgImgUrlActive=&bgImgUrlContent=&bgImgUrlDefault=&bgImgUrlError=&bgImgUrlHeader=&bgImgUrlHighlight=&bgImgUrlHover=&bgImgUrlOverlay=&bgImgUrlShadow=&iconsActive=url(%22images%2Fui-icons_ffffff_256x240.png%22)&iconsContent=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsDefault=url(%22images%2Fui-icons_777777_256x240.png%22)&iconsError=url(%22images%2Fui-icons_cc0000_256x240.png%22)&iconsHeader=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsHighlight=url(%22images%2Fui-icons_777620_256x240.png%22)&iconsHover=url(%22images%2Fui-icons_555555_256x240.png%22)&bgDefaultRepeat=&bgHoverRepeat=&bgActiveRepeat=&bgHeaderRepeat=&bgContentRepeat=&bgHighlightRepeat=&bgErrorRepeat=&bgOverlayRepeat=&bgShadowRepeat=&bgDefaultYPos=&bgHoverYPos=&bgActiveYPos=&bgHeaderYPos=&bgContentYPos=&bgHighlightYPos=&bgErrorYPos=&bgOverlayYPos=&bgShadowYPos=&bgDefaultXPos=&bgHoverXPos=&bgActiveXPos=&bgHeaderXPos=&bgContentXPos=&bgHighlightXPos=&bgErrorXPos=&bgOverlayXPos=&bgShadowXPos= +* Copyright OpenJS Foundation and other contributors; Licensed MIT */ -.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;-ms-filter:"alpha(opacity=0)"}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;font-size:100%}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-button{padding:.4em 1em;display:inline-block;position:relative;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2em;box-sizing:border-box;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-button-icon-only{text-indent:0}.ui-button-icon-only .ui-icon{position:absolute;top:50%;left:50%;margin-top:-8px;margin-left:-8px}.ui-button.ui-icon-notext .ui-icon{padding:0;width:2.1em;height:2.1em;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-icon-notext .ui-icon{width:auto;height:auto;text-indent:0;white-space:normal;padding:.4em 1em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-controlgroup{vertical-align:middle;display:inline-block}.ui-controlgroup > .ui-controlgroup-item{float:left;margin-left:0;margin-right:0}.ui-controlgroup > .ui-controlgroup-item:focus,.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus{z-index:9999}.ui-controlgroup-vertical > .ui-controlgroup-item{display:block;float:none;width:100%;margin-top:0;margin-bottom:0;text-align:left}.ui-controlgroup-vertical .ui-controlgroup-item{box-sizing:border-box}.ui-controlgroup .ui-controlgroup-label{padding:.4em 1em}.ui-controlgroup .ui-controlgroup-label span{font-size:80%}.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item{border-left:none}.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item{border-top:none}.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content{border-right:none}.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content{border-bottom:none}.ui-controlgroup-vertical .ui-spinner-input{width:75%;width:calc( 100% - 2.4em )}.ui-controlgroup-vertical .ui-spinner .ui-spinner-up{border-top-style:solid}.ui-checkboxradio-label .ui-icon-background{box-shadow:inset 1px 1px 1px #ccc;border-radius:.12em;border:none}.ui-checkboxradio-radio-label .ui-icon-background{width:16px;height:16px;border-radius:1em;overflow:visible;border:none}.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon{background-image:none;width:8px;height:8px;border-width:4px;border-style:solid}.ui-checkboxradio-disabled{pointer-events:none}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat;left:.5em;top:.3em}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-n{height:2px;top:0}.ui-dialog .ui-resizable-e{width:2px;right:0}.ui-dialog .ui-resizable-s{height:2px;bottom:0}.ui-dialog .ui-resizable-w{width:2px;left:0}.ui-dialog .ui-resizable-se,.ui-dialog .ui-resizable-sw,.ui-dialog .ui-resizable-ne,.ui-dialog .ui-resizable-nw{width:7px;height:7px}.ui-dialog .ui-resizable-se{right:0;bottom:0}.ui-dialog .ui-resizable-sw{left:0;bottom:0}.ui-dialog .ui-resizable-ne{right:0;top:0}.ui-dialog .ui-resizable-nw{left:0;top:0}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;-ms-filter:"alpha(opacity=25)";opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-text{display:block;margin-right:20px;overflow:hidden;text-overflow:ellipsis}.ui-selectmenu-button.ui-button{text-align:left;white-space:nowrap;width:14em}.ui-selectmenu-icon.ui-icon{float:right;margin-top:0}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:pointer;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:.222em 0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:2em}.ui-spinner-button{width:1.6em;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top-style:none;border-bottom-style:none;border-right-style:none}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#2b2b2b;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-icon-background,.ui-state-active .ui-icon-background{border:#003eff;background-color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-checked{border:1px solid #dad55e;background:#fffa90}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;-ms-filter:"alpha(opacity=70)";font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;-ms-filter:"alpha(opacity=35)";background-image:none}.ui-state-disabled .ui-icon{-ms-filter:"alpha(opacity=35)"}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("images/ui-icons_555555_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("images/ui-icons_777620_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cc0000_256x240.png")}.ui-button .ui-icon{background-image:url("images/ui-icons_777777_256x240.png")}.ui-icon-blank.ui-icon-blank.ui-icon-blank{background-image:none}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.3;-ms-filter:Alpha(Opacity=30)}.ui-widget-shadow{-webkit-box-shadow:0 0 5px #666;box-shadow:0 0 5px #666} \ No newline at end of file +.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;font-size:100%}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-button{padding:.4em 1em;display:inline-block;position:relative;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;-webkit-user-select:none;user-select:none}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2em;box-sizing:border-box;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-button-icon-only{text-indent:0}.ui-button-icon-only .ui-icon{position:absolute;top:50%;left:50%;margin-top:-8px;margin-left:-8px}.ui-button.ui-icon-notext .ui-icon{padding:0;width:2.1em;height:2.1em;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-icon-notext .ui-icon{width:auto;height:auto;text-indent:0;white-space:normal;padding:.4em 1em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-controlgroup{vertical-align:middle;display:inline-block}.ui-controlgroup > .ui-controlgroup-item{float:left;margin-left:0;margin-right:0}.ui-controlgroup > .ui-controlgroup-item:focus,.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus{z-index:9999}.ui-controlgroup-vertical > .ui-controlgroup-item{display:block;float:none;width:100%;margin-top:0;margin-bottom:0;text-align:left}.ui-controlgroup-vertical .ui-controlgroup-item{box-sizing:border-box}.ui-controlgroup .ui-controlgroup-label{padding:.4em 1em}.ui-controlgroup .ui-controlgroup-label span{font-size:80%}.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item{border-left:none}.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item{border-top:none}.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content{border-right:none}.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content{border-bottom:none}.ui-controlgroup-vertical .ui-spinner-input{width:calc( 100% - 2.4em )}.ui-controlgroup-vertical .ui-spinner .ui-spinner-up{border-top-style:solid}.ui-checkboxradio-label .ui-icon-background{box-shadow:inset 1px 1px 1px #ccc;border-radius:.12em;border:none}.ui-checkboxradio-radio-label .ui-icon-background{width:16px;height:16px;border-radius:1em;overflow:visible;border:none}.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon{background-image:none;width:8px;height:8px;border-width:4px;border-style:solid}.ui-checkboxradio-disabled{pointer-events:none}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat;left:.5em;top:.3em}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-n{height:2px;top:0}.ui-dialog .ui-resizable-e{width:2px;right:0}.ui-dialog .ui-resizable-s{height:2px;bottom:0}.ui-dialog .ui-resizable-w{width:2px;left:0}.ui-dialog .ui-resizable-se,.ui-dialog .ui-resizable-sw,.ui-dialog .ui-resizable-ne,.ui-dialog .ui-resizable-nw{width:7px;height:7px}.ui-dialog .ui-resizable-se{right:0;bottom:0}.ui-dialog .ui-resizable-sw{left:0;bottom:0}.ui-dialog .ui-resizable-ne{right:0;top:0}.ui-dialog .ui-resizable-nw{left:0;top:0}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-draggable-handle{touch-action:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-selectable{touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-text{display:block;margin-right:20px;overflow:hidden;text-overflow:ellipsis}.ui-selectmenu-button.ui-button{text-align:left;white-space:nowrap;width:14em}.ui-selectmenu-icon.ui-icon{float:right;margin-top:0}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:pointer;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-sortable-handle{touch-action:none}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:.222em 0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:2em}.ui-spinner-button{width:1.6em;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top-style:none;border-bottom-style:none;border-right-style:none}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#2b2b2b;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-icon-background,.ui-state-active .ui-icon-background{border:#003eff;background-color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-checked{border:1px solid #dad55e;background:#fffa90}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;background-image:none}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("images/ui-icons_555555_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("images/ui-icons_777620_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cc0000_256x240.png")}.ui-button .ui-icon{background-image:url("images/ui-icons_777777_256x240.png")}.ui-icon-blank.ui-icon-blank.ui-icon-blank{background-image:none}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.3}.ui-widget-shadow{box-shadow:0 0 5px #666} \ No newline at end of file diff --git a/static/css/jquery-ui.structure.min.css b/static/css/jquery-ui.structure.min.css index eefb29b8a..619b3e21b 100644 --- a/static/css/jquery-ui.structure.min.css +++ b/static/css/jquery-ui.structure.min.css @@ -1,5 +1,5 @@ -/*! jQuery UI - v1.13.1 - 2022-07-10 -* http://jqueryui.com -* Copyright jQuery Foundation and other contributors; Licensed MIT */ +/*! jQuery UI - v1.14.1 - 2024-10-30 +* https://jqueryui.com +* Copyright OpenJS Foundation and other contributors; Licensed MIT */ -.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;-ms-filter:"alpha(opacity=0)"}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;font-size:100%}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-button{padding:.4em 1em;display:inline-block;position:relative;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2em;box-sizing:border-box;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-button-icon-only{text-indent:0}.ui-button-icon-only .ui-icon{position:absolute;top:50%;left:50%;margin-top:-8px;margin-left:-8px}.ui-button.ui-icon-notext .ui-icon{padding:0;width:2.1em;height:2.1em;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-icon-notext .ui-icon{width:auto;height:auto;text-indent:0;white-space:normal;padding:.4em 1em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-controlgroup{vertical-align:middle;display:inline-block}.ui-controlgroup > .ui-controlgroup-item{float:left;margin-left:0;margin-right:0}.ui-controlgroup > .ui-controlgroup-item:focus,.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus{z-index:9999}.ui-controlgroup-vertical > .ui-controlgroup-item{display:block;float:none;width:100%;margin-top:0;margin-bottom:0;text-align:left}.ui-controlgroup-vertical .ui-controlgroup-item{box-sizing:border-box}.ui-controlgroup .ui-controlgroup-label{padding:.4em 1em}.ui-controlgroup .ui-controlgroup-label span{font-size:80%}.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item{border-left:none}.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item{border-top:none}.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content{border-right:none}.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content{border-bottom:none}.ui-controlgroup-vertical .ui-spinner-input{width:75%;width:calc( 100% - 2.4em )}.ui-controlgroup-vertical .ui-spinner .ui-spinner-up{border-top-style:solid}.ui-checkboxradio-label .ui-icon-background{box-shadow:inset 1px 1px 1px #ccc;border-radius:.12em;border:none}.ui-checkboxradio-radio-label .ui-icon-background{width:16px;height:16px;border-radius:1em;overflow:visible;border:none}.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon{background-image:none;width:8px;height:8px;border-width:4px;border-style:solid}.ui-checkboxradio-disabled{pointer-events:none}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat;left:.5em;top:.3em}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-n{height:2px;top:0}.ui-dialog .ui-resizable-e{width:2px;right:0}.ui-dialog .ui-resizable-s{height:2px;bottom:0}.ui-dialog .ui-resizable-w{width:2px;left:0}.ui-dialog .ui-resizable-se,.ui-dialog .ui-resizable-sw,.ui-dialog .ui-resizable-ne,.ui-dialog .ui-resizable-nw{width:7px;height:7px}.ui-dialog .ui-resizable-se{right:0;bottom:0}.ui-dialog .ui-resizable-sw{left:0;bottom:0}.ui-dialog .ui-resizable-ne{right:0;top:0}.ui-dialog .ui-resizable-nw{left:0;top:0}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;-ms-filter:"alpha(opacity=25)";opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-text{display:block;margin-right:20px;overflow:hidden;text-overflow:ellipsis}.ui-selectmenu-button.ui-button{text-align:left;white-space:nowrap;width:14em}.ui-selectmenu-icon.ui-icon{float:right;margin-top:0}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:pointer;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:.222em 0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:2em}.ui-spinner-button{width:1.6em;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top-style:none;border-bottom-style:none;border-right-style:none}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px}body .ui-tooltip{border-width:2px} \ No newline at end of file +.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;font-size:100%}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-button{padding:.4em 1em;display:inline-block;position:relative;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;-webkit-user-select:none;user-select:none}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2em;box-sizing:border-box;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-button-icon-only{text-indent:0}.ui-button-icon-only .ui-icon{position:absolute;top:50%;left:50%;margin-top:-8px;margin-left:-8px}.ui-button.ui-icon-notext .ui-icon{padding:0;width:2.1em;height:2.1em;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-icon-notext .ui-icon{width:auto;height:auto;text-indent:0;white-space:normal;padding:.4em 1em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-controlgroup{vertical-align:middle;display:inline-block}.ui-controlgroup > .ui-controlgroup-item{float:left;margin-left:0;margin-right:0}.ui-controlgroup > .ui-controlgroup-item:focus,.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus{z-index:9999}.ui-controlgroup-vertical > .ui-controlgroup-item{display:block;float:none;width:100%;margin-top:0;margin-bottom:0;text-align:left}.ui-controlgroup-vertical .ui-controlgroup-item{box-sizing:border-box}.ui-controlgroup .ui-controlgroup-label{padding:.4em 1em}.ui-controlgroup .ui-controlgroup-label span{font-size:80%}.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item{border-left:none}.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item{border-top:none}.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content{border-right:none}.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content{border-bottom:none}.ui-controlgroup-vertical .ui-spinner-input{width:calc( 100% - 2.4em )}.ui-controlgroup-vertical .ui-spinner .ui-spinner-up{border-top-style:solid}.ui-checkboxradio-label .ui-icon-background{box-shadow:inset 1px 1px 1px #ccc;border-radius:.12em;border:none}.ui-checkboxradio-radio-label .ui-icon-background{width:16px;height:16px;border-radius:1em;overflow:visible;border:none}.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon{background-image:none;width:8px;height:8px;border-width:4px;border-style:solid}.ui-checkboxradio-disabled{pointer-events:none}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat;left:.5em;top:.3em}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-n{height:2px;top:0}.ui-dialog .ui-resizable-e{width:2px;right:0}.ui-dialog .ui-resizable-s{height:2px;bottom:0}.ui-dialog .ui-resizable-w{width:2px;left:0}.ui-dialog .ui-resizable-se,.ui-dialog .ui-resizable-sw,.ui-dialog .ui-resizable-ne,.ui-dialog .ui-resizable-nw{width:7px;height:7px}.ui-dialog .ui-resizable-se{right:0;bottom:0}.ui-dialog .ui-resizable-sw{left:0;bottom:0}.ui-dialog .ui-resizable-ne{right:0;top:0}.ui-dialog .ui-resizable-nw{left:0;top:0}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-draggable-handle{touch-action:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-selectable{touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-text{display:block;margin-right:20px;overflow:hidden;text-overflow:ellipsis}.ui-selectmenu-button.ui-button{text-align:left;white-space:nowrap;width:14em}.ui-selectmenu-icon.ui-icon{float:right;margin-top:0}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:pointer;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-sortable-handle{touch-action:none}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:.222em 0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:2em}.ui-spinner-button{width:1.6em;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top-style:none;border-bottom-style:none;border-right-style:none}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px}body .ui-tooltip{border-width:2px} \ No newline at end of file diff --git a/static/css/jquery-ui.theme.min.css b/static/css/jquery-ui.theme.min.css index a703ccaab..670898b57 100644 --- a/static/css/jquery-ui.theme.min.css +++ b/static/css/jquery-ui.theme.min.css @@ -1,5 +1,5 @@ -/*! jQuery UI - v1.13.1 - 2022-07-10 -* http://jqueryui.com -* Copyright jQuery Foundation and other contributors; Licensed MIT */ +/*! jQuery UI - v1.14.1 - 2024-10-30 +* https://jqueryui.com +* Copyright OpenJS Foundation and other contributors; Licensed MIT */ -.ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#2b2b2b;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-icon-background,.ui-state-active .ui-icon-background{border:#003eff;background-color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-checked{border:1px solid #dad55e;background:#fffa90}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;-ms-filter:"alpha(opacity=70)";font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;-ms-filter:"alpha(opacity=35)";background-image:none}.ui-state-disabled .ui-icon{-ms-filter:"alpha(opacity=35)"}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("images/ui-icons_555555_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("images/ui-icons_777620_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cc0000_256x240.png")}.ui-button .ui-icon{background-image:url("images/ui-icons_777777_256x240.png")}.ui-icon-blank.ui-icon-blank.ui-icon-blank{background-image:none}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.3;-ms-filter:Alpha(Opacity=30)}.ui-widget-shadow{-webkit-box-shadow:0 0 5px #666;box-shadow:0 0 5px #666} \ No newline at end of file +.ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#2b2b2b;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-icon-background,.ui-state-active .ui-icon-background{border:#003eff;background-color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-checked{border:1px solid #dad55e;background:#fffa90}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;background-image:none}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("images/ui-icons_555555_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("images/ui-icons_777620_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cc0000_256x240.png")}.ui-button .ui-icon{background-image:url("images/ui-icons_777777_256x240.png")}.ui-icon-blank.ui-icon-blank.ui-icon-blank{background-image:none}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.3}.ui-widget-shadow{box-shadow:0 0 5px #666} \ No newline at end of file diff --git a/static/js/libs/jquery-ui.min.js b/static/js/libs/jquery-ui.min.js index b352e4b21..841179e84 100644 --- a/static/js/libs/jquery-ui.min.js +++ b/static/js/libs/jquery-ui.min.js @@ -1,6 +1,6 @@ -/*! jQuery UI - v1.13.1 - 2022-07-10 -* http://jqueryui.com -* Includes: widget.js, position.js, data.js, disable-selection.js, focusable.js, form-reset-mixin.js, jquery-patch.js, keycode.js, labels.js, scroll-parent.js, tabbable.js, unique-id.js, widgets/draggable.js, widgets/droppable.js, widgets/resizable.js, widgets/selectable.js, widgets/sortable.js, widgets/accordion.js, widgets/autocomplete.js, widgets/button.js, widgets/checkboxradio.js, widgets/controlgroup.js, widgets/datepicker.js, widgets/dialog.js, widgets/menu.js, widgets/mouse.js, widgets/progressbar.js, widgets/selectmenu.js, widgets/slider.js, widgets/spinner.js, widgets/tabs.js, widgets/tooltip.js, effect.js, effects/effect-blind.js, effects/effect-bounce.js, effects/effect-clip.js, effects/effect-drop.js, effects/effect-explode.js, effects/effect-fade.js, effects/effect-fold.js, effects/effect-highlight.js, effects/effect-puff.js, effects/effect-pulsate.js, effects/effect-scale.js, effects/effect-shake.js, effects/effect-size.js, effects/effect-slide.js, effects/effect-transfer.js -* Copyright jQuery Foundation and other contributors; Licensed MIT */ +/*! jQuery UI - v1.14.1 - 2024-10-30 +* https://jqueryui.com +* Includes: widget.js, position.js, data.js, disable-selection.js, effect.js, effects/effect-blind.js, effects/effect-bounce.js, effects/effect-clip.js, effects/effect-drop.js, effects/effect-explode.js, effects/effect-fade.js, effects/effect-fold.js, effects/effect-highlight.js, effects/effect-puff.js, effects/effect-pulsate.js, effects/effect-scale.js, effects/effect-shake.js, effects/effect-size.js, effects/effect-slide.js, effects/effect-transfer.js, focusable.js, form-reset-mixin.js, jquery-patch.js, keycode.js, labels.js, scroll-parent.js, tabbable.js, unique-id.js, widgets/accordion.js, widgets/autocomplete.js, widgets/button.js, widgets/checkboxradio.js, widgets/controlgroup.js, widgets/datepicker.js, widgets/dialog.js, widgets/draggable.js, widgets/droppable.js, widgets/menu.js, widgets/mouse.js, widgets/progressbar.js, widgets/resizable.js, widgets/selectable.js, widgets/selectmenu.js, widgets/slider.js, widgets/sortable.js, widgets/spinner.js, widgets/tabs.js, widgets/tooltip.js +* Copyright OpenJS Foundation and other contributors; Licensed MIT */ -!function(t){"use strict";"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)}(function(V){"use strict";V.ui=V.ui||{};V.ui.version="1.13.1";var n,i=0,a=Array.prototype.hasOwnProperty,r=Array.prototype.slice;V.cleanData=(n=V.cleanData,function(t){for(var e,i,s=0;null!=(i=t[s]);s++)(e=V._data(i,"events"))&&e.remove&&V(i).triggerHandler("remove");n(t)}),V.widget=function(t,i,e){var s,n,o,a={},r=t.split(".")[0],l=r+"-"+(t=t.split(".")[1]);return e||(e=i,i=V.Widget),Array.isArray(e)&&(e=V.extend.apply(null,[{}].concat(e))),V.expr.pseudos[l.toLowerCase()]=function(t){return!!V.data(t,l)},V[r]=V[r]||{},s=V[r][t],n=V[r][t]=function(t,e){if(!this||!this._createWidget)return new n(t,e);arguments.length&&this._createWidget(t,e)},V.extend(n,s,{version:e.version,_proto:V.extend({},e),_childConstructors:[]}),(o=new i).options=V.widget.extend({},o.options),V.each(e,function(e,s){function n(){return i.prototype[e].apply(this,arguments)}function o(t){return i.prototype[e].apply(this,t)}a[e]="function"==typeof s?function(){var t,e=this._super,i=this._superApply;return this._super=n,this._superApply=o,t=s.apply(this,arguments),this._super=e,this._superApply=i,t}:s}),n.prototype=V.widget.extend(o,{widgetEventPrefix:s&&o.widgetEventPrefix||t},a,{constructor:n,namespace:r,widgetName:t,widgetFullName:l}),s?(V.each(s._childConstructors,function(t,e){var i=e.prototype;V.widget(i.namespace+"."+i.widgetName,n,e._proto)}),delete s._childConstructors):i._childConstructors.push(n),V.widget.bridge(t,n),n},V.widget.extend=function(t){for(var e,i,s=r.call(arguments,1),n=0,o=s.length;n",options:{classes:{},disabled:!1,create:null},_createWidget:function(t,e){e=V(e||this.defaultElement||this)[0],this.element=V(e),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=V(),this.hoverable=V(),this.focusable=V(),this.classesElementLookup={},e!==this&&(V.data(e,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===e&&this.destroy()}}),this.document=V(e.style?e.ownerDocument:e.document||e),this.window=V(this.document[0].defaultView||this.document[0].parentWindow)),this.options=V.widget.extend({},this.options,this._getCreateOptions(),t),this._create(),this.options.disabled&&this._setOptionDisabled(this.options.disabled),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:function(){return{}},_getCreateEventData:V.noop,_create:V.noop,_init:V.noop,destroy:function(){var i=this;this._destroy(),V.each(this.classesElementLookup,function(t,e){i._removeClass(e,t)}),this.element.off(this.eventNamespace).removeData(this.widgetFullName),this.widget().off(this.eventNamespace).removeAttr("aria-disabled"),this.bindings.off(this.eventNamespace)},_destroy:V.noop,widget:function(){return this.element},option:function(t,e){var i,s,n,o=t;if(0===arguments.length)return V.widget.extend({},this.options);if("string"==typeof t)if(o={},t=(i=t.split(".")).shift(),i.length){for(s=o[t]=V.widget.extend({},this.options[t]),n=0;n

    "),i=e.children()[0];return V("body").append(e),t=i.offsetWidth,e.css("overflow","scroll"),t===(i=i.offsetWidth)&&(i=e[0].clientWidth),e.remove(),s=t-i},getScrollInfo:function(t){var e=t.isWindow||t.isDocument?"":t.element.css("overflow-x"),i=t.isWindow||t.isDocument?"":t.element.css("overflow-y"),e="scroll"===e||"auto"===e&&t.widthx(k(s),k(n))?o.important="horizontal":o.important="vertical",u.using.call(this,t,o)}),a.offset(V.extend(h,{using:t}))})},V.ui.position={fit:{left:function(t,e){var i=e.within,s=i.isWindow?i.scrollLeft:i.offset.left,n=i.width,o=t.left-e.collisionPosition.marginLeft,a=s-o,r=o+e.collisionWidth-n-s;e.collisionWidth>n?0n?0=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}}),V.ui.plugin={add:function(t,e,i){var s,n=V.ui[t].prototype;for(s in i)n.plugins[s]=n.plugins[s]||[],n.plugins[s].push([e,i[s]])},call:function(t,e,i,s){var n,o=t.plugins[e];if(o&&(s||t.element[0].parentNode&&11!==t.element[0].parentNode.nodeType))for(n=0;n").css("position","absolute").appendTo(t.parent()).outerWidth(t.outerWidth()).outerHeight(t.outerHeight()).offset(t.offset())[0]})},_unblockFrames:function(){this.iframeBlocks&&(this.iframeBlocks.remove(),delete this.iframeBlocks)},_blurActiveElement:function(t){var e=V.ui.safeActiveElement(this.document[0]);V(t.target).closest(e).length||V.ui.safeBlur(e)},_mouseStart:function(t){var e=this.options;return this.helper=this._createHelper(t),this._addClass(this.helper,"ui-draggable-dragging"),this._cacheHelperProportions(),V.ui.ddmanager&&(V.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(!0),this.offsetParent=this.helper.offsetParent(),this.hasFixedAncestor=0i[2]&&(o=i[2]+this.offset.click.left),t.pageY-this.offset.click.top>i[3]&&(a=i[3]+this.offset.click.top)),s.grid&&(t=s.grid[1]?this.originalPageY+Math.round((a-this.originalPageY)/s.grid[1])*s.grid[1]:this.originalPageY,a=!i||t-this.offset.click.top>=i[1]||t-this.offset.click.top>i[3]?t:t-this.offset.click.top>=i[1]?t-s.grid[1]:t+s.grid[1],t=s.grid[0]?this.originalPageX+Math.round((o-this.originalPageX)/s.grid[0])*s.grid[0]:this.originalPageX,o=!i||t-this.offset.click.left>=i[0]||t-this.offset.click.left>i[2]?t:t-this.offset.click.left>=i[0]?t-s.grid[0]:t+s.grid[0]),"y"===s.axis&&(o=this.originalPageX),"x"===s.axis&&(a=this.originalPageY)),{top:a-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.offset.scroll.top:n?0:this.offset.scroll.top),left:o-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.offset.scroll.left:n?0:this.offset.scroll.left)}},_clear:function(){this._removeClass(this.helper,"ui-draggable-dragging"),this.helper[0]===this.element[0]||this.cancelHelperRemoval||this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1,this.destroyOnClear&&this.destroy()},_trigger:function(t,e,i){return i=i||this._uiHash(),V.ui.plugin.call(this,t,[e,i,this],!0),/^(drag|start|stop)/.test(t)&&(this.positionAbs=this._convertPositionTo("absolute"),i.offset=this.positionAbs),V.Widget.prototype._trigger.call(this,t,e,i)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),V.ui.plugin.add("draggable","connectToSortable",{start:function(e,t,i){var s=V.extend({},t,{item:i.element});i.sortables=[],V(i.options.connectToSortable).each(function(){var t=V(this).sortable("instance");t&&!t.options.disabled&&(i.sortables.push(t),t.refreshPositions(),t._trigger("activate",e,s))})},stop:function(e,t,i){var s=V.extend({},t,{item:i.element});i.cancelHelperRemoval=!1,V.each(i.sortables,function(){var t=this;t.isOver?(t.isOver=0,i.cancelHelperRemoval=!0,t.cancelHelperRemoval=!1,t._storedCSS={position:t.placeholder.css("position"),top:t.placeholder.css("top"),left:t.placeholder.css("left")},t._mouseStop(e),t.options.helper=t.options._helper):(t.cancelHelperRemoval=!0,t._trigger("deactivate",e,s))})},drag:function(i,s,n){V.each(n.sortables,function(){var t=!1,e=this;e.positionAbs=n.positionAbs,e.helperProportions=n.helperProportions,e.offset.click=n.offset.click,e._intersectsWith(e.containerCache)&&(t=!0,V.each(n.sortables,function(){return this.positionAbs=n.positionAbs,this.helperProportions=n.helperProportions,this.offset.click=n.offset.click,t=this!==e&&this._intersectsWith(this.containerCache)&&V.contains(e.element[0],this.element[0])?!1:t})),t?(e.isOver||(e.isOver=1,n._parent=s.helper.parent(),e.currentItem=s.helper.appendTo(e.element).data("ui-sortable-item",!0),e.options._helper=e.options.helper,e.options.helper=function(){return s.helper[0]},i.target=e.currentItem[0],e._mouseCapture(i,!0),e._mouseStart(i,!0,!0),e.offset.click.top=n.offset.click.top,e.offset.click.left=n.offset.click.left,e.offset.parent.left-=n.offset.parent.left-e.offset.parent.left,e.offset.parent.top-=n.offset.parent.top-e.offset.parent.top,n._trigger("toSortable",i),n.dropped=e.element,V.each(n.sortables,function(){this.refreshPositions()}),n.currentItem=n.element,e.fromOutside=n),e.currentItem&&(e._mouseDrag(i),s.position=e.position)):e.isOver&&(e.isOver=0,e.cancelHelperRemoval=!0,e.options._revert=e.options.revert,e.options.revert=!1,e._trigger("out",i,e._uiHash(e)),e._mouseStop(i,!0),e.options.revert=e.options._revert,e.options.helper=e.options._helper,e.placeholder&&e.placeholder.remove(),s.helper.appendTo(n._parent),n._refreshOffsets(i),s.position=n._generatePosition(i,!0),n._trigger("fromSortable",i),n.dropped=!1,V.each(n.sortables,function(){this.refreshPositions()}))})}}),V.ui.plugin.add("draggable","cursor",{start:function(t,e,i){var s=V("body"),i=i.options;s.css("cursor")&&(i._cursor=s.css("cursor")),s.css("cursor",i.cursor)},stop:function(t,e,i){i=i.options;i._cursor&&V("body").css("cursor",i._cursor)}}),V.ui.plugin.add("draggable","opacity",{start:function(t,e,i){e=V(e.helper),i=i.options;e.css("opacity")&&(i._opacity=e.css("opacity")),e.css("opacity",i.opacity)},stop:function(t,e,i){i=i.options;i._opacity&&V(e.helper).css("opacity",i._opacity)}}),V.ui.plugin.add("draggable","scroll",{start:function(t,e,i){i.scrollParentNotHidden||(i.scrollParentNotHidden=i.helper.scrollParent(!1)),i.scrollParentNotHidden[0]!==i.document[0]&&"HTML"!==i.scrollParentNotHidden[0].tagName&&(i.overflowOffset=i.scrollParentNotHidden.offset())},drag:function(t,e,i){var s=i.options,n=!1,o=i.scrollParentNotHidden[0],a=i.document[0];o!==a&&"HTML"!==o.tagName?(s.axis&&"x"===s.axis||(i.overflowOffset.top+o.offsetHeight-t.pageY").css({overflow:"hidden",position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("ui-resizable",this.element.resizable("instance")),this.elementIsWrapper=!0,t={marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom"),marginLeft:this.originalElement.css("marginLeft")},this.element.css(t),this.originalElement.css("margin",0),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css(t),this._proportionallyResize()),this._setupHandles(),e.autoHide&&V(this.element).on("mouseenter",function(){e.disabled||(i._removeClass("ui-resizable-autohide"),i._handles.show())}).on("mouseleave",function(){e.disabled||i.resizing||(i._addClass("ui-resizable-autohide"),i._handles.hide())}),this._mouseInit()},_destroy:function(){this._mouseDestroy(),this._addedHandles.remove();function t(t){V(t).removeData("resizable").removeData("ui-resizable").off(".resizable")}var e;return this.elementIsWrapper&&(t(this.element),e=this.element,this.originalElement.css({position:e.css("position"),width:e.outerWidth(),height:e.outerHeight(),top:e.css("top"),left:e.css("left")}).insertAfter(e),e.remove()),this.originalElement.css("resize",this.originalResizeStyle),t(this.originalElement),this},_setOption:function(t,e){switch(this._super(t,e),t){case"handles":this._removeHandles(),this._setupHandles();break;case"aspectRatio":this._aspectRatio=!!e}},_setupHandles:function(){var t,e,i,s,n,o=this.options,a=this;if(this.handles=o.handles||(V(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se"),this._handles=V(),this._addedHandles=V(),this.handles.constructor===String)for("all"===this.handles&&(this.handles="n,e,s,w,se,sw,ne,nw"),i=this.handles.split(","),this.handles={},e=0;e"),this._addClass(n,"ui-resizable-handle "+s),n.css({zIndex:o.zIndex}),this.handles[t]=".ui-resizable-"+t,this.element.children(this.handles[t]).length||(this.element.append(n),this._addedHandles=this._addedHandles.add(n));this._renderAxis=function(t){var e,i,s;for(e in t=t||this.element,this.handles)this.handles[e].constructor===String?this.handles[e]=this.element.children(this.handles[e]).first().show():(this.handles[e].jquery||this.handles[e].nodeType)&&(this.handles[e]=V(this.handles[e]),this._on(this.handles[e],{mousedown:a._mouseDown})),this.elementIsWrapper&&this.originalElement[0].nodeName.match(/^(textarea|input|select|button)$/i)&&(i=V(this.handles[e],this.element),s=/sw|ne|nw|se|n|s/.test(e)?i.outerHeight():i.outerWidth(),i=["padding",/ne|nw|n/.test(e)?"Top":/se|sw|s/.test(e)?"Bottom":/^e$/.test(e)?"Right":"Left"].join(""),t.css(i,s),this._proportionallyResize()),this._handles=this._handles.add(this.handles[e])},this._renderAxis(this.element),this._handles=this._handles.add(this.element.find(".ui-resizable-handle")),this._handles.disableSelection(),this._handles.on("mouseover",function(){a.resizing||(this.className&&(n=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i)),a.axis=n&&n[1]?n[1]:"se")}),o.autoHide&&(this._handles.hide(),this._addClass("ui-resizable-autohide"))},_removeHandles:function(){this._addedHandles.remove()},_mouseCapture:function(t){var e,i,s=!1;for(e in this.handles)(i=V(this.handles[e])[0])!==t.target&&!V.contains(i,t.target)||(s=!0);return!this.options.disabled&&s},_mouseStart:function(t){var e,i,s=this.options,n=this.element;return this.resizing=!0,this._renderProxy(),e=this._num(this.helper.css("left")),i=this._num(this.helper.css("top")),s.containment&&(e+=V(s.containment).scrollLeft()||0,i+=V(s.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:e,top:i},this.size=this._helper?{width:this.helper.width(),height:this.helper.height()}:{width:n.width(),height:n.height()},this.originalSize=this._helper?{width:n.outerWidth(),height:n.outerHeight()}:{width:n.width(),height:n.height()},this.sizeDiff={width:n.outerWidth()-n.width(),height:n.outerHeight()-n.height()},this.originalPosition={left:e,top:i},this.originalMousePosition={left:t.pageX,top:t.pageY},this.aspectRatio="number"==typeof s.aspectRatio?s.aspectRatio:this.originalSize.width/this.originalSize.height||1,s=V(".ui-resizable-"+this.axis).css("cursor"),V("body").css("cursor","auto"===s?this.axis+"-resize":s),this._addClass("ui-resizable-resizing"),this._propagate("start",t),!0},_mouseDrag:function(t){var e=this.originalMousePosition,i=this.axis,s=t.pageX-e.left||0,e=t.pageY-e.top||0,i=this._change[i];return this._updatePrevProperties(),i&&(e=i.apply(this,[t,s,e]),this._updateVirtualBoundaries(t.shiftKey),(this._aspectRatio||t.shiftKey)&&(e=this._updateRatio(e,t)),e=this._respectSize(e,t),this._updateCache(e),this._propagate("resize",t),e=this._applyChanges(),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),V.isEmptyObject(e)||(this._updatePrevProperties(),this._trigger("resize",t,this.ui()),this._applyChanges())),!1},_mouseStop:function(t){this.resizing=!1;var e,i,s,n=this.options,o=this;return this._helper&&(s=(e=(i=this._proportionallyResizeElements).length&&/textarea/i.test(i[0].nodeName))&&this._hasScroll(i[0],"left")?0:o.sizeDiff.height,i=e?0:o.sizeDiff.width,e={width:o.helper.width()-i,height:o.helper.height()-s},i=parseFloat(o.element.css("left"))+(o.position.left-o.originalPosition.left)||null,s=parseFloat(o.element.css("top"))+(o.position.top-o.originalPosition.top)||null,n.animate||this.element.css(V.extend(e,{top:s,left:i})),o.helper.height(o.size.height),o.helper.width(o.size.width),this._helper&&!n.animate&&this._proportionallyResize()),V("body").css("cursor","auto"),this._removeClass("ui-resizable-resizing"),this._propagate("stop",t),this._helper&&this.helper.remove(),!1},_updatePrevProperties:function(){this.prevPosition={top:this.position.top,left:this.position.left},this.prevSize={width:this.size.width,height:this.size.height}},_applyChanges:function(){var t={};return this.position.top!==this.prevPosition.top&&(t.top=this.position.top+"px"),this.position.left!==this.prevPosition.left&&(t.left=this.position.left+"px"),this.size.width!==this.prevSize.width&&(t.width=this.size.width+"px"),this.size.height!==this.prevSize.height&&(t.height=this.size.height+"px"),this.helper.css(t),t},_updateVirtualBoundaries:function(t){var e,i,s=this.options,n={minWidth:this._isNumber(s.minWidth)?s.minWidth:0,maxWidth:this._isNumber(s.maxWidth)?s.maxWidth:1/0,minHeight:this._isNumber(s.minHeight)?s.minHeight:0,maxHeight:this._isNumber(s.maxHeight)?s.maxHeight:1/0};(this._aspectRatio||t)&&(e=n.minHeight*this.aspectRatio,i=n.minWidth/this.aspectRatio,s=n.maxHeight*this.aspectRatio,t=n.maxWidth/this.aspectRatio,e>n.minWidth&&(n.minWidth=e),i>n.minHeight&&(n.minHeight=i),st.width,a=this._isNumber(t.height)&&e.minHeight&&e.minHeight>t.height,r=this.originalPosition.left+this.originalSize.width,l=this.originalPosition.top+this.originalSize.height,h=/sw|nw|w/.test(i),i=/nw|ne|n/.test(i);return o&&(t.width=e.minWidth),a&&(t.height=e.minHeight),s&&(t.width=e.maxWidth),n&&(t.height=e.maxHeight),o&&h&&(t.left=r-e.minWidth),s&&h&&(t.left=r-e.maxWidth),a&&i&&(t.top=l-e.minHeight),n&&i&&(t.top=l-e.maxHeight),t.width||t.height||t.left||!t.top?t.width||t.height||t.top||!t.left||(t.left=null):t.top=null,t},_getPaddingPlusBorderDimensions:function(t){for(var e=0,i=[],s=[t.css("borderTopWidth"),t.css("borderRightWidth"),t.css("borderBottomWidth"),t.css("borderLeftWidth")],n=[t.css("paddingTop"),t.css("paddingRight"),t.css("paddingBottom"),t.css("paddingLeft")];e<4;e++)i[e]=parseFloat(s[e])||0,i[e]+=parseFloat(n[e])||0;return{height:i[0]+i[2],width:i[1]+i[3]}},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var t,e=0,i=this.helper||this.element;e").css({overflow:"hidden"}),this._addClass(this.helper,this._helper),this.helper.css({width:this.element.outerWidth(),height:this.element.outerHeight(),position:"absolute",left:this.elementOffset.left+"px",top:this.elementOffset.top+"px",zIndex:++e.zIndex}),this.helper.appendTo("body").disableSelection()):this.helper=this.element},_change:{e:function(t,e){return{width:this.originalSize.width+e}},w:function(t,e){var i=this.originalSize;return{left:this.originalPosition.left+e,width:i.width-e}},n:function(t,e,i){var s=this.originalSize;return{top:this.originalPosition.top+i,height:s.height-i}},s:function(t,e,i){return{height:this.originalSize.height+i}},se:function(t,e,i){return V.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[t,e,i]))},sw:function(t,e,i){return V.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[t,e,i]))},ne:function(t,e,i){return V.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[t,e,i]))},nw:function(t,e,i){return V.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[t,e,i]))}},_propagate:function(t,e){V.ui.plugin.call(this,t,[e,this.ui()]),"resize"!==t&&this._trigger(t,e,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),V.ui.plugin.add("resizable","animate",{stop:function(e){var i=V(this).resizable("instance"),t=i.options,s=i._proportionallyResizeElements,n=s.length&&/textarea/i.test(s[0].nodeName),o=n&&i._hasScroll(s[0],"left")?0:i.sizeDiff.height,a=n?0:i.sizeDiff.width,n={width:i.size.width-a,height:i.size.height-o},a=parseFloat(i.element.css("left"))+(i.position.left-i.originalPosition.left)||null,o=parseFloat(i.element.css("top"))+(i.position.top-i.originalPosition.top)||null;i.element.animate(V.extend(n,o&&a?{top:o,left:a}:{}),{duration:t.animateDuration,easing:t.animateEasing,step:function(){var t={width:parseFloat(i.element.css("width")),height:parseFloat(i.element.css("height")),top:parseFloat(i.element.css("top")),left:parseFloat(i.element.css("left"))};s&&s.length&&V(s[0]).css({width:t.width,height:t.height}),i._updateCache(t),i._propagate("resize",e)}})}}),V.ui.plugin.add("resizable","containment",{start:function(){var i,s,n=V(this).resizable("instance"),t=n.options,e=n.element,o=t.containment,a=o instanceof V?o.get(0):/parent/.test(o)?e.parent().get(0):o;a&&(n.containerElement=V(a),/document/.test(o)||o===document?(n.containerOffset={left:0,top:0},n.containerPosition={left:0,top:0},n.parentData={element:V(document),left:0,top:0,width:V(document).width(),height:V(document).height()||document.body.parentNode.scrollHeight}):(i=V(a),s=[],V(["Top","Right","Left","Bottom"]).each(function(t,e){s[t]=n._num(i.css("padding"+e))}),n.containerOffset=i.offset(),n.containerPosition=i.position(),n.containerSize={height:i.innerHeight()-s[3],width:i.innerWidth()-s[1]},t=n.containerOffset,e=n.containerSize.height,o=n.containerSize.width,o=n._hasScroll(a,"left")?a.scrollWidth:o,e=n._hasScroll(a)?a.scrollHeight:e,n.parentData={element:a,left:t.left,top:t.top,width:o,height:e}))},resize:function(t){var e=V(this).resizable("instance"),i=e.options,s=e.containerOffset,n=e.position,o=e._aspectRatio||t.shiftKey,a={top:0,left:0},r=e.containerElement,t=!0;r[0]!==document&&/static/.test(r.css("position"))&&(a=s),n.left<(e._helper?s.left:0)&&(e.size.width=e.size.width+(e._helper?e.position.left-s.left:e.position.left-a.left),o&&(e.size.height=e.size.width/e.aspectRatio,t=!1),e.position.left=i.helper?s.left:0),n.top<(e._helper?s.top:0)&&(e.size.height=e.size.height+(e._helper?e.position.top-s.top:e.position.top),o&&(e.size.width=e.size.height*e.aspectRatio,t=!1),e.position.top=e._helper?s.top:0),i=e.containerElement.get(0)===e.element.parent().get(0),n=/relative|absolute/.test(e.containerElement.css("position")),i&&n?(e.offset.left=e.parentData.left+e.position.left,e.offset.top=e.parentData.top+e.position.top):(e.offset.left=e.element.offset().left,e.offset.top=e.element.offset().top),n=Math.abs(e.sizeDiff.width+(e._helper?e.offset.left-a.left:e.offset.left-s.left)),s=Math.abs(e.sizeDiff.height+(e._helper?e.offset.top-a.top:e.offset.top-s.top)),n+e.size.width>=e.parentData.width&&(e.size.width=e.parentData.width-n,o&&(e.size.height=e.size.width/e.aspectRatio,t=!1)),s+e.size.height>=e.parentData.height&&(e.size.height=e.parentData.height-s,o&&(e.size.width=e.size.height*e.aspectRatio,t=!1)),t||(e.position.left=e.prevPosition.left,e.position.top=e.prevPosition.top,e.size.width=e.prevSize.width,e.size.height=e.prevSize.height)},stop:function(){var t=V(this).resizable("instance"),e=t.options,i=t.containerOffset,s=t.containerPosition,n=t.containerElement,o=V(t.helper),a=o.offset(),r=o.outerWidth()-t.sizeDiff.width,o=o.outerHeight()-t.sizeDiff.height;t._helper&&!e.animate&&/relative/.test(n.css("position"))&&V(this).css({left:a.left-s.left-i.left,width:r,height:o}),t._helper&&!e.animate&&/static/.test(n.css("position"))&&V(this).css({left:a.left-s.left-i.left,width:r,height:o})}}),V.ui.plugin.add("resizable","alsoResize",{start:function(){var t=V(this).resizable("instance").options;V(t.alsoResize).each(function(){var t=V(this);t.data("ui-resizable-alsoresize",{width:parseFloat(t.width()),height:parseFloat(t.height()),left:parseFloat(t.css("left")),top:parseFloat(t.css("top"))})})},resize:function(t,i){var e=V(this).resizable("instance"),s=e.options,n=e.originalSize,o=e.originalPosition,a={height:e.size.height-n.height||0,width:e.size.width-n.width||0,top:e.position.top-o.top||0,left:e.position.left-o.left||0};V(s.alsoResize).each(function(){var t=V(this),s=V(this).data("ui-resizable-alsoresize"),n={},e=t.parents(i.originalElement[0]).length?["width","height"]:["width","height","top","left"];V.each(e,function(t,e){var i=(s[e]||0)+(a[e]||0);i&&0<=i&&(n[e]=i||null)}),t.css(n)})},stop:function(){V(this).removeData("ui-resizable-alsoresize")}}),V.ui.plugin.add("resizable","ghost",{start:function(){var t=V(this).resizable("instance"),e=t.size;t.ghost=t.originalElement.clone(),t.ghost.css({opacity:.25,display:"block",position:"relative",height:e.height,width:e.width,margin:0,left:0,top:0}),t._addClass(t.ghost,"ui-resizable-ghost"),!1!==V.uiBackCompat&&"string"==typeof t.options.ghost&&t.ghost.addClass(this.options.ghost),t.ghost.appendTo(t.helper)},resize:function(){var t=V(this).resizable("instance");t.ghost&&t.ghost.css({position:"relative",height:t.size.height,width:t.size.width})},stop:function(){var t=V(this).resizable("instance");t.ghost&&t.helper&&t.helper.get(0).removeChild(t.ghost.get(0))}}),V.ui.plugin.add("resizable","grid",{resize:function(){var t,e=V(this).resizable("instance"),i=e.options,s=e.size,n=e.originalSize,o=e.originalPosition,a=e.axis,r="number"==typeof i.grid?[i.grid,i.grid]:i.grid,l=r[0]||1,h=r[1]||1,c=Math.round((s.width-n.width)/l)*l,u=Math.round((s.height-n.height)/h)*h,d=n.width+c,p=n.height+u,f=i.maxWidth&&i.maxWidthd,s=i.minHeight&&i.minHeight>p;i.grid=r,m&&(d+=l),s&&(p+=h),f&&(d-=l),g&&(p-=h),/^(se|s|e)$/.test(a)?(e.size.width=d,e.size.height=p):/^(ne)$/.test(a)?(e.size.width=d,e.size.height=p,e.position.top=o.top-u):/^(sw)$/.test(a)?(e.size.width=d,e.size.height=p,e.position.left=o.left-c):((p-h<=0||d-l<=0)&&(t=e._getPaddingPlusBorderDimensions(this)),0"),this._addClass(this.helper,"ui-selectable-helper")},_destroy:function(){this.selectees.removeData("selectable-item"),this._mouseDestroy()},_mouseStart:function(i){var s=this,t=this.options;this.opos=[i.pageX,i.pageY],this.elementPos=V(this.element[0]).offset(),this.options.disabled||(this.selectees=V(t.filter,this.element[0]),this._trigger("start",i),V(t.appendTo).append(this.helper),this.helper.css({left:i.pageX,top:i.pageY,width:0,height:0}),t.autoRefresh&&this.refresh(),this.selectees.filter(".ui-selected").each(function(){var t=V.data(this,"selectable-item");t.startselected=!0,i.metaKey||i.ctrlKey||(s._removeClass(t.$element,"ui-selected"),t.selected=!1,s._addClass(t.$element,"ui-unselecting"),t.unselecting=!0,s._trigger("unselecting",i,{unselecting:t.element}))}),V(i.target).parents().addBack().each(function(){var t,e=V.data(this,"selectable-item");if(e)return t=!i.metaKey&&!i.ctrlKey||!e.$element.hasClass("ui-selected"),s._removeClass(e.$element,t?"ui-unselecting":"ui-selected")._addClass(e.$element,t?"ui-selecting":"ui-unselecting"),e.unselecting=!t,e.selecting=t,(e.selected=t)?s._trigger("selecting",i,{selecting:e.element}):s._trigger("unselecting",i,{unselecting:e.element}),!1}))},_mouseDrag:function(s){if(this.dragged=!0,!this.options.disabled){var t,n=this,o=this.options,a=this.opos[0],r=this.opos[1],l=s.pageX,h=s.pageY;return ll||i.righth||i.bottoma&&i.rightr&&i.bottom *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3,activate:null,beforeStop:null,change:null,deactivate:null,out:null,over:null,receive:null,remove:null,sort:null,start:null,stop:null,update:null},_isOverAxis:function(t,e,i){return e<=t&&t*{ cursor: "+o.cursor+" !important; }").appendTo(n)),o.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",o.zIndex)),o.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",o.opacity)),this.scrollParent[0]!==this.document[0]&&"HTML"!==this.scrollParent[0].tagName&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",t,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions(),!i)for(s=this.containers.length-1;0<=s;s--)this.containers[s]._trigger("activate",t,this._uiHash(this));return V.ui.ddmanager&&(V.ui.ddmanager.current=this),V.ui.ddmanager&&!o.dropBehaviour&&V.ui.ddmanager.prepareOffsets(this,t),this.dragging=!0,this._addClass(this.helper,"ui-sortable-helper"),this.helper.parent().is(this.appendTo)||(this.helper.detach().appendTo(this.appendTo),this.offset.parent=this._getParentOffset()),this.position=this.originalPosition=this._generatePosition(t),this.originalPageX=t.pageX,this.originalPageY=t.pageY,this.lastPositionAbs=this.positionAbs=this._convertPositionTo("absolute"),this._mouseDrag(t),!0},_scroll:function(t){var e=this.options,i=!1;return this.scrollParent[0]!==this.document[0]&&"HTML"!==this.scrollParent[0].tagName?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-t.pageYt[this.floating?"width":"height"]?h&&c:o",i.document[0]);return i._addClass(t,"ui-sortable-placeholder",s||i.currentItem[0].className)._removeClass(t,"ui-sortable-helper"),"tbody"===n?i._createTrPlaceholder(i.currentItem.find("tr").eq(0),V("",i.document[0]).appendTo(t)):"tr"===n?i._createTrPlaceholder(i.currentItem,t):"img"===n&&t.attr("src",i.currentItem.attr("src")),s||t.css("visibility","hidden"),t},update:function(t,e){s&&!o.forcePlaceholderSize||(e.height()&&(!o.forcePlaceholderSize||"tbody"!==n&&"tr"!==n)||e.height(i.currentItem.innerHeight()-parseInt(i.currentItem.css("paddingTop")||0,10)-parseInt(i.currentItem.css("paddingBottom")||0,10)),e.width()||e.width(i.currentItem.innerWidth()-parseInt(i.currentItem.css("paddingLeft")||0,10)-parseInt(i.currentItem.css("paddingRight")||0,10)))}}),i.placeholder=V(o.placeholder.element.call(i.element,i.currentItem)),i.currentItem.after(i.placeholder),o.placeholder.update(i,i.placeholder)},_createTrPlaceholder:function(t,e){var i=this;t.children().each(function(){V(" ",i.document[0]).attr("colspan",V(this).attr("colspan")||1).appendTo(e)})},_contactContainers:function(t){for(var e,i,s,n,o,a,r,l,h,c=null,u=null,d=this.containers.length-1;0<=d;d--)V.contains(this.currentItem[0],this.containers[d].element[0])||(this._intersectsWith(this.containers[d].containerCache)?c&&V.contains(this.containers[d].element[0],c.element[0])||(c=this.containers[d],u=d):this.containers[d].containerCache.over&&(this.containers[d]._trigger("out",t,this._uiHash(this)),this.containers[d].containerCache.over=0));if(c)if(1===this.containers.length)this.containers[u].containerCache.over||(this.containers[u]._trigger("over",t,this._uiHash(this)),this.containers[u].containerCache.over=1);else{for(i=1e4,s=null,n=(l=c.floating||this._isFloating(this.currentItem))?"left":"top",o=l?"width":"height",h=l?"pageX":"pageY",e=this.items.length-1;0<=e;e--)V.contains(this.containers[u].element[0],this.items[e].item[0])&&this.items[e].item[0]!==this.currentItem[0]&&(a=this.items[e].item.offset()[n],r=!1,t[h]-a>this.items[e][o]/2&&(r=!0),Math.abs(t[h]-a)this.containment[2]&&(i=this.containment[2]+this.offset.click.left),t.pageY-this.offset.click.top>this.containment[3]&&(s=this.containment[3]+this.offset.click.top)),e.grid&&(t=this.originalPageY+Math.round((s-this.originalPageY)/e.grid[1])*e.grid[1],s=!this.containment||t-this.offset.click.top>=this.containment[1]&&t-this.offset.click.top<=this.containment[3]?t:t-this.offset.click.top>=this.containment[1]?t-e.grid[1]:t+e.grid[1],t=this.originalPageX+Math.round((i-this.originalPageX)/e.grid[0])*e.grid[0],i=!this.containment||t-this.offset.click.left>=this.containment[0]&&t-this.offset.click.left<=this.containment[2]?t:t-this.offset.click.left>=this.containment[0]?t-e.grid[0]:t+e.grid[0])),{top:s-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():o?0:n.scrollTop()),left:i-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():o?0:n.scrollLeft())}},_rearrange:function(t,e,i,s){i?i[0].appendChild(this.placeholder[0]):e.item[0].parentNode.insertBefore(this.placeholder[0],"down"===this.direction?e.item[0]:e.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var n=this.counter;this._delay(function(){n===this.counter&&this.refreshPositions(!s)})},_clear:function(t,e){this.reverting=!1;var i,s=[];if(!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null,this.helper[0]===this.currentItem[0]){for(i in this._storedCSS)"auto"!==this._storedCSS[i]&&"static"!==this._storedCSS[i]||(this._storedCSS[i]="");this.currentItem.css(this._storedCSS),this._removeClass(this.currentItem,"ui-sortable-helper")}else this.currentItem.show();function n(e,i,s){return function(t){s._trigger(e,t,i._uiHash(i))}}for(this.fromOutside&&!e&&s.push(function(t){this._trigger("receive",t,this._uiHash(this.fromOutside))}),!this.fromOutside&&this.domPosition.prev===this.currentItem.prev().not(".ui-sortable-helper")[0]&&this.domPosition.parent===this.currentItem.parent()[0]||e||s.push(function(t){this._trigger("update",t,this._uiHash())}),this!==this.currentContainer&&(e||(s.push(function(t){this._trigger("remove",t,this._uiHash())}),s.push(function(e){return function(t){e._trigger("receive",t,this._uiHash(this))}}.call(this,this.currentContainer)),s.push(function(e){return function(t){e._trigger("update",t,this._uiHash(this))}}.call(this,this.currentContainer)))),i=this.containers.length-1;0<=i;i--)e||s.push(n("deactivate",this,this.containers[i])),this.containers[i].containerCache.over&&(s.push(n("out",this,this.containers[i])),this.containers[i].containerCache.over=0);if(this.storedCursor&&(this.document.find("body").css("cursor",this.storedCursor),this.storedStylesheet.remove()),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex","auto"===this._storedZIndex?"":this._storedZIndex),this.dragging=!1,e||this._trigger("beforeStop",t,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.cancelHelperRemoval||(this.helper[0]!==this.currentItem[0]&&this.helper.remove(),this.helper=null),!e){for(i=0;i li > :first-child").add(t.find("> :not(li)").even())},heightStyle:"auto",icons:{activeHeader:"ui-icon-triangle-1-s",header:"ui-icon-triangle-1-e"},activate:null,beforeActivate:null},hideProps:{borderTopWidth:"hide",borderBottomWidth:"hide",paddingTop:"hide",paddingBottom:"hide",height:"hide"},showProps:{borderTopWidth:"show",borderBottomWidth:"show",paddingTop:"show",paddingBottom:"show",height:"show"},_create:function(){var t=this.options;this.prevShow=this.prevHide=V(),this._addClass("ui-accordion","ui-widget ui-helper-reset"),this.element.attr("role","tablist"),t.collapsible||!1!==t.active&&null!=t.active||(t.active=0),this._processPanels(),t.active<0&&(t.active+=this.headers.length),this._refresh()},_getCreateEventData:function(){return{header:this.active,panel:this.active.length?this.active.next():V()}},_createIcons:function(){var t,e=this.options.icons;e&&(t=V(""),this._addClass(t,"ui-accordion-header-icon","ui-icon "+e.header),t.prependTo(this.headers),t=this.active.children(".ui-accordion-header-icon"),this._removeClass(t,e.header)._addClass(t,null,e.activeHeader)._addClass(this.headers,"ui-accordion-icons"))},_destroyIcons:function(){this._removeClass(this.headers,"ui-accordion-icons"),this.headers.children(".ui-accordion-header-icon").remove()},_destroy:function(){var t;this.element.removeAttr("role"),this.headers.removeAttr("role aria-expanded aria-selected aria-controls tabIndex").removeUniqueId(),this._destroyIcons(),t=this.headers.next().css("display","").removeAttr("role aria-hidden aria-labelledby").removeUniqueId(),"content"!==this.options.heightStyle&&t.css("height","")},_setOption:function(t,e){"active"!==t?("event"===t&&(this.options.event&&this._off(this.headers,this.options.event),this._setupEvents(e)),this._super(t,e),"collapsible"!==t||e||!1!==this.options.active||this._activate(0),"icons"===t&&(this._destroyIcons(),e&&this._createIcons())):this._activate(e)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",t),this._toggleClass(null,"ui-state-disabled",!!t),this._toggleClass(this.headers.add(this.headers.next()),null,"ui-state-disabled",!!t)},_keydown:function(t){if(!t.altKey&&!t.ctrlKey){var e=V.ui.keyCode,i=this.headers.length,s=this.headers.index(t.target),n=!1;switch(t.keyCode){case e.RIGHT:case e.DOWN:n=this.headers[(s+1)%i];break;case e.LEFT:case e.UP:n=this.headers[(s-1+i)%i];break;case e.SPACE:case e.ENTER:this._eventHandler(t);break;case e.HOME:n=this.headers[0];break;case e.END:n=this.headers[i-1]}n&&(V(t.target).attr("tabIndex",-1),V(n).attr("tabIndex",0),V(n).trigger("focus"),t.preventDefault())}},_panelKeyDown:function(t){t.keyCode===V.ui.keyCode.UP&&t.ctrlKey&&V(t.currentTarget).prev().trigger("focus")},refresh:function(){var t=this.options;this._processPanels(),!1===t.active&&!0===t.collapsible||!this.headers.length?(t.active=!1,this.active=V()):!1===t.active?this._activate(0):this.active.length&&!V.contains(this.element[0],this.active[0])?this.headers.length===this.headers.find(".ui-state-disabled").length?(t.active=!1,this.active=V()):this._activate(Math.max(0,t.active-1)):t.active=this.headers.index(this.active),this._destroyIcons(),this._refresh()},_processPanels:function(){var t=this.headers,e=this.panels;"function"==typeof this.options.header?this.headers=this.options.header(this.element):this.headers=this.element.find(this.options.header),this._addClass(this.headers,"ui-accordion-header ui-accordion-header-collapsed","ui-state-default"),this.panels=this.headers.next().filter(":not(.ui-accordion-content-active)").hide(),this._addClass(this.panels,"ui-accordion-content","ui-helper-reset ui-widget-content"),e&&(this._off(t.not(this.headers)),this._off(e.not(this.panels)))},_refresh:function(){var i,t=this.options,e=t.heightStyle,s=this.element.parent();this.active=this._findActive(t.active),this._addClass(this.active,"ui-accordion-header-active","ui-state-active")._removeClass(this.active,"ui-accordion-header-collapsed"),this._addClass(this.active.next(),"ui-accordion-content-active"),this.active.next().show(),this.headers.attr("role","tab").each(function(){var t=V(this),e=t.uniqueId().attr("id"),i=t.next(),s=i.uniqueId().attr("id");t.attr("aria-controls",s),i.attr("aria-labelledby",e)}).next().attr("role","tabpanel"),this.headers.not(this.active).attr({"aria-selected":"false","aria-expanded":"false",tabIndex:-1}).next().attr({"aria-hidden":"true"}).hide(),this.active.length?this.active.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0}).next().attr({"aria-hidden":"false"}):this.headers.eq(0).attr("tabIndex",0),this._createIcons(),this._setupEvents(t.event),"fill"===e?(i=s.height(),this.element.siblings(":visible").each(function(){var t=V(this),e=t.css("position");"absolute"!==e&&"fixed"!==e&&(i-=t.outerHeight(!0))}),this.headers.each(function(){i-=V(this).outerHeight(!0)}),this.headers.next().each(function(){V(this).height(Math.max(0,i-V(this).innerHeight()+V(this).height()))}).css("overflow","auto")):"auto"===e&&(i=0,this.headers.next().each(function(){var t=V(this).is(":visible");t||V(this).show(),i=Math.max(i,V(this).css("height","").height()),t||V(this).hide()}).height(i))},_activate:function(t){t=this._findActive(t)[0];t!==this.active[0]&&(t=t||this.active[0],this._eventHandler({target:t,currentTarget:t,preventDefault:V.noop}))},_findActive:function(t){return"number"==typeof t?this.headers.eq(t):V()},_setupEvents:function(t){var i={keydown:"_keydown"};t&&V.each(t.split(" "),function(t,e){i[e]="_eventHandler"}),this._off(this.headers.add(this.headers.next())),this._on(this.headers,i),this._on(this.headers.next(),{keydown:"_panelKeyDown"}),this._hoverable(this.headers),this._focusable(this.headers)},_eventHandler:function(t){var e=this.options,i=this.active,s=V(t.currentTarget),n=s[0]===i[0],o=n&&e.collapsible,a=o?V():s.next(),r=i.next(),a={oldHeader:i,oldPanel:r,newHeader:o?V():s,newPanel:a};t.preventDefault(),n&&!e.collapsible||!1===this._trigger("beforeActivate",t,a)||(e.active=!o&&this.headers.index(s),this.active=n?V():s,this._toggle(a),this._removeClass(i,"ui-accordion-header-active","ui-state-active"),e.icons&&(i=i.children(".ui-accordion-header-icon"),this._removeClass(i,null,e.icons.activeHeader)._addClass(i,null,e.icons.header)),n||(this._removeClass(s,"ui-accordion-header-collapsed")._addClass(s,"ui-accordion-header-active","ui-state-active"),e.icons&&(n=s.children(".ui-accordion-header-icon"),this._removeClass(n,null,e.icons.header)._addClass(n,null,e.icons.activeHeader)),this._addClass(s.next(),"ui-accordion-content-active")))},_toggle:function(t){var e=t.newPanel,i=this.prevShow.length?this.prevShow:t.oldPanel;this.prevShow.add(this.prevHide).stop(!0,!0),this.prevShow=e,this.prevHide=i,this.options.animate?this._animate(e,i,t):(i.hide(),e.show(),this._toggleComplete(t)),i.attr({"aria-hidden":"true"}),i.prev().attr({"aria-selected":"false","aria-expanded":"false"}),e.length&&i.length?i.prev().attr({tabIndex:-1,"aria-expanded":"false"}):e.length&&this.headers.filter(function(){return 0===parseInt(V(this).attr("tabIndex"),10)}).attr("tabIndex",-1),e.attr("aria-hidden","false").prev().attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0})},_animate:function(t,i,e){var s,n,o,a=this,r=0,l=t.css("box-sizing"),h=t.length&&(!i.length||t.index()",delay:300,options:{icons:{submenu:"ui-icon-caret-1-e"},items:"> *",menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.lastMousePosition={x:null,y:null},this.element.uniqueId().attr({role:this.options.role,tabIndex:0}),this._addClass("ui-menu","ui-widget ui-widget-content"),this._on({"mousedown .ui-menu-item":function(t){t.preventDefault(),this._activateItem(t)},"click .ui-menu-item":function(t){var e=V(t.target),i=V(V.ui.safeActiveElement(this.document[0]));!this.mouseHandled&&e.not(".ui-state-disabled").length&&(this.select(t),t.isPropagationStopped()||(this.mouseHandled=!0),e.has(".ui-menu").length?this.expand(t):!this.element.is(":focus")&&i.closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":"_activateItem","mousemove .ui-menu-item":"_activateItem",mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this._menuItems().first();e||this.focus(t,i)},blur:function(t){this._delay(function(){V.contains(this.element[0],V.ui.safeActiveElement(this.document[0]))||this.collapseAll(t)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(t){this._closeOnDocumentClick(t)&&this.collapseAll(t,!0),this.mouseHandled=!1}})},_activateItem:function(t){var e,i;this.previousFilter||t.clientX===this.lastMousePosition.x&&t.clientY===this.lastMousePosition.y||(this.lastMousePosition={x:t.clientX,y:t.clientY},e=V(t.target).closest(".ui-menu-item"),i=V(t.currentTarget),e[0]===i[0]&&(i.is(".ui-state-active")||(this._removeClass(i.siblings().children(".ui-state-active"),null,"ui-state-active"),this.focus(t,i))))},_destroy:function(){var t=this.element.find(".ui-menu-item").removeAttr("role aria-disabled").children(".ui-menu-item-wrapper").removeUniqueId().removeAttr("tabIndex role aria-haspopup");this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeAttr("role aria-labelledby aria-expanded aria-hidden aria-disabled tabIndex").removeUniqueId().show(),t.children().each(function(){var t=V(this);t.data("ui-menu-submenu-caret")&&t.remove()})},_keydown:function(t){var e,i,s,n=!0;switch(t.keyCode){case V.ui.keyCode.PAGE_UP:this.previousPage(t);break;case V.ui.keyCode.PAGE_DOWN:this.nextPage(t);break;case V.ui.keyCode.HOME:this._move("first","first",t);break;case V.ui.keyCode.END:this._move("last","last",t);break;case V.ui.keyCode.UP:this.previous(t);break;case V.ui.keyCode.DOWN:this.next(t);break;case V.ui.keyCode.LEFT:this.collapse(t);break;case V.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(t);break;case V.ui.keyCode.ENTER:case V.ui.keyCode.SPACE:this._activate(t);break;case V.ui.keyCode.ESCAPE:this.collapse(t);break;default:e=this.previousFilter||"",s=n=!1,i=96<=t.keyCode&&t.keyCode<=105?(t.keyCode-96).toString():String.fromCharCode(t.keyCode),clearTimeout(this.filterTimer),i===e?s=!0:i=e+i,e=this._filterMenuItems(i),(e=s&&-1!==e.index(this.active.next())?this.active.nextAll(".ui-menu-item"):e).length||(i=String.fromCharCode(t.keyCode),e=this._filterMenuItems(i)),e.length?(this.focus(t,e),this.previousFilter=i,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter}n&&t.preventDefault()},_activate:function(t){this.active&&!this.active.is(".ui-state-disabled")&&(this.active.children("[aria-haspopup='true']").length?this.expand(t):this.select(t))},refresh:function(){var t,e,s=this,n=this.options.icons.submenu,i=this.element.find(this.options.menus);this._toggleClass("ui-menu-icons",null,!!this.element.find(".ui-icon").length),e=i.filter(":not(.ui-menu)").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var t=V(this),e=t.prev(),i=V("").data("ui-menu-submenu-caret",!0);s._addClass(i,"ui-menu-icon","ui-icon "+n),e.attr("aria-haspopup","true").prepend(i),t.attr("aria-labelledby",e.attr("id"))}),this._addClass(e,"ui-menu","ui-widget ui-widget-content ui-front"),(t=i.add(this.element).find(this.options.items)).not(".ui-menu-item").each(function(){var t=V(this);s._isDivider(t)&&s._addClass(t,"ui-menu-divider","ui-widget-content")}),i=(e=t.not(".ui-menu-item, .ui-menu-divider")).children().not(".ui-menu").uniqueId().attr({tabIndex:-1,role:this._itemRole()}),this._addClass(e,"ui-menu-item")._addClass(i,"ui-menu-item-wrapper"),t.filter(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!V.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){var i;"icons"===t&&(i=this.element.find(".ui-menu-icon"),this._removeClass(i,null,this.options.icons.submenu)._addClass(i,null,e.submenu)),this._super(t,e)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",String(t)),this._toggleClass(null,"ui-state-disabled",!!t)},focus:function(t,e){var i;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),i=this.active.children(".ui-menu-item-wrapper"),this._addClass(i,null,"ui-state-active"),this.options.role&&this.element.attr("aria-activedescendant",i.attr("id")),i=this.active.parent().closest(".ui-menu-item").children(".ui-menu-item-wrapper"),this._addClass(i,null,"ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),(i=e.children(".ui-menu")).length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(t){var e,i,s;this._hasScroll()&&(i=parseFloat(V.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(V.css(this.activeMenu[0],"paddingTop"))||0,e=t.offset().top-this.activeMenu.offset().top-i-s,i=this.activeMenu.scrollTop(),s=this.activeMenu.height(),t=t.outerHeight(),e<0?this.activeMenu.scrollTop(i+e):s",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,liveRegionTimer:null,_create:function(){var i,s,n,t=this.element[0].nodeName.toLowerCase(),e="textarea"===t,t="input"===t;this.isMultiLine=e||!t&&this._isContentEditable(this.element),this.valueMethod=this.element[e||t?"val":"text"],this.isNewMenu=!0,this._addClass("ui-autocomplete-input"),this.element.attr("autocomplete","off"),this._on(this.element,{keydown:function(t){if(this.element.prop("readOnly"))s=n=i=!0;else{s=n=i=!1;var e=V.ui.keyCode;switch(t.keyCode){case e.PAGE_UP:i=!0,this._move("previousPage",t);break;case e.PAGE_DOWN:i=!0,this._move("nextPage",t);break;case e.UP:i=!0,this._keyEvent("previous",t);break;case e.DOWN:i=!0,this._keyEvent("next",t);break;case e.ENTER:this.menu.active&&(i=!0,t.preventDefault(),this.menu.select(t));break;case e.TAB:this.menu.active&&this.menu.select(t);break;case e.ESCAPE:this.menu.element.is(":visible")&&(this.isMultiLine||this._value(this.term),this.close(t),t.preventDefault());break;default:s=!0,this._searchTimeout(t)}}},keypress:function(t){if(i)return i=!1,void(this.isMultiLine&&!this.menu.element.is(":visible")||t.preventDefault());if(!s){var e=V.ui.keyCode;switch(t.keyCode){case e.PAGE_UP:this._move("previousPage",t);break;case e.PAGE_DOWN:this._move("nextPage",t);break;case e.UP:this._keyEvent("previous",t);break;case e.DOWN:this._keyEvent("next",t)}}},input:function(t){if(n)return n=!1,void t.preventDefault();this._searchTimeout(t)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(t){clearTimeout(this.searching),this.close(t),this._change(t)}}),this._initSource(),this.menu=V("