diff --git a/css/main.css b/css/main.css index 4f651a1..d47030b 100644 --- a/css/main.css +++ b/css/main.css @@ -556,6 +556,10 @@ img { #exam-time { font-weight: bold; + width: auto; + max-width: 50px; + border: none; + padding: 0px; } .dialog-cancel { @@ -606,7 +610,12 @@ img { #review-overlay { overflow: scroll; + padding: 20px; +} +#exam-stats { + display: none; + float: right; } .close { @@ -654,6 +663,12 @@ img { color: red; } +.mark-correct { + /* float: right; */ + color: lightblue; + font-size: smaller; +} + textarea.incorrect { background-color: lightcoral; color: lightgray; @@ -886,20 +901,6 @@ select option:disabled { } } -.user-review-answer-list { -float: left; -width: 45%; -padding-left: 5%; -} -.model-review-answer-list { -float: right; -width: 45%; -padding-right: 5%; -} - -#review-answer-list li { -clear: both; -} .displayblock { display: block @@ -910,7 +911,16 @@ display: block text-decoration: underline; } +.answer-sub td { + text-align: center; + width: 50%; + padding-top: 0px; + padding-bottom: 0px; +} + .answer-sub { + padding-top: 0px; + padding-bottom: 0px; opacity: 50%; text-align: center; } @@ -920,4 +930,31 @@ display: block top: 50%; left: 50%; transform: translate(-50%, -50%); +} + +.packet-database-options { + float: right; + font-size: xx-small; +} + +.review-table-heading td { + text-align: center; +} + +.review-table-heading td:hover { + background-color: #311B92; +} + +#review-answer-table { + border: none; +} + +#review-answer-table td { + border: none; + /* border: 3px solid transparent; */ +} + +#review-answer-table tr:hover { + /* border: 3px solid #4527A0; */ + background-color: #4527A0; } \ No newline at end of file diff --git a/index.html b/index.html index 06851fd..07742d6 100644 --- a/index.html +++ b/index.html @@ -71,7 +71,6 @@
-

Available Packets:

@@ -79,8 +78,18 @@
-
+
+

Performance breakdown:

+ Unanswered questions:
+ Answered normal:
+ Answered abnormal:
+
+ Undercall:
+ Overcall:
+ Incorrect call:
+
+
@@ -95,9 +104,13 @@ @@ -105,18 +118,18 @@
diff --git a/js/helpers.js b/js/helpers.js index 2830694..6def092 100644 --- a/js/helpers.js +++ b/js/helpers.js @@ -39,4 +39,4 @@ export function formatBytes(bytes, decimals = 2) { const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; -} \ No newline at end of file +} diff --git a/js/main.js b/js/main.js index 1e0419d..e29182d 100644 --- a/js/main.js +++ b/js/main.js @@ -20,6 +20,8 @@ window.exam_time = 60; window.allow_self_marking = true; +window.timer = null; + //window.dfile = null; retrievePacketList(); @@ -126,19 +128,14 @@ function setUpQuestions() { } }); - console.log("pre", window.question_order); window.question_order = helper.shuffleArray(window.question_order); - console.log("post", window.question_order); window.number_of_questions = Object.keys(window.questions).length; window.review = false; - console.log(window.question_order); // Horrible way to get type of questions // We assume they are all of the same type.... - if (window.question_type == null) { - window.question_type = - window.questions[Object.keys(window.questions)[0]].type; - } + window.question_type = + window.questions[Object.keys(window.questions)[0]].type; if (window.exam_mode) { $("#options-button, #review-overlay-button").hide(); @@ -149,6 +146,8 @@ function setUpQuestions() { loadQuestion(0, 1, true); createQuestionListPanel(); + console.log(window.question_type); + if (window.question_type == "rapid") { window.exam_time = 35 * 60; } else if (window.question_type == "anatomy") { @@ -157,7 +156,11 @@ function setUpQuestions() { window.exam_time = 75 * 60; } - $("#exam-time").text(window.exam_time / 60); + $("#exam-time") + .val(window.exam_time / 60) + .change(() => { + window.exam_time = $("#exam-time").val() * 60; + }); $("#start-dialog").modal(); } @@ -618,35 +621,35 @@ function loadQuestion(n, section = 1, force_reload = false) { display_n + '.1Observations

' + '" data-qidn=1 style="margin-right:0">⚐

' ); ap.append( '

' + display_n + '.2Interpretation

' + '" data-qidn=2 style="margin-right:0">⚐

' ); ap.append( '

' + display_n + '.3Principal Diagnosis

' + '" data-qidn=3 style="margin-right:0">⚐

' ); ap.append( '

' + display_n + '.4Differential Diagnosis

' + '" data-qidn=4 style="margin-right:0">⚐

' ); ap.append( '

' + display_n + '.5Management (if appropriate)

' + '" data-qidn=5 style="margin-right:0">⚐

' ); // Save long answers on textchange @@ -730,7 +733,10 @@ function setFocus(section) { // Horrible (but it works) setTimeout(function () { // In pratique it selects the end of a textarea but I'm not sure if I can be bothered... - $("*[data-answer-section-qidn=" + section + "]").focus(); + // Apparently I can... + const el = $("*[data-answer-section-qidn=" + section + "]"); + const val = el.val(); + el.focus().val("").val(val); }, 200); } @@ -880,50 +886,50 @@ function keydown_handler(event) { switch (event.code) { case "KeyP": sel.selectedIndex = viewer.find_option(sel, "pan"); - changeControlSelection(); + viewer.changeControlSelection(); break; case "KeyZ": sel.selectedIndex = viewer.find_option(sel, "zoom"); - changeControlSelection(); + viewer.changeControlSelection(); break; case "KeyR": sel.selectedIndex = viewer.find_option(sel, "rotate"); - changeControlSelection(); + viewer.changeControlSelection(); break; case "KeyS": sel.selectedIndex = viewer.find_option(sel, "scroll"); - changeControlSelection(); + viewer.changeControlSelection(); break; case "KeyW": sel.selectedIndex = viewer.find_option(sel, "window"); - changeControlSelection(); + viewer.changeControlSelection(); break; case "KeyA": sel.selectedIndex = viewer.find_option(sel, "abdomen"); - changeControlSelection(); + viewer.changeControlSelection(); break; case "KeyU": sel.selectedIndex = viewer.find_option(sel, "pulmonary"); - changeControlSelection(); + viewer.changeControlSelection(); break; case "KeyB": sel.selectedIndex = viewer.find_option(sel, "brain"); - changeControlSelection(); + viewer.changeControlSelection(); break; case "KeyO": sel.selectedIndex = viewer.find_option(sel, "bone"); - changeControlSelection(); + viewer.changeControlSelection(); break; case "KeyE": sel.selectedIndex = viewer.find_option(sel, "reset"); // $("#dicom-image").get(0).scrollIntoView(false); - changeControlSelection(); + viewer.changeControlSelection(); break; // Escape and C do the same case "KeyC": case "Escape": sel.selectedIndex = viewer.find_option(sel, "close"); - changeControlSelection(); + viewer.changeControlSelection(); break; case "Enter": t.focus(); @@ -1016,105 +1022,6 @@ function keydown_handler(event) { event.preventDefault(); } -/** - * Registers the selected dicom tool - */ -function changeControlSelection() { - // We also duplicate the sel that are available - const sel = $(".control-overlay").get(0); - - const old = sel.oldSelectedIndex; - sel.oldSelectedIndex = sel.selectedIndex; - - console.log(event); - - const dicom_element = document.getElementById("dicom-image"); - switch (sel.options[sel.selectedIndex].value) { - case "pan": { - cornerstoneTools.setToolActive("Pan", { mouseButtonMask: 1 }); - break; - } - case "zoom": { - cornerstoneTools.setToolActive("Zoom", { mouseButtonMask: 1 }); - break; - } - case "rotate": { - cornerstoneTools.setToolActive("Rotate", { mouseButtonMask: 1 }); - break; - } - case "scroll": { - cornerstoneTools.setToolActive("StackScroll", { mouseButtonMask: 1 }); - // _this.left.onstart = _this.scroll_start; - // _this.selected_control = t.selectedIndex; - break; - } - case "window": { - cornerstoneTools.setToolActive("Wwwc", { mouseButtonMask: 1 }); - break; - } - case "abdomen": { - const viewport = cornerstone.getViewport(dicom_element); - viewport.voi.windowCenter = 150; - viewport.voi.windowWidth = 500; - cornerstone.setViewport(dicom_element, viewport); - sel.selectedIndex = viewer.find_option(sel, "window"); - changeControlSelection(); - break; - } - case "pulmonary": { - const viewport = cornerstone.getViewport(dicom_element); - viewport.voi.windowCenter = -500; - viewport.voi.windowWidth = 1500; - cornerstone.setViewport(dicom_element, viewport); - sel.selectedIndex = viewer.find_option(sel, "window"); - changeControlSelection(); - break; - } - case "brain": { - const viewport = cornerstone.getViewport(dicom_element); - viewport.voi.windowCenter = 50; - viewport.voi.windowWidth = 80; - cornerstone.setViewport(dicom_element, viewport); - sel.selectedIndex = viewer.find_option(sel, "window"); - changeControlSelection(); - break; - } - case "bone": { - const viewport = cornerstone.getViewport(dicom_element); - viewport.voi.windowCenter = 570; - viewport.voi.windowWidth = 3000; - cornerstone.setViewport(dicom_element, viewport); - sel.selectedIndex = viewer.find_option(sel, "window"); - changeControlSelection(); - break; - } - case "reset": { - cornerstone.reset(dicom_element); - sel.selectedIndex = old; - sel.oldSelectedIndex = old; - break; - } - case "close": { - // disable fullscreen if required - if ($(".canvas-panel").length == 0) { - viewer.disableFullscreen(dicom_element); - } - if (dicom_element != undefined) { - cornerstone.removeElementData(dicom_element); - cornerstone.disable(dicom_element); - $(".canvas-panel").remove(); - $(".figure-open").removeClass("figure-open").addClass("figure"); - $(dicom_element).remove(); - } - break; - } - } - sel.blur(); - if (event) { - event.preventDefault(); - } -} - // Register Key Event Listener window.addEventListener("keydown", keydown_handler); @@ -1255,6 +1162,10 @@ function reviewQuestions() { $("#review-overlay").show(); $("#review-answer-list").empty(); + if (window.question_type == "long") { + $("#review-overlay").append("
"); + } + loadQuestion(0, 0, true); db.answers @@ -1272,7 +1183,67 @@ function reviewQuestions() { }); let correct_count = 0; + let not_answered = 0; + let answered_normal = 0; + let answered_abnormal = 0; + let undercall_number = 0; + let overcall_number = 0; + let incorrectcall_number = 0; window.question_order.forEach(function (qid, n) { + if (window.question_type == "long") { + $("#review-answer-table").append( + $( + `Question ${n + 1}` + ).click(() => { + loadQuestion(n); + $("#review-overlay").hide(); + }) + ); + // $("#review-answer-table").append(``); + $("#review-answer-table").append( + `Your answersModel answers` + ); + + const model_answers = window.questions[qid].answers; + + const titles = [ + "Observations", + "Interpretation", + "Principal Diagnosis", + "Differential Diagnosis", + "Management", + ]; + + // let user_td = $( + // `#review-answer-${qid} td .review-user-answer-cell` + // ); + // let model_td = $( + // `#review-answer-${qid} td .review-model-answer-cell` + // ); + + titles.forEach((title, n) => { + let user_answer = current_answers[`${qid},${n}`]; + if (user_answer == undefined) { + user_answer = "Not answered"; + } + let user_ans = + "

" + title + "

" + user_answer; + let model_ans = + `

` + + title + + "

" + + model_answers[0][title.toLowerCase()]; + + $("#review-answer-table").append( + `${user_ans}${model_ans}` + ); + }); + + return; + } + $("#review-answer-list").append( "
  • " - ); - - const model_answers = window.questions[qid].answers; - - const titles = [ - "Observations", - "Interpretation", - "Principal Diagnosis", - "Differential Diagnosis", - "Management", - ]; - - let user_li = $( - `#review-answer-${qid} span .user-review-answer-list` - ); - let model_li = $( - `#review-answer-${qid} span .model-review-answer-list` - ); - - user_li.append("
    Your answers
    "); - model_li.append("
    Model answers
    "); - - titles.forEach((title, n) => { - let user_answer = current_answers[`${qid},${n}`]; - if (user_answer == undefined) { - user_answer = "Not answered"; - } - user_li.append( - "

    " + title + "

    " + user_answer - ); - model_li.append( - `

    ` + - title + - "

    " + - model_answers[0][title.toLowerCase()] - ); - }); - - return; - } db.user_answers .get({ qid: qid }) .then(function (answers) { + // Merge the question answers with the user saved answers let question_answers = window.questions[qid]["answers"]; if (answers != undefined) { question_answers = question_answers.concat(answers.ans); @@ -1346,13 +1272,17 @@ function reviewQuestions() { if (section_1_answer == undefined) { section_1_answer = "Not Answered"; + not_answered++; } if (section_2_answer == undefined) { if (section_1_answer == "Normal") { section_2_answer = "Normal"; + answered_normal++; } else { section_2_answer = "Not Answered"; } + } else { + answered_abnormal++; } let el = $("#review-answer-" + qid + " span"); @@ -1361,7 +1291,7 @@ function reviewQuestions() { * Helper function to define how review items are displayed * Yes it is a bit shit * @param {*} el - element - * @param {*} c - clans + * @param {*} c - class * @param {*} user_answer - * @param {*} normal - * @param {*} question_answers - @@ -1407,6 +1337,9 @@ function reviewQuestions() { ); correct_count++; } else { + if (section_1_answer != "Not Answered") { + overcall_number++; + } setReviewAnswer( el, "incorrect", @@ -1433,6 +1366,22 @@ function reviewQuestions() { false, question_answers ); + + 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); + reviewQuestions(); + }); + } } } } else if (window.question_type == "anatomy") { @@ -1463,6 +1412,18 @@ function reviewQuestions() { " out of " + window.question_order.length ); + + if (window.question_type == "rapid") { + $("#exam-stats").show(); + $("#unanswered-number").html(not_answered); + $("#normal-number").html(answered_normal); + $("#abnormal-number").html(answered_abnormal); + $("#undercall-number").html(undercall_number); + $("#overcall-number").html(overcall_number); + $("#incorrectcall-number").html(incorrectcall_number); + } else { + $("#exam-stats").show(); + } }) .catch(function (error) { console.log("error-", error); @@ -1489,6 +1450,7 @@ function markAnswer(qid, current_question) { if (type == "rapid") { option = document.getElementById("rapid-option"); + // If the current question is normal if (current_question.normal == true) { $(".answer-panel").append( "
    This is normal
    " @@ -1524,18 +1486,21 @@ function markAnswer(qid, current_question) { let user_answer = textarea.val(); + // Load user saved answers db.user_answers .get({ qid: qid }) - .then(function (answers) { - if (answers != undefined) { - answers.ans.forEach(function (answer, n) { - ul.append("
  • " + answer + "
  • "); - if (compareString(answer, user_answer)) { + .then(function (saved_user_answers) { + // check if given answer matches a user saved answer + if (saved_user_answers != undefined) { + saved_user_answers.ans.forEach(function (saved_answer, n) { + ul.append("
  • " + saved_answer + "
  • "); + if (compareString(saved_answer, user_answer)) { textarea.removeClass("incorrect").addClass("correct"); } }); } + // check if given answer matches an answer from the question current_question.answers.forEach(function (answer, n) { ul.append("
  • " + answer + "
  • "); if (compareString(answer, user_answer)) { @@ -1543,13 +1508,17 @@ function markAnswer(qid, current_question) { } }); + // If the given answer is not abnormal it must be incorrect if (type == "rapid" && option.value != "Abnormal") { option.classList.add("incorrect"); } else { + // If it is blank it must also be incorrect if ( textarea.hasClass("incorrect") == true && user_answer.replace(/\s/g, "") != "" ) { + // Otherwise there is a chance that it may be correct + // so we give the option to mark it correct if (window.allow_self_marking) { $(".answer-panel").append( $("").click( @@ -1682,6 +1651,11 @@ $("#start-exam-button").click(function (evt) { }); $("#start-packet-button").click(function (evt) { + if (window.timer != null) { + window.timer.stop(); + } + window.timer = null; + let timer = new easytimer.Timer(); //window.exam_time = 2; timer.start({ countdown: true, startValues: { seconds: window.exam_time } }); @@ -1692,6 +1666,8 @@ $("#start-packet-button").click(function (evt) { $("#time-up-dialog").modal(); }); + window.timer = timer; + $("#timer").click(() => { timer.pause(); $("#pause").empty(); @@ -1731,7 +1707,9 @@ $("#start-packet-button").click(function (evt) { }); $("#btn-delete-databases").click(function (evt) { - var r = confirm("This will delete ALL saved answers!"); + var r = confirm( + "This will delete ALL saved answers (including saved correct answers)!" + ); if (r == true) { db.delete() .then(() => { @@ -1754,22 +1732,59 @@ $("#btn-delete-databases").click(function (evt) { }); $("#btn-delete-current").click(function (evt) { + if (window.question_order.length < 1) { + $.notify("No packet is currently loaded", "warn"); + return; + } + + db.flags + .where("qid") + .anyOf(window.question_order) + .delete() + .then(function (deleteCount) { + $.notify("Packet flags deleted", "success"); + }) + .catch((err) => { + $.notify("Error deleting packet flags", "error"); + $.notify("You may have to delete all answers this time", "info"); + }); + db.answers .where("qid") .anyOf(window.question_order) .delete() .then(function (deleteCount) { $.notify("Packet successfully reset", "success"); - $.notify("The page will now reload", "warn"); - setTimeout(() => { - location.reload(); - }, 2000); + loadQuestion(0, 0, true); + // $.notify("The page will now reload", "warn"); + // setTimeout(() => { + // location.reload(); + // }, 2000); console.log("Deleted " + deleteCount + " objects"); }) .catch((err) => { $.notify("Error reseting packet", "error"); $.notify("You may have to delete all answers this time", "info"); - console.error("Could not answers"); + console.log(err); + }); +}); + +$("#btn-delete-current-saved-answers").click(function (evt) { + if (window.question_order.length < 1) { + $.notify("No packet is currently loaded", "warn"); + return; + } + + db.user_answers + .where("qid") + .anyOf(window.question_order) + .delete() + .then(function (deleteCount) { + $.notify("Packet flags deleted", "success"); + }) + .catch((err) => { + $.notify("Error deleting saved answers", "error"); + $.notify("You may have to delete all answers this time", "info"); }); }); diff --git a/js/viewer.js b/js/viewer.js index 0bf47f8..2cd5e41 100644 --- a/js/viewer.js +++ b/js/viewer.js @@ -229,3 +229,102 @@ export function debugCornerstone() { let viewport = cornerstone.getViewport(dicom_element); let c = cornerstone.getEnabledElement(dicom_element); } + +/** + * Registers the selected dicom tool + */ +export function changeControlSelection() { + // We also duplicate the sel that are available + const sel = $(".control-overlay").get(0); + + const old = sel.oldSelectedIndex; + sel.oldSelectedIndex = sel.selectedIndex; + + console.log(event); + + const dicom_element = document.getElementById("dicom-image"); + switch (sel.options[sel.selectedIndex].value) { + case "pan": { + cornerstoneTools.setToolActive("Pan", { mouseButtonMask: 1 }); + break; + } + case "zoom": { + cornerstoneTools.setToolActive("Zoom", { mouseButtonMask: 1 }); + break; + } + case "rotate": { + cornerstoneTools.setToolActive("Rotate", { mouseButtonMask: 1 }); + break; + } + case "scroll": { + cornerstoneTools.setToolActive("StackScroll", { mouseButtonMask: 1 }); + // _this.left.onstart = _this.scroll_start; + // _this.selected_control = t.selectedIndex; + break; + } + case "window": { + cornerstoneTools.setToolActive("Wwwc", { mouseButtonMask: 1 }); + break; + } + case "abdomen": { + const viewport = cornerstone.getViewport(dicom_element); + viewport.voi.windowCenter = 150; + viewport.voi.windowWidth = 500; + cornerstone.setViewport(dicom_element, viewport); + sel.selectedIndex = find_option(sel, "window"); + changeControlSelection(); + break; + } + case "pulmonary": { + const viewport = cornerstone.getViewport(dicom_element); + viewport.voi.windowCenter = -500; + viewport.voi.windowWidth = 1500; + cornerstone.setViewport(dicom_element, viewport); + sel.selectedIndex = find_option(sel, "window"); + changeControlSelection(); + break; + } + case "brain": { + const viewport = cornerstone.getViewport(dicom_element); + viewport.voi.windowCenter = 50; + viewport.voi.windowWidth = 80; + cornerstone.setViewport(dicom_element, viewport); + sel.selectedIndex = find_option(sel, "window"); + changeControlSelection(); + break; + } + case "bone": { + const viewport = cornerstone.getViewport(dicom_element); + viewport.voi.windowCenter = 570; + viewport.voi.windowWidth = 3000; + cornerstone.setViewport(dicom_element, viewport); + sel.selectedIndex = find_option(sel, "window"); + changeControlSelection(); + break; + } + case "reset": { + cornerstone.reset(dicom_element); + sel.selectedIndex = old; + sel.oldSelectedIndex = old; + break; + } + case "close": { + // disable fullscreen if required + if ($(".canvas-panel").length == 0) { + disableFullscreen(dicom_element); + } + if (dicom_element != undefined) { + cornerstone.removeElementData(dicom_element); + cornerstone.disable(dicom_element); + $(".canvas-panel").remove(); + $(".figure-open").removeClass("figure-open").addClass("figure"); + $(dicom_element).remove(); + } + break; + } + } + sel.blur(); + if (event) { + event.preventDefault(); + } +}