import * as helper from "./helpers.js"; // cid = null; window.cid = 1234; window.eid = 5678; window.exam_mode = false; window.packet_list = []; window.questions = null; window.question_order = []; window.number_of_questions = null; window.question_type = null; window.loaded_question = null; window.review = false; window.allow_self_marking = true; window.dfile = null; retrievePacketList(); /** * Retrieves a list of available packets via JSON ajax requests * Will initially try /packets/ (for example if index.php generates the list) * and then fallback to packets.json */ function retrievePacketList() { $.getJSON("packets/", function (data) { loadPacketList(data); }) .done(function () {}) .fail(function () { $.getJSON("packets/packets.json", function (data) { loadPacketList(data); }).fail(function () { console.log("No packet list available"); showLoginDialog(); }); }) .always(function () {}); // setUpPacket(questions); } /** * Generate a list of the packets that are available to be loaded * * @param {JSON} data - json containing available packets */ function loadPacketList(data) { window.packet_list = data.packets; window.packet_list.forEach(function (packet) { $("#packet-list").append( $("
" + packet + "
").click(function () { loadPacketFromAjax("packets/" + packet); }) ); }); $("#options-panel").show(); } /** * Loads the requisite packet into the quiz system * @param {string} path - relative path to the packet json file */ function loadPacketFromAjax(path) { console.log("loading packet from " + path); $.getJSON(path, function (data) { setUpPacket(data); $("#options-panel").hide(); }) .done(function () {}) .fail(function () { console.log("Unable to load packet at: " + path); }); } /** * Build the currently loaded quiz */ function setUpQuestions() { if (window.questions == undefined) { return; } // Set an order for the questions window.question_order = []; Object.keys(window.questions).forEach(function (e) { window.question_order.push(e); // Make sure answers are arrays if (!Array.isArray(window.questions[e]["answers"])) { window.questions[e]["answers"] = [window.questions[e]["answers"]]; } }); 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; } if (window.exam_mode) { $("#options-button, #review-overlay-button").hide(); } else { $("#submit-button").hide(); } loadQuestion(0, 1, true); createQuestionListPanel(); } // Set up cornerstone (the dicom viewer) cornerstoneBase64ImageLoader.external.cornerstone = cornerstone; cornerstoneWebImageLoader.external.cornerstone = cornerstone; // cornerstoneWebImageLoader.configure({ // beforeSend: function(xhr) { // // Add custom headers here (e.g. auth tokens) // //xhr.setRequestHeader('x-auth-token', 'my auth token'); // } // }); cornerstoneWADOImageLoader.external.cornerstone = cornerstone; // cornerstoneWADOImageLoader.configure({ // beforeSend: function(xhr) { // // Add custom headers here (e.g. auth tokens) // //xhr.setRequestHeader('APIKEY', 'my auth token'); // } // }); cornerstoneTools.init(); // Set up database const db = new Dexie("answers_database"); db.version(1).stores({ answers: "[cid+eid+qid+qidn], [cid+eid], ans", flags: "[cid+eid+qid+qidn], [cid+eid]", user_answers: "qid, ans", }); /** * Parse the packet and extract metadata (if it exists) * Will then call setUpQuestions() to load the packet * @param {JSON} data */ function setUpPacket(data) { if (data.hasOwnProperty("exam_name")) { $(".exam-name").text("Exam: " + data.exam_name); } if (data.hasOwnProperty("eid")) { window.eid = data.eid; } if (data.hasOwnProperty("type")) { window.question_type = data.type; } if (data.hasOwnProperty("exam_mode")) { window.exam_mode = data.exam_mode; } if (data.hasOwnProperty("questions")) { window.questions = data.questions; } else { window.questions = data; } setUpQuestions(); } /** * Loads a specific question * @param {number} n - Question number to load * @param {number} section - Question section to focus * @param {boolean} force_reload - Force question to be reloaded if already loaded */ function loadQuestion(n, section = 1, force_reload = false) { // Make sure we have an integer n = parseInt(n); console.log(n); const qid = window.question_order[n]; console.log("qid", qid); const current_question = window.questions[qid]; console.log("N=", n, section, current_question); if (n == window.loaded_question && force_reload == false) { // Question already loaded setFocus(section); return; } window.loaded_question = n; // Set up question navigation $(".navigation[value='next']").off(); $(".navigation[value='previous']").off(); if (n == 0) { $(".navigation[value='previous']").attr("disabled", "disabled"); } else { $(".navigation[value='previous']") .removeAttr("disabled") .click(function () { loadQuestion(n - 1); }); } if (n == window.number_of_questions - 1) { $(".navigation[value='next']").attr("disabled", "disabled"); } else { $(".navigation[value='next']") .removeAttr("disabled") .click(function () { loadQuestion(n + 1); }); } const display_n = n + 1; if (current_question.title) { $(".question .title").get(0).innerHTML = '' + display_n + " " + current_question.title; } else { $(".question .title").get(0).innerHTML = '' + display_n + ""; } // Close any open figures let el = document.getElementById("dicom-image"); if (el != undefined) { cornerstone.disable(el); $(".canvas-panel").remove(); } // Set up thumbnails const thumbnails = $(".thumbs"); // Why are we checking this? if (thumbnails) { thumbnails.empty(); // TODO: figure captions (need to extend base json) current_question.images.forEach(function createThumbnail(image, id) { // For thumbnails we only want a single image if (Array.isArray(image)) { image = image[0]; // Do we want the middle image? } thumbnails.append( '
...
' ); // const thumbnail = $(".figure .thumbnail").get(id); // cornerstone.enable(thumbnail) // based_img = current_question.images[id]; const based_img = image; // based (image) data url, just load the image directly if (based_img.startsWith("data:image/")) { const img = $("", { src: based_img, id: "thumb-" + id, class: "thumbnail", title: "Click on the thumbnail to view and manipulate the image.", draggable: "false", style: "height: 100px;", }); $("#figure-" + id).append(img); // otherwise try to load it as a dicom } else { // convert the data url to a file urltoFile(based_img, "dicom", "application/dicom").then(function ( dfile ) { // load the file using cornerstoneWADO file loader const imageId = cornerstoneWADOImageLoader.wadouri.fileManager.add( dfile ); cornerstone.loadAndCacheImage(imageId).then(function (image) { const img = $("
").get(0); img.id = "thumb-" + id; img.class = "thumbnail"; img.title = "Click on the thumbnail to view and manipulate the image."; img.draggable = "false"; img.style = "height: 100px; width: 100px"; $("#figure-" + id).append(img); const element = document.getElementById("thumb-" + id); cornerstone.enable(element); cornerstone.displayImage(element, image); cornerstone.resize(element); }); // .catch( function(error) { }); } }); } $(".thumbs .figure").each(function handleThumbnailClicks(id, thumbnail) { thumbnail.onclick = function (event) { view(event, this); }; }); /** * View cornstone image * @param {*} t * @param {*} source */ function view(t, source) { if (current_question) { // Check if the figure is already loaded (or if another one is) const open_figure = $("#dicom-image").data("figure"); const figure_to_load = source.id; if (open_figure != undefined) { // No full size figure / dicom loaded yet if (figure_to_load == open_figure) { return; } else { el = document.getElementById("dicom-image"); if (el != undefined) { cornerstone.disable(el); $(".canvas-panel").remove(); } } } $(".figure-open").removeClass("figure-open").addClass("figure"); source.className = "figure-open"; $(".question").append( '
' ); let images = current_question.images[figure_to_load.split("-")[1]]; // images = current_question.images // Make sure we have an array if (!Array.isArray(images)) { images = [images]; } load(images); /** * Load image * @param {*} images - list of images */ async function load(images) { const imageIds = []; for (let i = 0; i < images.length; i++) { const data_url = images[i]; // check stack type if (data_url.startsWith("data:image")) { const imageId = "base64://" + data_url.split(",")[1]; imageIds.push(imageId); } else if (data_url.startsWith("data:application/dicom")) { // stack = stack.split(";")[1]; dfile = await urltoFile(data_url, "dicom", "application/dicom"); const imageId = cornerstoneWADOImageLoader.wadouri.fileManager.add( dfile ); imageIds.push(imageId); // cornerstone.loadImage(imageId).then(function(image) { // tempFunction(image); // }); } } const stack = { currentImageIdIndex: 0, imageIds, }; cornerstone.loadAndCacheImage(imageIds[0]).then(function (image) { loadMainImage(image, stack); }); } } } // Answer setup const ap = $(".answer-panel"); ap.empty(); switch (current_question.type) { case "rapid": { 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"); } db.answers.put({ cid, eid: eid, qid: qid, qidn: "1", ans: evt.target.value, }); updateQuestionListPanel(); }); // 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([cid, eid, qid, "2"]); $( "#question-list-item-" + window.question_order.indexOf(qid) + "-2" ).removeClass("question-saved-localdb"); return; } // db.answers.put({aid: [cid, eid, qidn], ans: evt.target.value}); db.answers.put({ cid: cid, eid: eid, qid: qid, qidn: "2", ans: evt.target.value, }); $( "#question-list-item-" + window.question_order.indexOf(qid) + "-2" ).addClass("question-saved-localdb"); updateQuestionListPanel(); }); // We chain our db requests as we can only check answers once // they have been loaded (should probably use then or after) db.answers .get({ cid: cid, eid: eid, qid: qid, 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.log("DB", cid, eid, qid); console.log("error-", error); }) .then( db.answers .get({ cid: cid, eid: eid, qid: qid, qidn: "2" }) .then(function (answer) { if (answer != undefined) { console.log(answer); $(".long-answer").text(answer.ans); } markAnswer(qid, "rapid"); }) .catch(function (error) { console.log("error-", error); }) ); addFlagEvents(); loadFlagsFromDb(qid, "1"); loadFlagsFromDb(qid, "2"); break; } case "anatomy": { ap.append( '

' + display_n + '.1' + current_question.question + '

' ); $(".long-answer").change(function (evt) { // ignore blank text and delete any stored value if (evt.target.value.length < 1) { db.answers.delete([cid, eid, qid, "1"]); $( "#question-list-item-" + window.question_order.indexOf(qid) + "-1" ).removeClass("question-saved-localdb"); return; } // db.answers.put({aid: [cid, eid, qidn], ans: evt.target.value}); db.answers.put({ cid: cid, eid: eid, qid: qid, qidn: "1", ans: evt.target.value, }); $( "#question-list-item-" + window.question_order.indexOf(qid) + "-1" ).addClass("question-saved-localdb"); updateQuestionListPanel(); }); db.answers .get({ cid: cid, eid: eid, qid: qid, qidn: "1" }) .then(function (answer) { if (answer != undefined) { $(".long-answer").text(answer.ans); } markAnswer(qid, "anatomy"); }) .catch(function (error) { console.log(error); }); addFlagEvents(); loadFlagsFromDb(qid, "1"); break; } case "long": { ap.append( '

' + display_n + '.1Observations

' ); ap.append( '

' + display_n + '.2Interpretation

' ); ap.append( '

' + display_n + '.3Principle Diagnosis

' ); ap.append( '

' + display_n + '.4Differential Diagnosis

' ); ap.append( '

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

' ); // Save long answers on textchange $(".long-answer").change(function (evt) { const qidn = evt.target.dataset.qidn.toString(); // ignore blank text and delete any stored value if (evt.target.value.length < 1) { db.answers.delete([cid, eid, qid, qidn]); $( "#question-list-item-" + window.question_order.indexOf(qid) + "-" + qidn ).removeClass("question-saved-localdb"); return; } // db.answers.put({aid: [cid, eid, qidn], ans: evt.target.value}); db.answers.put({ cid: cid, eid: eid, qid: qid, qidn: qidn, ans: evt.target.value, }); console.log("SAVE", qid, qidn, evt.target.value); $( "#question-list-item-" + window.question_order.indexOf(qid) + "-" + qidn ).addClass("question-saved-localdb"); updateQuestionListPanel(); }); // Loop through the 5 text areas and retrieve from db // could all be retrieved at once with an addition key index // for (let qidn_count = 1; qidn_count < 6; qidn_count++) { db.answers .get({ cid: cid, eid: eid, qid: qid, qidn: qidn_count.toString() }) .then(function (answer) { if (answer != undefined) { $(".long-answer") .eq(parseInt(answer.qidn) - 1) .text(answer.ans); } // markAnswer(qid, "anatomy"); }) .catch(function (error) { console.log(error); }); } addFlagEvents(); loadFlagsFromDb(qid, "1"); loadFlagsFromDb(qid, "2"); loadFlagsFromDb(qid, "3"); loadFlagsFromDb(qid, "4"); loadFlagsFromDb(qid, "5"); break; } } updateQuestionListPanel(); setFocus(section); } /** * Sets the focus to the required question answer section * @param {number} section - section to focus */ 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(); }, 200); } /** * Updates the question list panel * Test with just a global update (may need to do these individually * if it gets slow...) */ function updateQuestionListPanel() { console.log("UP"); // Reset all classes db.answers .where({ cid: cid, eid: eid }) .toArray() .then(function (answers) { $(".question-list-panel div") .slice(1) .attr("class", "question-list-item"); answers.forEach(function (answer, n) { $( "#question-list-item-" + window.question_order.indexOf(answer.qid) + "-" + answer.qidn ).addClass("question-saved-localdb"); if (window.question_type == "rapid" && answer.qidn == "1") { if (answer.ans == "Abnormal") { $( "#question-list-item-" + window.question_order.indexOf(answer.qid) + "-2" ).css("display", "block"); } else { $( "#question-list-item-" + window.question_order.indexOf(answer.qid) + "-2" ).css("display", "none"); } } }); // $(".long-answer").text(answer.ans); }) .catch(function (error) { console.log("error - ", error); }); db.flags .where({ cid: cid, eid: eid }) .toArray() .then(function (answers) { answers.forEach(function (answer, n) { $( "#question-list-item-" + window.question_order.indexOf(answer.qid) + "-" + answer.qidn + " span" ).css("visibility", "visible"); }); // $(".long-answer").text(answer.ans); }) .catch(function (error) { console.log("error - ", error); }); } /** * Builds the QuestionListPanel */ function createQuestionListPanel() { $(".question-list-panel").empty(); $(".question-list-panel").append( '
Questions
' ); /** * Appends QuestionList item * @param {number} n - question number * @param {number} qidn - question section number * @return {*} el - element that has been created */ function appendReviewItem(n, qidn) { const qn = n - 1; const el = $( '
' + n + "." + qidn + '
' ); $(".question-list-panel").append(el); // return the new element so it can be hidden if necessary return el; } if (window.question_type == "rapid") { // Loop through all questions and append list items for (let n = 1; n < window.number_of_questions + 1; n++) { appendReviewItem(n, "1"); appendReviewItem(n, "2").hide(); } } else if (window.question_type == "anatomy") { for (let n = 1; n < window.number_of_questions + 1; n++) { appendReviewItem(n, "1"); } } else if (window.question_type == "long") { for (let n = 1; n < window.number_of_questions + 1; n++) { appendReviewItem(n, "1"); appendReviewItem(n, "2"); appendReviewItem(n, "3"); appendReviewItem(n, "4"); appendReviewItem(n, "5"); } } $(".question-list-item").click(function (evt) { loadQuestion($(this).attr("data-qid"), $(this).attr("data-qidn")); }); } /** * Finds a select options but its text value * @param {*} select - select element * @param {*} text - text value * @return {number} - number of found option or 'undefined' */ function find_option(select, text) { for (let i = 0; i < select.length; ++i) { if (select.options[i].value == text) { return i; } } return undefined; } /** * Handle key button events * These are the same as in the RCR pratique implementation * @param {*} event - KeyEvent */ function keydown_handler(event) { const target_element = event.target.tagName; // Catch all keypresses unless user is typing if (target_element == "INPUT" || target_element == "TEXTAREA") { return; } const sel = $(".control-overlay").get(0); switch (event.code) { case "KeyP": sel.selectedIndex = find_option(sel, "pan"); changeControlSelection(); break; case "KeyZ": sel.selectedIndex = find_option(sel, "zoom"); changeControlSelection(); break; case "KeyR": sel.selectedIndex = find_option(sel, "rotate"); changeControlSelection(); break; case "KeyS": sel.selectedIndex = find_option(sel, "scroll"); changeControlSelection(); break; case "KeyW": sel.selectedIndex = find_option(sel, "window"); changeControlSelection(); break; case "KeyA": sel.selectedIndex = find_option(sel, "abdomen"); changeControlSelection(); break; case "KeyU": sel.selectedIndex = find_option(sel, "pulmonary"); changeControlSelection(); break; case "KeyB": sel.selectedIndex = find_option(sel, "brain"); changeControlSelection(); break; case "KeyO": sel.selectedIndex = find_option(sel, "bone"); changeControlSelection(); break; case "KeyE": sel.selectedIndex = find_option(sel, "reset"); // $("#dicom-image").get(0).scrollIntoView(false); changeControlSelection(); break; // Escape and C do the same case "KeyC": case "Escape": sel.selectedIndex = find_option(sel, "close"); changeControlSelection(); break; case "Enter": t.focus(); break; // TODO: implement arrow stuff // case "ArrowUp": { // Arrow function depends on the currently selected option switch (sel.options[sel.selectedIndex].value) { case "pan": manualPanDicom(0, -1); break; case "zoom": // _this.current_dicom.current_view.pre_scale_at(1.01, 1.01, _this.current_dicom.cols / 2, _this.current_dicom.rows / 2); manualZoomDicom(1); break; case "rotate": manualRotateDicom(-1); break; case "scroll": manualScrollDicom(-1); break; case "window": manualWindowDicom(1, 0); break; } break; } case "ArrowDown": { switch (sel.options[sel.selectedIndex].value) { case "pan": manualPanDicom(0, 1); break; case "zoom": manualZoomDicom(-1); break; case "rotate": manualRotateDicom(1); break; case "scroll": manualScrollDicom(1); break; case "window": manualWindowDicom(-1, 0); break; } break; } case "ArrowLeft": { switch (sel.options[sel.selectedIndex].value) { case "pan": manualPanDicom(-1, 0); break; case "zoom": manualZoomDicom(-1); break; case "rotate": manualRotateDicom(1); break; case "scroll": manualScrollDicom(1); break; case "window": manualWindowDicom(0, -1); break; } break; } case "ArrowRight": { switch (sel.options[sel.selectedIndex].value) { case "pan": manualPanDicom(1, 0); break; case "zoom": manualZoomDicom(1); break; case "rotate": manualRotateDicom(-1); break; case "scroll": manualScrollDicom(-1); break; case "window": manualWindowDicom(0, 1); break; } break; } } 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 = 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(); } } /** * Called when a dicom image is rendered * @param {*} e - Event */ function onImageRendered(e) { const eventData = e.detail; // Update ww/wl const sel = $(".control-overlay").get(0); sel.options[find_option(sel, "window")].firstChild.textContent = "window (" + Math.round(eventData.viewport.voi.windowCenter) + " \u00b1 " + Math.round(eventData.viewport.voi.windowWidth) + ")"; // update stack data const stack = eventData.enabledElement.toolStateManager.toolState.stack.data[0]; if (stack.imageIds.length > 1) { $("option[value=scroll").prop("disabled", false); $("option[value=scroll").prop("hidden", false); $("option[value=scroll").text( "scroll (" + (stack.currentImageIdIndex + 1) + "/" + stack.imageIds.length + ")" ); // Temp way to enable CT window presets // Enable for all stacked images... $("option[value=abdomen").prop("hidden", false); $("option[value=pulmonary").prop("hidden", false); $("option[value=brain").prop("hidden", false); $("option[value=bone").prop("hidden", false); $("option[value=abdomen").prop("disabled", false); $("option[value=pulmonary").prop("disabled", false); $("option[value=brain").prop("disabled", false); $("option[value=bone").prop("disabled", false); } } /** * Disables fullscreen mode * @param {*} dicom_element - */ function disableFullscreen(dicom_element) { // TODO: rescale the image to stop it getting zoomed in $(".canvas-panel-fullscreen").toggleClass( "canvas-panel canvas-panel-fullscreen" ); $(".question").append($(".canvas-panel")); $(".canvas-panel").get(0).scrollIntoView(); setDicomCanvasNonFullscreen(dicom_element); } // Register Key Event Listener window.addEventListener("keydown", keydown_handler); $("#submit-button").click(function (evt) { submitAnswers(); }); $("#review-button").click(function (evt) { $(".question-list-panel").toggle(); }); $("#options-button").click(function (evt) { $("#options-panel").toggle(); }); $("#review-overlay-button").click(function (evt) { if (window.review == true) { reviewQuestions(); } else { $("#finish-dialog").modal(); } }); $("#finish-exam").click(function (evt) { window.review = true; reviewQuestions(); $.modal.close(); }); $("#finish-cancel").click(function (evt) { $.modal.close(); }); $("#overlay-close").click(function (evt) { $("#options-panel").hide(); }); $("#review-overlay-close").click(function (evt) { $("#review-overlay").hide(); }); /** * Gets a json representation of answers * @return {JSON} - answers */ function getJsonAnswers() { return db.answers.where({ cid: cid, eid: eid }).toArray(); // .then(function(ans) { // console.log(ans); // submitAnswers(ans); // }); } /** * Posts answers to url * @param {*} ans - json representation of answers */ function postAnswers(ans) { console.log(ans); // ans = {"test" : 1} $.post("http://localhost:8000/submit_answers", JSON.stringify(ans)).done( (data) => { console.log(data); } ); // $.post( "http://localhost:8000/submit_answers", JSON.stringify(ans)); } /** * Loads a local question set (from a file) * This is defined in index.html */ function loadLocalQuestionSet() { let input; let file; let fr; if (typeof window.FileReader !== "function") { alert("The file API isn't supported on this browser yet."); return; } input = document.getElementById("fileinput"); if (!input) { alert("No fileinput element."); } else if (!input.files) { alert( "This browser doesn't seem to support the `files` property of file inputs." ); } else if (!input.files[0]) { alert("Please select a file before clicking 'Load'"); } else { file = input.files[0]; fr = new FileReader(); fr.onload = receivedText; fr.readAsText(file); } /** * Reads text from event and loads the packet * @param {*} e - event */ function receivedText(e) { const lines = e.target.result; j = JSON.parse(lines); setUpPacket(j); } } /** * Submits answers */ function submitAnswers() { console.log( getJsonAnswers().then((a) => { postAnswers(a); }) ); } /** * Displays the review question panel with a summary of marks */ function reviewQuestions() { $("#review-overlay").show(); $("#review-answer-list").empty(); loadQuestion(0); db.answers .where({ cid: cid, eid: eid }) .toArray() .then(function (answers) { let current_answers = {}; answers.forEach(function (arr, n) { answer = arr["ans"]; if (answer == undefined) { answer = "Not answered"; } current_answers[[arr["qid"], arr["qidn"]]] = answer; }); correct_count = 0; window.question_order.forEach(function (qid, n) { $("#review-answer-list").append( "
  • Question " + (n + 1) + ": Not answered
  • " ); $("#review-answer-list a").click(function (evt) { loadQuestion(this.dataset.qid); $("#review-overlay").hide(); }); db.user_answers .get({ qid: qid }) .then(function (answers) { let question_answers = window.questions[qid]["answers"]; if (answers != undefined) { question_answers = question_answers.concat(answers.ans); } let section_1_answer = current_answers[[qid, "1"]]; let section_2_answer = current_answers[[qid, "2"]]; if (section_1_answer == undefined) { section_1_answer = "Not Answered"; } if (section_2_answer == undefined) { if (section_1_answer == "Normal") { section_2_answer = "Normal"; } else { section_2_answer = "Not Answered"; } } el = $("#review-answer-" + qid + " span"); /** * Helper function to define how review items are displayed * Yes it is a bit shit * @param {*} el - element * @param {*} c - clans * @param {*} user_answer - * @param {*} normal - * @param {*} question_answers - */ function setReviewAnswer( el, c, user_answer, normal, question_answers ) { if (normal) { el.html( "Answer: " + user_answer + " (Normal)" ); } else { el.html( "Answer: " + user_answer + " (Abnormal: " + question_answers.join(", ") + ")" ); } } if (window.question_type == "rapid") { // First check normal vs abnormal if (window.questions[qid]["normal"] == true) { if (section_1_answer == "Normal") { setReviewAnswer( el, "correct", section_1_answer, true, question_answers ); correct_count++; } else { setReviewAnswer( el, "incorrect", section_1_answer, true, question_answers ); } } else { if (answerInArray(question_answers, section_2_answer)) { correct_count++; setReviewAnswer( el, "correct", section_2_answer, false, question_answers ); } else { setReviewAnswer( el, "incorrect", section_2_answer, false, question_answers ); } } } else if (window.question_type == "anatomy") { // Anatomy answers are either correct or incorrect if (answerInArray(question_answers, section_1_answer)) { correct_count++; setReviewAnswer( el, "correct", section_1_answer, false, question_answers ); } else { setReviewAnswer( el, "incorrect", section_1_answer, false, question_answers ); } } $("#review-score").text( "Score: " + correct_count + " out of " + window.question_order.length ); }) .catch(function (error) { console.log("error-", error); }); }); }) .catch(function (error) { console.log("error - ", error); }); } /** * Marks the loaded question and updates display * @param {*} qid - * @param {*} type - */ function markAnswer(qid, type) { console.log("mark", qid); if (window.review == true) { // Disable all possible answer elements $("#rapid-option,.long-answer").attr("disabled", "true"); if (type == "rapid") { option = document.getElementById("rapid-option"); if (current_question.normal == true) { $(".answer-panel").append( "
    This is normal
    " ); if (option.value == "Normal") { option.classList.add("correct"); } else { option.classList.add("incorrect"); } // If answer is normal we have nothing else to add. addFeedback(); } } $(".answer-panel").append( "
    Correct answer(s):
    " ); ul = $("#answer-list"); textarea = $(".long-answer"); textarea.addClass("incorrect"); user_answer = textarea.val(); 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)) { textarea.removeClass("incorrect").addClass("correct"); } }); } current_question.answers.forEach(function (answer, n) { ul.append("
  • " + answer + "
  • "); if (compareString(answer, user_answer)) { textarea.removeClass("incorrect").addClass("correct"); } }); if (type == "rapid" && option.value != "Abnormal") { option.classList.add("incorrect"); } else { if ( textarea.hasClass("incorrect") == true && user_answer.replace(/\s/g, "") != "" ) { if (window.allow_self_marking) { $(".answer-panel").append( $("").click( function () { markCorrect(qid, user_answer); } ) ); } } } }) .catch(function (error) { console.log("error-", error); }); addFeedback(); } } function addFeedback() { if ( current_question.hasOwnProperty("feedback") && current_question.feedback.length > 0 ) { $(".answer-panel").append( $( "

    Feedback

    " + current_question.feedback + "
    " ) ); } } function markCorrect(qid, user_answer) { if (user_answer == "") { return; } db.user_answers .get({ qid: qid }) .then(function (answers) { let new_answers = []; if (answers != undefined) { new_answers = answers.ans; } new_answers.push(user_answer); db.user_answers.put({ qid: qid, ans: new_answers }); }) .catch(function (error) { console.log("error-", error); }); if (window.allow_self_marking) { $("#mark-correct").remove(); } let textarea = $(".long-answer").attr("class", "correct"); } function compareString(a, b) { return ( a.toLowerCase().replace(/\s/g, "") == b.toLowerCase().replace(/\s/g, "") ); } function answerInArray(arr, ans) { return ( arr .map((v) => v.toLowerCase().replace(/\s/g, "")) .includes(ans.toLowerCase().replace(/\s/g, "")) == true ); } function addFlagEvents() { // Bind flag change events $("button.flag").click(function (event) { const el = $(this); const nqid = el.attr("data-qid"); const qid = window.question_order[nqid]; const qidn = el.attr("data-qidn"); el.toggleClass("flag flag-selected"); if (el.hasClass("flag")) { $("#question-list-item-" + nqid + "-" + qidn + " span").css( "visibility", "hidden" ); db.flags.delete([cid, eid, qid, qidn]); } else { $("#question-list-item-" + nqid + "-" + qidn + " span").css( "visibility", "visible" ); db.flags.put({ cid: cid, eid: eid, qid: qid, qidn: qidn }); } updateQuestionListPanel(); }); } function loadFlagsFromDb(qid, n) { console.log("loadFlagsFromDb", cid, eid, qid, n); db.flags .get({ cid: cid, eid: eid, qid: qid, qidn: n }) .then(function (answer) { if (answer != undefined) { $("button.flag, button.flag-selected") .eq(parseInt(answer.qidn) - 1) .toggleClass("flag flag-selected"); } }) .catch(function (error) { console.log(error); }); } function urltoFile(url, filename, mimeType) { return fetch(url) .then(function (res) { return res.arrayBuffer(); }) .then(function (buf) { return new File([buf], filename, { type: mimeType }); }); } function loadMainImage(image, stack) { const PanTool = cornerstoneTools.PanTool; const ZoomTool = cornerstoneTools.ZoomTool; const WwwcTool = cornerstoneTools.WwwcTool; const RotateTool = cornerstoneTools.RotateTool; const StackScrollTool = cornerstoneTools.StackScrollTool; const element = document.getElementById("dicom-image"); cornerstone.enable(element); cornerstone.displayImage(element, image); cornerstoneTools.addStackStateManager(element, ["stack"]); cornerstoneTools.addToolState(element, "stack", stack); cornerstoneTools.addTool(PanTool); cornerstoneTools.addTool(ZoomTool); cornerstoneTools.addTool(WwwcTool); cornerstoneTools.addTool(RotateTool); cornerstoneTools.addTool(StackScrollTool); cornerstoneTools.setToolActive("Pan", { mouseButtonMask: 1 }); element.addEventListener("cornerstoneimagerendered", onImageRendered); setDicomCanvasNonFullscreen(element); cornerstone.reset(element); 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")); $(".canvas-panel").toggleClass("canvas-panel canvas-panel-fullscreen"); $("#dicom-image").attr("height", "100%"); $("#dicom-image").height("100%"); // $(".cornerstone-canvas").attr("height", "100%"); // $(".cornerstone-canvas").height("100%"); cornerstone.resize(this, true); } }); element.removeEventListener("wheel", element.wheelEventHandler); // Add tool selector $(".canvas-panel").append( '' ); $(".control-overlay") .get(0) .addEventListener("change", function () { changeControlSelection(); }); } function setDicomCanvasNonFullscreen(element) { const h = window.innerHeight - $(".nav-bar").height() - 4; $("#dicom-image").attr("height", h); $("#dicom-image").height(h); // $("#dicom-image.cornerstone-canvas").attr("height", h); // $("#dicom-image.cornerstone-canvas").height(h); $("#dicom-image .cornerstone-canvas").attr("width", "100%"); cornerstone.resize(element, true); } function showLoginDialog() { $("#login-dialog").modal(); } $("#start-exam-button").click(function (evt) { cid = parseInt($("#candidate-number").val()); $.modal.close(); }); function manualPanDicom(x, y) { dicom_element = document.getElementById("dicom-image"); const viewport = cornerstone.getViewport(dicom_element); viewport.translation.x += x; viewport.translation.y += y; cornerstone.setViewport(dicom_element, viewport); } function manualZoomDicom(delta) { dicom_element = document.getElementById("dicom-image"); const viewport = cornerstone.getViewport(dicom_element); viewport.scale += delta / 100; cornerstone.setViewport(dicom_element, viewport); } function manualRotateDicom(delta) { dicom_element = document.getElementById("dicom-image"); const viewport = cornerstone.getViewport(dicom_element); viewport.rotation += delta; cornerstone.setViewport(dicom_element, viewport); } function manualScrollDicom(n) { // There must be a better way to do this... dicom_element = document.getElementById("dicom-image"); c = cornerstone.getEnabledElement(dicom_element); max = c.toolStateManager.toolState.stack.data[0].imageIds.length; if (max < 2) { return; } current_index = c.toolStateManager.toolState.stack.data[0].currentImageIdIndex; // Loop through stack if out of bounds // Yes it is annoying but it's the way it is done... new_index = current_index + n; if (new_index >= max) { new_index = new_index - max; } else if (new_index < 0) { new_index = new_index + max; } c.toolStateManager.toolState.stack.data[0].currentImageIdIndex = new_index; id = c.toolStateManager.toolState.stack.data[0].imageIds[new_index]; cornerstone.loadImage(id).then((b) => { cornerstone.displayImage(dicom_element, b); }); // c = cornerstone.getEnabledElement(dicom_element) } function manualWindowDicom(wl, ww) { dicom_element = document.getElementById("dicom-image"); const viewport = cornerstone.getViewport(dicom_element); // For reference // brightness ~= wl // contrast ~= ww viewport.voi.windowCenter += wl; viewport.voi.windowWidth += ww; cornerstone.setViewport(dicom_element, viewport); } function debugCornerstone() { dicom_element = document.getElementById("dicom-image"); viewport = cornerstone.getViewport(dicom_element); c = cornerstone.getEnabledElement(dicom_element); }