diff --git a/css/main.css b/css/main.css index 8db139d..78c9e5d 100644 --- a/css/main.css +++ b/css/main.css @@ -497,6 +497,20 @@ td { box-shadow: 0 0 0 3px #4527A0; } +/* .short-option-answer { + width: 100%; + display: block; + border: 1px solid #4527A0; + margin: 0.8rem 1.6rem 1.6rem 1.6rem; + overflow: hidden; + resize: none; + padding: 0.4rem; +} + +.short-option-answer:focus { + box-shadow: 0 0 0 3px #4527A0; +} */ + img { display: block; padding: 0; @@ -1124,4 +1138,33 @@ h2:has(+ #exam-list:empty), h2:has(+ #packet-list:empty){ #exam-list:empty::after, #packet-list:empty::after{ content: "None available."; opacity: 30%; +} + +#viewer-title-bar { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 0.8rem 1.6rem; + background-color: gray; + color: white; +} + +.title-bar-button { + display: inline-flex; + flex-direction: column; + align-items: center; + border: none; + margin: 0 0 0 2px; + background-color: gray; + color: white; + border-left: 1px solid white; +} + +.viewer-icon { + margin: auto; +} + +.title-bar-button:hover { + background-color: darkgray; } \ No newline at end of file diff --git a/js/main.js b/js/main.js index b573cdc..f9cc97a 100644 --- a/js/main.js +++ b/js/main.js @@ -515,6 +515,15 @@ async function loadExamList(data) { }); // Sort different lists + $(".packet-list.short div") + .sort(function(a, b) { + return a.dataset.eid > b.dataset.eid ? + 1 : + a.dataset.eid < b.dataset.eid ? + -1 : + 0; + }) + .appendTo(".packet-list.short"); $(".packet-list.rapid div") .sort(function(a, b) { return a.dataset.eid > b.dataset.eid ? @@ -1040,6 +1049,8 @@ function setUpQuestions(load_previous) { if (!load_previous && exam_time == null) { if (question_type == "rapid") { exam_time = 35 * 60; + } else if (question_type == "short") { + exam_time = 120 * 60; } else if (question_type == "anatomy") { exam_time = 90 * 60; } else if (question_type == "long") { @@ -1291,6 +1302,138 @@ async function loadQuestion(n, section = 1, force_reload = false) { ap.empty(); switch (question_data.type) { + case "short": { + ap.append( + '

' + + display_n + + '.1Case normal/abnormal

' + ); + ap.append( + '' + ); + // Handle changing display of optional answer fields and saving of data + $("#rapid-option").change(function(evt) { + if (evt.target.value == "Abnormal") { + $("#rapid-text").css("display", "block"); + } else { + $("#rapid-text").css("display", "none"); + } + + let answer = { + aid: aid, + cid: cid, + eid: eid, + qid: qid, + qidn: "1", + passcode: passcode, + ans: evt.target.value, + }; + + db.answers.put(answer); + console.debug("Save ", answer) + saveSession(); + updateQuestionListPanel(answer); + }); + + // Save long answers on textchange + $(".long-answer").change(function(evt) { + // ignore blank text and delete any stored value + if (evt.target.value.length < 1) { + db.answers.delete([aid, cid, eid, qid, "2"]); + $( + "#question-list-item-" + + exam_details.question_order.indexOf(qid) + + "-2" + ).removeClass("question-saved-localdb"); + return; + } + + let answer = { + aid: aid, + cid: cid, + eid: eid, + qid: qid, + qidn: "2", + passcode: passcode, + ans: evt.target.value, + }; + // db.answers.put({aid: [cid, eid, qidn], ans: evt.target.value}); + console.debug("Save ", answer) + db.answers.put(answer); + + $( + "#question-list-item-" + + exam_details.question_order.indexOf(qid) + + "-2" + ).addClass("question-saved-localdb"); + + saveSession(); + updateQuestionListPanel(answer); + }); + + // We chain our db requests as we can only check answers once + // they have been loaded (should probably use then or after) + + console.debug("Load rapid answers") + db.answers + .get({ + aid: aid, + cid: cid, + eid: eid, + qid: qid, + passcode: passcode, + qidn: "1" + }) + .then(function(answer) { + if (answer != undefined) { + $("#rapid-option option:contains(" + answer.ans + ")").prop( + "selected", + true + ); + // For some reason a change event is not fired... + if (answer.ans == "Abnormal") { + $("#rapid-text").css("display", "block"); + } + $("#rapid-option-not-answered").remove(); + } + }) + .catch(function(error) { + console.debug("DB", cid, eid, qid); + console.debug("error-", error); + }) + //.then(function() { + db.answers + .get({ + aid: aid, + cid: cid, + eid: eid, + qid: qid, + qidn: "2", + passcode: passcode, + }) + .then(function(answer) { + if (answer != undefined) { + $(".long-answer").text(answer.ans); + } + markAnswer(qid, question_data, n); + }) + .catch(function(error) { + console.debug("error-", error); + }) + //}); + + addFlagEvents(); + loadFlagsFromDb(qid, "1"); + loadFlagsFromDb(qid, "2"); + + break; + } case "rapid": { ap.append( '

' + @@ -1646,7 +1789,7 @@ function updateQuestionListPanel(answer) { answer.qidn ).addClass("question-saved-localdb"); - if (question_type == "rapid" && answer.qidn == "1") { + if ((question_type == "rapid" || question_type == "short") && answer.qidn == "1") { if (answer.ans == "Abnormal") { $( "#question-list-item-" + @@ -1795,6 +1938,12 @@ function createQuestionListPanel() { appendQuestionListItem(n, "1"); appendQuestionListItem(n, "2").hide(); } + } else if (question_type == "short") { + // Loop through all questions and append list items + for (let n = 1; n < exam_details.number_of_questions + 1; n++) { + appendQuestionListItem(n, "1"); + appendQuestionListItem(n, "2").hide(); + } } else if (question_type == "anatomy") { for (let n = 1; n < exam_details.number_of_questions + 1; n++) { appendQuestionListItem(n, "1"); @@ -2194,6 +2343,69 @@ function reviewQuestions() { } } } + } else if (question_type == "short") { + // First check normal vs abnormal + if (question["normal"] == true) { + if (section_1_answer == "Normal") { + setReviewAnswer( + el, + "correct", + section_1_answer, + true, + question_answers + ); + correct_count++; + questions_correct[qid] = true; + } else { + if (section_1_answer != "Not Answered") { + overcall_number++; + } + setReviewAnswer( + el, + "incorrect", + section_1_answer, + true, + question_answers + ); + questions_correct[qid] = false; + } + } else { + if (answerInArray(question_answers, section_2_answer)) { + correct_count++; + setReviewAnswer( + el, + "correct", + section_2_answer, + false, + question_answers + ); + questions_correct[qid] = true; + } else { + setReviewAnswer( + el, + "incorrect", + section_2_answer, + false, + question_answers + ); + questions_correct[qid] = false; + + if (section_1_answer == "Not Answered") {} else if (section_1_answer == "Normal") { + undercall_number++; + } else { + // Incorrect calls could be correct if + // the answer is not in the database + incorrectcall_number++; + + el.append( + "[Mark correct]" + ).click(() => { + markCorrect(qid, section_2_answer, question_type); + reviewQuestions(); + }); + } + } + } } else if (question_type == "anatomy") { // Anatomy answers are either correct or incorrect if (answerInArray(question_answers, section_1_answer)) { @@ -2278,7 +2490,7 @@ function markAnswer(qid, current_question, n) { $(".long-answer").attr("readonly", "true"); $(".long-answer").addClass("long-answer-marked"); - if (type == "rapid") { + if (type == "rapid" || type == "short") { option = document.getElementById("rapid-option"); // If the current question is normal if (current_question.normal == true) { diff --git a/js/viewer.js b/js/viewer.js index 1d6dd3e..1f3ac5a 100644 --- a/js/viewer.js +++ b/js/viewer.js @@ -11,6 +11,9 @@ export function loadMainImage(image, stack) { const RotateTool = cornerstoneTools.RotateTool; const StackScrollTool = cornerstoneTools.StackScrollTool; const ArrowAnnotateTool = cornerstoneTools.ArrowAnnotateTool; + const LengthTool = cornerstoneTools.LengthTool; + const RectangleRoiTool = cornerstoneTools.RectangleRoiTool; + const EllipseRoiTool = cornerstoneTools.EllipseRoiTool; const AngleTool = cornerstoneTools.AngleTool; const CobbAngleTool = cornerstoneTools.CobbAngleTool; const EraserTool = cornerstoneTools.EraserTool; @@ -35,6 +38,9 @@ export function loadMainImage(image, stack) { cornerstoneTools.addToolForElement(element, CobbAngleTool); cornerstoneTools.addToolForElement(element, StackScrollTool); cornerstoneTools.addToolForElement(element, EraserTool); + cornerstoneTools.addToolForElement(element, LengthTool); + cornerstoneTools.addToolForElement(element, RectangleRoiTool); + //cornerstoneTools.addToolForElement(element, EllipseRoiTool); cornerstoneTools.addToolForElement(element, ArrowAnnotateTool, { configuration: { @@ -58,15 +64,17 @@ export function loadMainImage(image, stack) { setDicomCanvasNonFullscreen(element); cornerstone.reset(element); - element.scrollIntoView(false); - // element.scrollTo(0); + let viewerTitleBar = document.getElementById("viewer-title-bar"); + viewerTitleBar.scrollIntoView(); + //element.scrollIntoView(false); + //element.scrollTo(0); $(element).dblclick(function () { if ($(".canvas-panel").length == 0) { // already fullscreen (disable it) disableFullscreen(this); } else { - $(".content-panel").append($(".canvas-panel")); + $(".content-panel").prepend($(".canvas-panel")); $(".canvas-panel").toggleClass("canvas-panel canvas-panel-fullscreen"); $("#dicom-image").attr("height", "100%"); $("#dicom-image").height("100%"); @@ -78,28 +86,7 @@ export function loadMainImage(image, stack) { element.removeEventListener("wheel", element.wheelEventHandler); - // Add tool selector - $(".canvas-panel").append( - '' - ); - - $(".control-overlay") + $(".dicom-select-control") .get(0) .addEventListener("change", function () { changeControlSelection(); @@ -117,7 +104,7 @@ function onImageRendered(e) { const eventData = e.detail; // Update ww/wl - const sel = $(".control-overlay").get(0); + const sel = $(".dicom-select-control").get(0); sel.options[find_option(sel, "window")].firstChild.textContent = "window (" + Math.round(eventData.viewport.voi.windowCenter) + @@ -176,7 +163,7 @@ export function disableFullscreen(dicom_element) { $(".canvas-panel-fullscreen").toggleClass( "canvas-panel canvas-panel-fullscreen" ); - $(".question").append($(".canvas-panel")); + $(".question").prepend($(".canvas-panel")); $(".canvas-panel").get(0).scrollIntoView(); setDicomCanvasNonFullscreen(dicom_element); @@ -284,7 +271,7 @@ export function debugCornerstone() { */ export function changeControlSelection() { // We also duplicate the sel that are available - const sel = $(".control-overlay").get(0); + const sel = $(".dicom-select-control").get(0); const old = sel.oldSelectedIndex; sel.oldSelectedIndex = sel.selectedIndex; @@ -309,6 +296,25 @@ export function changeControlSelection() { }); break; } + case "measure": { + cornerstoneTools.setToolActiveForElement(dicom_element, "Length", { + mouseButtonMask: 1, + }); + break; + } + case "rectangle": { + cornerstoneTools.setToolActiveForElement(dicom_element, "RectangleRoi", { + mouseButtonMask: 1, + }); + break; + } + case "ellipse": { + cornerstoneTools.setToolActiveForElement(dicom_element, "EllipseRoi", { + mouseButtonMask: 1, + }); + break; + } + case "angle": { cornerstoneTools.setToolActiveForElement(dicom_element, "Angle", { mouseButtonMask: 1, @@ -384,18 +390,7 @@ export function changeControlSelection() { break; } case "close": { - // disable fullscreen if required - if ($(".canvas-panel").length == 0) { - disableFullscreen(dicom_element); - } - if (dicom_element != undefined) { - // See https://github.com/cornerstonejs/cornerstoneTools/issues/1337 - cornerstone.removeElementData(dicom_element); - cornerstone.disable(dicom_element); - $(".canvas-panel").remove(); - $(".figure-open").removeClass("figure-open").addClass("figure"); - $(dicom_element).remove(); - } + closeViewer(dicom_element); break; } } @@ -424,7 +419,7 @@ export function keydown_handler(event) { if (target_element == "INPUT" || target_element == "TEXTAREA") { return; } - const sel = $(".control-overlay").get(0); + const sel = $(".dicom-select-control").get(0); switch (event.code) { case "KeyP": sel.selectedIndex = find_option(sel, "pan"); @@ -660,7 +655,7 @@ export function openMainImage(current_question, t, source) { // No full size figure / dicom loaded yet if (figure_to_load == open_figure) { // Scroll to the image if it is already open - document.getElementById("dicom-image").scrollIntoView(); + document.getElementById("viewer-title-bar").scrollIntoView(); return; } else { let el; @@ -678,8 +673,33 @@ export function openMainImage(current_question, t, source) { source.className = "figure-open"; - $(".question").append( - '

+
+
Sample Title [REPLACE]
+ + + +
+ +
' ); @@ -700,10 +720,31 @@ export function openMainImage(current_question, t, source) { } } + // Bind button actions + $("#close-viewer-button").click(function () { + const dicom_element = document.getElementById("dicom-image"); + closeViewer(dicom_element); + }); + load(images, annotations); } } +function closeViewer(dicom_element) { + // disable fullscreen if required + if ($(".canvas-panel").length == 0) { + disableFullscreen(dicom_element); + } + if (dicom_element != undefined) { + // See https://github.com/cornerstonejs/cornerstoneTools/issues/1337 + cornerstone.removeElementData(dicom_element); + cornerstone.disable(dicom_element); + $(".canvas-panel").remove(); + $(".figure-open").removeClass("figure-open").addClass("figure"); + $(dicom_element).remove(); + } +} + export function urltoFile(url, filename, mimeType) { return fetch(url) .then(function (res) {