i should do these more often

This commit is contained in:
Ross
2025-06-23 09:25:48 +01:00
parent 820d0bc030
commit 2f8601aa1a
15 changed files with 637 additions and 188 deletions
@@ -440,8 +440,7 @@
}, 200);
document.getElementById("reload-finding").addEventListener("click", function () {
importAnnotations_root(annotationjson3d)
importViewerState_root(viewerState);
window.mountDicomViewers();
});
// Initialize the DICOM viewer with the images and annotations
@@ -0,0 +1,68 @@
{% extends 'atlas/exams.html' %}
{% block content %}
<h2>Setup default display for case</h2>
<div id="root" class="dicom-viewer-root" data-images="{{case_detail.case.get_series_images_nested}}"
style="box-sizing: border-box; background: #222; width: 100vw; height: 800px;"
data-auto-cache-stack="true"
{% if case_detail.default_viewerstate %}
data-viewerstate={{ case_detail.default_viewerstate }}
{% endif %}
></div>
<button id="save-viewerstate" type="button" class="btn btn-primary mt-3">
Save Viewer State
</button>
<button id="reset-viewerstate" type="button" class="btn btn-secondary mt-3 ms-2">
Reset to Default
</button>
<div id="viewerstate-save-response" class="mt-2"></div>
<script>
document.getElementById('save-viewerstate').addEventListener('click', function() {
let viewerState = window.exportViewerState_root();
if (!viewerState) {
document.getElementById('viewerstate-save-response').innerHTML = "<span class='text-danger'>Viewer state could not be retrieved.</span>";
return;
}
fetch("{% url 'atlas:collection_case_display_setup' case_detail.collection.pk case_detail.case.pk %}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": "{{ csrf_token }}"
},
body: JSON.stringify({viewerstate: viewerState})
})
.then(response => response.text())
.then(html => {
document.getElementById('viewerstate-save-response').innerHTML = html;
})
.catch(error => {
document.getElementById('viewerstate-save-response').innerHTML = "<span class='text-danger'>Error saving viewer state.</span>";
});
});
document.getElementById('reset-viewerstate').addEventListener('click', function() {
fetch("{% url 'atlas:collection_case_display_setup' case_detail.collection.pk case_detail.case.pk %}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": "{{ csrf_token }}"
},
body: JSON.stringify({reset_viewerstate: true})
})
.then(response => response.text())
.then(html => {
document.getElementById('viewerstate-save-response').innerHTML = html;
// Optionally reload the page or viewer here if needed
})
.catch(error => {
document.getElementById('viewerstate-save-response').innerHTML = "<span class='text-danger'>Error resetting viewer state.</span>";
});
});
</script>
{% endblock %}
@@ -18,6 +18,8 @@
<li data-question_pk={{casedetail.case.pk}}><a title="sort_order: {{casedetail.sort_order}}" href="{% url 'atlas:collection_case_view' pk=collection.pk case_number=forloop.counter0 %}">Case {{forloop.counter}}</a>
: {{casedetail.case.title}}
(<a href="{% url 'atlas:collection_case_display_setup' casedetail.collection.pk casedetail.case.pk %}">setup default display</a>)
{% if collection.collection_type == "QUE" %}
(<a href='{% url "atlas:collection_case_details" casedetail.collection.pk casedetail.case.pk %}'>edit questions</a>
&nbsp;
+97 -4
View File
@@ -9,23 +9,29 @@
<p>Click on the 'Load' button to open the case in a the linked window. Loading subsequent cases/series will replace the first case in the window.</p>
<p>It is also possible to open a single series by clicking on the 'Load' button next to the individual series.</p>
<p>Once opened the case details will be show on the right hand side of the page.</p>
<div class="mb-2">
<button id="toggle-load-mode" class="btn btn-secondary">Switch to Load OHIF</button>
</div>
</details>
<div class="container">
<div class="row">
<div class="col-sm">
<h3>Cases</h3>
<button id="open-viewer" >Open Viewer</button>
<button id="open-viewer" >Open Viewer OHIF</button>
<button id="open-viewer-local" >Open Viewer Local</button>
<button id="view-series" class="text-dark border-dark" >Toggle series</button>
<ol id="" class="sortable">
{% for case in cases %}
<li data-question_pk={{case.pk}}>
{{case.casedetail.sort_order}}
<div class="case-item" data-title="{{case.title}}" data-case={{case.pk}} data-case-json='{{case.get_viva_details_json}}'>Case {{forloop.counter}}: {{case.title}} &nbsp;
<div class="case-item" data-title="{{case.title}}" data-case={{case.pk}} data-casejson='{{case.get_viva_details_json}}' data-images='{{case.get_series_images_nested}}'>Case {{forloop.counter}}: {{case.title}} &nbsp;
{% comment %} <button class="show-case no-padding" data-target="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}">Show</button> {% endcomment %}
{% comment %} <button class="send-message no-padding" data-title="{{case.title}}" data-target="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}">Open</button> {% endcomment %}
{% comment %} <button class="open-case no-padding" data-target="{% url 'atlas:collection_viva_case' collection.pk forloop.counter0 %}" data-type="case">Load</button> {% endcomment %}
<button class="open-case no-padding" data-target="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}" data-type="case">Load</button>
<button class="open-case no-padding" data-target="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}" data-type="case">Load OHIF</button>
<button class="open-case-local no-padding" data-target=case.pk data-type="case">Load</button>
<br />
<div class="pre-whitespace multi-image-block">
<details class="series-detail"><summary>Series:</summary>
@@ -42,6 +48,13 @@
<button class="open-series no-padding text-secondary border-secondary" data-type="series" data-series={{forloop.counter}} data-case={{case.pk}} data-target="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:series_dicom_json' series.pk %}">
Load
</button>
<button class="open-series-local no-padding text-secondary border-secondary"
data-type="series"
data-series="{{ forloop.counter }}"
data-case="{{ case.pk }}"
data-target="{{ series.pk }}">
Load
</button>
<br>
</span>
</span>
@@ -79,6 +92,38 @@
var win2 = false;
$(document).ready(function() {
let loadLocalMode = true;
// Update all "Load" buttons to reflect the current mode
function updateLoadButtons() {
if (loadLocalMode) {
$('.open-case').hide();
$('.open-series').hide();
$('.open-case-local').show();
$('.open-series-local').show();
$('#toggle-load-mode').text('Switch to Load OHIF');
$("#open-viewer").hide()
$("#open-viewer-local").show()
} else {
$('.open-case').show();
$('.open-series').show();
$('.open-case-local').hide();
$('.open-series-local').hide();
$("#open-viewer").show()
$("#open-viewer-local").hide()
$('#toggle-load-mode').text('Switch to Load Local');
}
}
// Initial state: show only normal Load buttons
updateLoadButtons();
// Toggle button click handler
$('#toggle-load-mode').click(function() {
loadLocalMode = !loadLocalMode;
updateLoadButtons();
});
const bc = new BroadcastChannel("viva_load");
bc.onmessage = (event) => {
console.log("message received");
@@ -88,6 +133,12 @@
$("#open-viewer").removeClass("flash-button");
//bc.postMessage({"type": "open-complete", "url" : event.data.url});
}
if (event.data.type == "open-complete-local") {
$("#loading-case").hide();
$("#open-viewer-local").removeClass("flash-button");
//bc.postMessage({"type": "open-complete-local"});
}
};
console.log('ready')
$('.show-collection, .show-case, .show-series').click(function() {
@@ -96,6 +147,43 @@
$('#case-title').text($(this).data('title'));
$('#viewer').attr('src', url)
});
$('.open-case-local').click(function() {
let c = this.closest(".case-item");
console.log('clicked local', c)
console.log('open local', c.dataset.case)
case_details = JSON.parse(c.dataset.casejson);
$('#loading-case').show()
$("#open-viewer-local").addClass("flash-button");
$('#current-case-title').html("<span class='title'>Case:</span> "+c.dataset.title);
$("#current-case-history").html("<span class='title'>History:</span> "+case_details['history']);
$("#current-case-discussion").html("<span class='title'>Discussion:</span> "+case_details['discussion']);
$("#current-case-report").html("<span class='title'>Report:</span> "+case_details['report']);
bc.postMessage({"type": "open", "case" : c.dataset.case, "images": c.dataset.images});
});
$('.open-series-local').click(function() {
let c = $(this).closest(".case-item")[0];
let seriesNumber = $(this).data('series');
let seriesPk = $(this).data('target');
let caseDetails = JSON.parse(c.dataset.casejson);
let images = JSON.parse(c.dataset.images)[seriesNumber - 1] || [];
$('#loading-case').show();
$("#open-viewer-local").addClass("flash-button");
$('#current-case-title').html("<span class='title'>Case:</span> " + c.dataset.title);
$('#current-case-series').html("<span class='title'>Series:</span> " + seriesNumber);
$("#current-case-history").html("<span class='title'>History:</span> " + caseDetails['history']);
$("#current-case-discussion").html("<span class='title'>Discussion:</span> " + caseDetails['discussion']);
$("#current-case-report").html("<span class='title'>Report:</span> " + caseDetails['report']);
// Send only the images for this series
bc.postMessage({
"type": "open",
"case": c.dataset.case,
"series": seriesPk,
"images": JSON.stringify(images)
});
});
$('.open-case, .open-series').click(function() {
var url = $(this).data('target');
console.log('clicked', url)
@@ -103,7 +191,7 @@
let c = $(this).closest(".case-item");
case_details = c.data('case-json');
case_details = c.data('casejson');
console.log(case_details);
$('#loading-case').show()
@@ -137,6 +225,11 @@
openSecondaryWindow("{% url 'atlas:collection_viewer' %}");
//bc.postMessage(url);
})
$('#open-viewer-local').click(function() {
$("#open-viewer-local").removeClass("flash-button");
openSecondaryWindow("{% url 'atlas:collection_viewer_local' %}");
//bc.postMessage(url);
})
$('#view-series').click(function() {
$(".series-detail").attr("open", (_, attr) => !attr);
//document.getElementsByClassName("series-detail").toggleAttribute("open");
+87 -2
View File
@@ -1,8 +1,6 @@
{% extends 'atlas/base.html' %}
{% load render_table from django_tables2 %}
{% block css %}
{% endblock %}
{% block content %}
@@ -20,8 +18,95 @@
</div>
View my <a href='{% url "atlas:case_view" %}?author={{request.user.id}}'>cases</a>.
</details>
<details class="mt-3">
<summary>
<strong>Extra options</strong>
</summary>
<form id="bulk-delete-form" method="post" action="{% url 'atlas:series_bulk_delete' %}">
{% csrf_token %}
<button type="submit" class="btn btn-danger mb-2" onclick="return confirm('Delete selected series?');">
Delete Selected
</button>
</form>
</details>
{% render_table table %}
</div>
<script>
document.getElementById('bulk-delete-form').addEventListener('submit', function(e) {
// Remove any previously added hidden inputs
document.querySelectorAll('#bulk-delete-form input[name="series_ids"]').forEach(el => el.remove());
// Find all checked checkboxes
const checked = document.querySelectorAll('input[name="selection"]:checked');
if (checked.length === 0) {
alert("Please select at least one series to delete.");
e.preventDefault();
return false;
}
// Add hidden inputs for each checked series
checked.forEach(cb => {
const hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = 'series_ids';
hidden.value = cb.value;
this.appendChild(hidden);
});
});
</script>
<div id="exam-options"></div>
{% endblock %}
{% block js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Delegate click event to all table rows
document.querySelectorAll('.table tbody tr').forEach(function(row) {
row.addEventListener('click', function(e) {
// Ignore clicks on links or checkboxes
if (e.target.tagName === 'A' || e.target.tagName === 'INPUT') return;
// Find the checkbox in this row
const checkbox = row.querySelector('input[name="selection"]');
if (checkbox) {
checkbox.checked = !checkbox.checked;
row.classList.toggle('selected', checkbox.checked);
}
});
// Keep row visually in sync with checkbox state (e.g. after page reload)
const checkbox = row.querySelector('input[name="selection"]');
if (checkbox && checkbox.checked) {
row.classList.add('selected');
}
// Also toggle row selection when checkbox is clicked directly
if (checkbox) {
checkbox.addEventListener('click', function(e) {
row.classList.toggle('selected', checkbox.checked);
// Prevent row click event from firing
e.stopPropagation();
});
}
});
});
</script>
{% endblock %}
{% block css %}
<style>
/* Highlight row on hover */
.table tbody tr:hover {
background-color: darkblue;
cursor: pointer;
}
/* Highlight selected row */
.table tbody tr.selected {
background-color: #b3d7ff !important;
color: #333;
}
</style>
{% endblock css %}
+1 -1
View File
@@ -44,7 +44,7 @@ Associated case:
</div> {% endcomment %}
<div id="root" class="dicom-viewer-root" data-images="{{ image_url_array_and_count.0 }}"
style="width: 1000px; height: 600px; box-sizing: border-box; background: #222;"
style="max-width: 1000px; height: 600px; box-sizing: border-box; background: #222;"
data-auto-cache-stack=true
></div>
+78 -15
View File
@@ -68,23 +68,16 @@
hx-confirm="This will clear selected uploads, Continue?"
title="Deleted selected uploads"
>Delete Uploads</button>
{% if case %}
<button id="import-dicoms-case-button"
hx-post="{% url 'api-1:import_dicoms_case' case.id %}"
hx-target=".imported"
{% comment %} hx-indicator=".indicator" {% endcomment %}
hx-include=".selected,[name='order-series']"
>Import into case</button>
{% else %}
<button id="import-dicoms-button"
hx-post="{% url 'api-1:import_dicoms' %}"
hx-target=".imported"
{% comment %} hx-indicator=".indicator" {% endcomment %}
hx-include=".selected,[name='order-series']"
>Import</button>
{% endif %}
<button id="import-dicoms-sequential-button"
type="button"
class="btn btn-primary"
>Import {% if case %}into case{% endif %} (one at a time)</button>
<div id="import-status"></div>
<div class="indicator"><div class="loader"></div>Loading...</div>
<div id="import-status"></div>
<div class="imported">
@@ -96,6 +89,72 @@
<p><a href="{% url 'atlas:series_view' %}?case=null&sort=-modified_date">View orphan series</a></p>
<script>
document.getElementById('import-dicoms-sequential-button').addEventListener('click', async function() {
const checkboxes = Array.from(document.querySelectorAll('input[name="selection"]:checked'));
const allCheckboxes = Array.from(document.querySelectorAll('input[name="selection"]'));
// Only include checkboxes that are not disabled (i.e., not already imported)
const selectableCheckboxes = allCheckboxes.filter(cb => !cb.disabled);
const toImport = checkboxes.length
? checkboxes.filter(cb => !cb.disabled)
: selectableCheckboxes;
if (toImport.length === 0) {
alert("Please select at least one series to import (not already imported).");
return;
}
const statusDiv = document.getElementById('import-status');
statusDiv.innerHTML = '';
const orderSeries = document.querySelector('input[name="order-series"]:checked').value;
const csrfToken = '{{ csrf_token }}';
{% if case %}
const importUrl = "{% url 'api-1:import_dicoms_case' case.id %}";
{% else %}
const importUrl = "{% url 'api-1:import_dicoms' %}";
{% endif %}
for (let i = 0; i < toImport.length; i++) {
const seriesId = toImport[i].value;
statusDiv.innerHTML += `<div id="import-status-${seriesId}">Importing series ${seriesId}...</div>`;
try {
const response = await fetch(importUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"X-CSRFToken": csrfToken,
},
body: `selection=${encodeURIComponent(seriesId)}&order-series=${encodeURIComponent(orderSeries)}`
});
const text = await response.text();
document.getElementById(`import-status-${seriesId}`).innerHTML = `Series ${seriesId}: ${text}`;
// Mark as imported in the UI and make unselectable
const li = document.querySelector(`#series-list input[value="${seriesId}"]`)?.closest('li');
if (li && !li.classList.contains('imported')) {
li.classList.add('imported');
// Add badge if not already present
if (!li.querySelector('.badge.bg-success')) {
const badge = document.createElement('span');
badge.className = 'badge bg-success ms-2';
badge.textContent = 'Imported';
li.appendChild(badge);
}
// Disable checkbox and prevent further selection
const checkbox = li.querySelector('input[name="selection"]');
if (checkbox) {
checkbox.disabled = true;
checkbox.checked = false;
}
// Remove click handler for selection
li.querySelector('span').onclick = null;
li.style.pointerEvents = "none";
li.style.opacity = "0.7";
}
} catch (e) {
document.getElementById(`import-status-${seriesId}`).innerHTML = `Series ${seriesId}: <span style="color:red;">Failed</span>`;
}
}
statusDiv.innerHTML += "<div>All done.</div>";
});
</script>
{% endblock %}
@@ -155,6 +214,10 @@
.series-block-popup-link {
float: right;
}
#series-list li.imported {
border-color: #e6ffe6;
opacity: 0.7;
}
</style>
{% endblock css %}
+54
View File
@@ -0,0 +1,54 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>Viewer</title>
<style type="text/css">
body, html
{
margin: 0; padding: 0; height: 100%; overflow: hidden;
}
#viewer
{
position:absolute; left: 0; right: 0; bottom: 0; top: 0px;
}
</style>
<script src="{% static 'dv3d/index.js' %}" type="module" defer="defer" type="module"></script>
</head>
<body>
<div id="awating-load">
<p>Awating load request</p>
This page will display series when loaded from other pages.
</div>
<div id="root" class="dicom-viewer-root"
style="box-sizing: border-box; background: #222; position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100vw; height: 100vh;"
data-auto-cache-stack="true"
></div>
<script type="text/javascript">
var win2 = false;
document.addEventListener("DOMContentLoaded", function() {
document.title = "Case: {{current_case.title}}";
console.log('ready');
const bc = new BroadcastChannel("viva_load");
bc.onmessage = (event) => {
console.log("message received");
console.log(event);
if (event.data.type == "open") {
document.getElementById("awating-load").style.display = "none";
console.log("open, images: ", event.data.images);
document.getElementById("root").dataset.images = event.data.images;
window.mountDicomViewers();
bc.postMessage({"type": "open-complete-local"});
}
};
});
</script>
</body>
</html>