feat(optimization): Implement series optimization features including downsampling, empty DICOM removal, and reconstruction options
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
<span class="series-block series-block-interactive" id="series-block-{{ series.pk }}" data-series-id="{{series.pk}}">
|
||||
<span>
|
||||
<span class="series-block-series-number">Series {{ forloop.counter }}:</span><br>
|
||||
<a href="{% url 'atlas:series_detail' pk=series.pk }}" class="series-block-title-link">
|
||||
<a href="{% url 'atlas:series_detail' pk=series.pk %}" class="series-block-title-link">
|
||||
{{series.get_block}}
|
||||
</a>
|
||||
<br>
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
class="btn btn-outline-warning btn-sm"
|
||||
hx-get="{% url 'atlas:series_split_by_tag' pk=series.pk %}?analyze=1"
|
||||
hx-target="#series-split-by-tag-panel"
|
||||
hx-swap="outerHTML">
|
||||
hx-swap="outerHTML"
|
||||
hx-indicator="#series-split-analysis-indicator">
|
||||
Analyze split options
|
||||
</button>
|
||||
<span id="series-split-analysis-indicator" class="spinner-border spinner-border-sm text-warning htmx-indicator" role="status" aria-hidden="true"></span>
|
||||
</div>
|
||||
|
||||
{% if analyze_requested %}
|
||||
@@ -16,7 +18,8 @@
|
||||
action="{% url 'atlas:series_split_by_tag' pk=series.pk %}"
|
||||
hx-post="{% url 'atlas:series_split_by_tag' pk=series.pk %}"
|
||||
hx-target="#split-by-tag-result"
|
||||
hx-swap="innerHTML">
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#series-split-submit-indicator">
|
||||
{% csrf_token %}
|
||||
<div class="input-group input-group-sm">
|
||||
<select name="dicom_tag" class="form-select form-select-sm">
|
||||
@@ -27,6 +30,7 @@
|
||||
<button type="submit" class="btn btn-outline-warning btn-sm">Split</button>
|
||||
</div>
|
||||
</form>
|
||||
<span id="series-split-submit-indicator" class="spinner-border spinner-border-sm text-warning htmx-indicator" role="status" aria-hidden="true"></span>
|
||||
<p class="small text-muted mb-0">Only tags that can produce more than one output series are shown.</p>
|
||||
{% else %}
|
||||
<div class="alert alert-secondary mb-0 small">
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
class="btn btn-outline-info btn-sm"
|
||||
hx-get="{% url 'atlas:series_tag_consistency' pk=series.pk %}?analyze=1"
|
||||
hx-target="#series-tag-consistency-panel"
|
||||
hx-swap="outerHTML">
|
||||
hx-swap="outerHTML"
|
||||
hx-indicator="#series-tag-consistency-indicator">
|
||||
Re-run analysis
|
||||
</button>
|
||||
<span id="series-tag-consistency-indicator" class="spinner-border spinner-border-sm text-info htmx-indicator" role="status" aria-hidden="true"></span>
|
||||
{% if analyze_requested %}
|
||||
<span class="small text-muted">Scanned {{ image_count }} image{{ image_count|pluralize }}</span>
|
||||
{% endif %}
|
||||
@@ -57,7 +59,8 @@
|
||||
action="{% url 'atlas:series_split_by_tag' pk=series.pk %}"
|
||||
hx-post="{% url 'atlas:series_split_by_tag' pk=series.pk %}"
|
||||
hx-target="#tag-consistency-split-result"
|
||||
hx-swap="innerHTML">
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#tag-consistency-split-indicator">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="dicom_tag" value="{{ row.tag_name }}">
|
||||
<button type="submit" class="btn btn-outline-warning btn-sm">Split</button>
|
||||
@@ -99,7 +102,8 @@
|
||||
action="{% url 'atlas:series_split_by_tag' pk=series.pk %}"
|
||||
hx-post="{% url 'atlas:series_split_by_tag' pk=series.pk %}"
|
||||
hx-target="#tag-consistency-split-result"
|
||||
hx-swap="innerHTML">
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#tag-consistency-split-indicator">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="dicom_tag" value="{{ row.tag_name }}">
|
||||
<button type="submit" class="btn btn-outline-warning btn-sm">Split</button>
|
||||
@@ -143,7 +147,8 @@
|
||||
action="{% url 'atlas:series_split_by_tag' pk=series.pk %}"
|
||||
hx-post="{% url 'atlas:series_split_by_tag' pk=series.pk %}"
|
||||
hx-target="#tag-consistency-split-result"
|
||||
hx-swap="innerHTML">
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#tag-consistency-split-indicator">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="dicom_tag" value="{{ row.tag_name }}">
|
||||
<button type="submit" class="btn btn-outline-warning btn-sm">Split</button>
|
||||
@@ -168,6 +173,7 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<span id="tag-consistency-split-indicator" class="spinner-border spinner-border-sm text-warning htmx-indicator me-auto" role="status" aria-hidden="true"></span>
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
{% with image_url_array_and_count=series.get_image_url_array_and_count %}
|
||||
<div id="root"
|
||||
class="dicom-viewer-root w-100 viewer-frame-standard series-viewer-resizable"
|
||||
style="min-height: clamp(600px, 75vh, 1200px); resize: vertical; overflow: auto;"
|
||||
style="height: clamp(640px, 78vh, 1200px); min-height: 640px; resize: vertical; overflow: auto;"
|
||||
data-images="{{ image_url_array_and_count.0 }}"
|
||||
data-auto-cache-stack=false>
|
||||
</div>
|
||||
@@ -202,10 +202,12 @@
|
||||
hx-get="{% url 'atlas:series_tag_consistency' pk=series.pk %}?analyze=1"
|
||||
hx-target="#series-tag-consistency-modal .modal-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#series-tag-consistency-load-indicator"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#series-tag-consistency-modal">
|
||||
Analyze tag consistency
|
||||
</button>
|
||||
<span id="series-tag-consistency-load-indicator" class="spinner-border spinner-border-sm text-info htmx-indicator" role="status" aria-hidden="true"></span>
|
||||
{% if can_edit %}
|
||||
<button class="btn btn-outline-warning btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
@@ -232,88 +234,139 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Truncate Series Modal -->
|
||||
<!-- Series Optimization Modal -->
|
||||
{% if can_edit %}
|
||||
<div id="truncate-series-modal" class="modal modal-blur fade" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Truncate Series & Optimize Disk Space</h5>
|
||||
<h5 class="modal-title">Series Optimization</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<!-- Viewer side -->
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header fw-semibold">Viewer Preview</div>
|
||||
<div class="card-body p-2" style="max-height: 500px; overflow-y: auto;">
|
||||
<div id="truncate-modal-viewer" class="dicom-viewer-root w-100"
|
||||
style="height: 400px; background: #222;"
|
||||
<div class="card-header fw-semibold">Viewer / Compare Preview</div>
|
||||
<div class="card-body p-2 d-flex flex-column gap-2">
|
||||
<div id="truncate-modal-viewer" class="dicom-viewer-root w-100"
|
||||
style="height: 320px; background: #222;"
|
||||
data-images="{{ image_url_array_and_count.0 }}"
|
||||
data-auto-cache-stack="false">
|
||||
</div>
|
||||
<div id="downsample-compare-wrap" class="d-none">
|
||||
<div class="small text-muted mb-1">Downsample preview comparison</div>
|
||||
<div id="downsample-compare-viewer" class="dicom-viewer-root w-100"
|
||||
style="height: 240px; background: #222;"
|
||||
data-auto-cache-stack="false"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls side -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header fw-semibold">Truncate Range</div>
|
||||
<div class="card-header fw-semibold">Operations</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<strong>⚠️ Warning:</strong> Truncation is irreversible. If you have reordered the series, this may remove the wrong images.
|
||||
<strong>Warning:</strong> Destructive actions cannot be undone.
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Start Image</label>
|
||||
<div class="input-group">
|
||||
<input id="truncate-lower-input" type="number" class="form-control form-control-sm" value="1" min="1" max="{{image_url_array_and_count.1}}">
|
||||
<button id="truncate-set-lower-btn" class="btn btn-outline-secondary btn-sm" type="button">Set</button>
|
||||
<form id="series-optimize-form"
|
||||
hx-post="{% url 'atlas:series_optimize' series_id=series.pk %}"
|
||||
hx-target="#series-optimize-feedback"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#series-optimize-indicator">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="start" id="opt-start-hidden" value="1">
|
||||
<input type="hidden" name="end" id="opt-end-hidden" value="{{ image_url_array_and_count.1 }}">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Start image</label>
|
||||
<div class="input-group">
|
||||
<input id="truncate-lower-input" type="number" class="form-control form-control-sm" value="1" min="1" max="{{image_url_array_and_count.1}}">
|
||||
<button id="truncate-set-lower-btn" class="btn btn-outline-secondary btn-sm" type="button">Set from viewer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">End Image</label>
|
||||
<div class="input-group">
|
||||
<input id="truncate-upper-input" type="number" class="form-control form-control-sm" value="{{image_url_array_and_count.1}}" min="1" max="{{image_url_array_and_count.1}}">
|
||||
<button id="truncate-set-upper-btn" class="btn btn-outline-secondary btn-sm" type="button">Set</button>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">End image</label>
|
||||
<div class="input-group">
|
||||
<input id="truncate-upper-input" type="number" class="form-control form-control-sm" value="{{image_url_array_and_count.1}}" min="1" max="{{image_url_array_and_count.1}}">
|
||||
<button id="truncate-set-upper-btn" class="btn btn-outline-secondary btn-sm" type="button">Set from viewer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<button id="truncate-test-modal-btn" class="btn btn-sm btn-outline-primary">Test Preview</button>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="mb-3">
|
||||
<h6>Disk Space Optimization</h6>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="remove-empty-dicoms-check">
|
||||
<label class="form-check-label" for="remove-empty-dicoms-check">
|
||||
Remove empty DICOMs (no content)
|
||||
</label>
|
||||
<small class="d-block text-muted mt-1">Removes DICOM files that do not contain usable image data.</small>
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button id="truncate-test-modal-btn" class="btn btn-sm btn-outline-primary" type="button">Test preview</button>
|
||||
<button class="btn btn-sm btn-danger" type="submit" name="operation" value="truncate">Apply truncate</button>
|
||||
<button class="btn btn-sm btn-outline-warning" type="submit" name="operation" value="remove_empty">Remove empty DICOMs</button>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="downsample-pixels-check">
|
||||
<label class="form-check-label" for="downsample-pixels-check">
|
||||
Downsample pixel data (by 50%)
|
||||
</label>
|
||||
<small class="d-block text-muted mt-1">Reduces resolution while preserving original hash for duplicate detection.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="truncate-modal-output" class="alert alert-info d-none" role="alert"></div>
|
||||
<hr>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="downsample-pct" class="form-label">Downsample percentage</label>
|
||||
<select id="downsample-pct" name="downsample_pct" class="form-select form-select-sm">
|
||||
<option value="25">25% reduction</option>
|
||||
<option value="40">40% reduction</option>
|
||||
<option value="50" selected>50% reduction</option>
|
||||
<option value="60">60% reduction</option>
|
||||
<option value="75">75% reduction</option>
|
||||
</select>
|
||||
<small class="text-muted">Original hash is preserved after downsampling.</small>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button class="btn btn-sm btn-outline-info" type="submit" name="operation" value="downsample_preview">Preview downsample compare</button>
|
||||
<button class="btn btn-sm btn-outline-success" type="submit" name="operation" value="downsample">Apply downsample</button>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="fw-semibold small mb-2">Generate reconstructions</div>
|
||||
<div class="d-flex flex-wrap gap-3 mb-2">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="recon-axial" name="recon_planes" value="axial" checked>
|
||||
<label class="form-check-label" for="recon-axial">Axial</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="recon-coronal" name="recon_planes" value="coronal" checked>
|
||||
<label class="form-check-label" for="recon-coronal">Coronal</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="recon-sagittal" name="recon_planes" value="sagittal" checked>
|
||||
<label class="form-check-label" for="recon-sagittal">Sagittal</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<label class="form-label small mb-1" for="recon-slice-thickness">Slice thickness (mm)</label>
|
||||
<input type="number" step="0.1" min="0.1" id="recon-slice-thickness" name="recon_slice_thickness" class="form-control form-control-sm" placeholder="e.g. 1.0">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label small mb-1" for="recon-slice-spacing">Slice spacing (mm)</label>
|
||||
<input type="number" step="0.1" min="0.1" id="recon-slice-spacing" name="recon_slice_spacing" class="form-control form-control-sm" placeholder="e.g. 1.0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-sm btn-outline-primary" type="submit" name="operation" value="reconstruct">Generate recon series</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="d-flex gap-2">
|
||||
<button id="truncate-execute-modal-btn" class="btn btn-danger btn-sm">Execute Truncation</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
<div class="card-footer d-flex align-items-center gap-2">
|
||||
<span id="series-optimize-indicator" class="spinner-border spinner-border-sm text-primary htmx-indicator" role="status" aria-hidden="true"></span>
|
||||
<small class="text-muted">Processing operations may take time for large stacks.</small>
|
||||
<button type="button" class="btn btn-secondary btn-sm ms-auto" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
<div class="px-3 pb-3">
|
||||
<div id="series-optimize-feedback" class="small"></div>
|
||||
<div id="truncate-modal-output" class="alert alert-info d-none mt-2 mb-0" role="alert"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -338,7 +391,7 @@
|
||||
if (saved) {
|
||||
const viewer = document.getElementById('root');
|
||||
if (viewer) {
|
||||
viewer.style.minHeight = saved;
|
||||
viewer.style.height = saved;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -360,117 +413,133 @@
|
||||
});
|
||||
resizeObserver.observe(viewer);
|
||||
restoreViewerHeight();
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
if (window.resizeDicomViewers) {
|
||||
window.resizeDicomViewers();
|
||||
}
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
} catch (e) {
|
||||
console.warn('Viewer resize dispatch failed', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Modal truncate handlers
|
||||
function syncOptimizeBounds() {
|
||||
const startInput = document.getElementById('truncate-lower-input');
|
||||
const endInput = document.getElementById('truncate-upper-input');
|
||||
const startHidden = document.getElementById('opt-start-hidden');
|
||||
const endHidden = document.getElementById('opt-end-hidden');
|
||||
if (!startInput || !endInput || !startHidden || !endHidden) {
|
||||
return;
|
||||
}
|
||||
startHidden.value = startInput.value;
|
||||
endHidden.value = endInput.value;
|
||||
}
|
||||
|
||||
// Modal optimization handlers
|
||||
const truncateModal = document.getElementById('truncate-series-modal');
|
||||
if (truncateModal) {
|
||||
truncateModal.addEventListener('show.bs.modal', function() {
|
||||
// Initialize the modal viewer
|
||||
truncateModal.addEventListener('show.bs.modal', function () {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (window.mountDicomViewers) {
|
||||
window.mountDicomViewers();
|
||||
}
|
||||
if (window.resizeDicomViewers) {
|
||||
window.resizeDicomViewers();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Modal viewer init failed', e);
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
|
||||
const getStackPosition = window[`getCurrentStackPosition_${apiKey}`] || window.getCurrentStackPosition_root;
|
||||
|
||||
document.getElementById('truncate-set-lower-btn')?.addEventListener('click', () => {
|
||||
if (window.getCurrentStackPosition_root) {
|
||||
const pos = window.getCurrentStackPosition_root(0) + 1;
|
||||
if (getStackPosition) {
|
||||
const pos = getStackPosition(0) + 1;
|
||||
document.getElementById('truncate-lower-input').value = pos;
|
||||
syncOptimizeBounds();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('truncate-set-upper-btn')?.addEventListener('click', () => {
|
||||
if (window.getCurrentStackPosition_root) {
|
||||
const pos = window.getCurrentStackPosition_root(0) + 1;
|
||||
if (getStackPosition) {
|
||||
const pos = getStackPosition(0) + 1;
|
||||
document.getElementById('truncate-upper-input').value = pos;
|
||||
syncOptimizeBounds();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('truncate-lower-input')?.addEventListener('input', syncOptimizeBounds);
|
||||
document.getElementById('truncate-upper-input')?.addEventListener('input', syncOptimizeBounds);
|
||||
|
||||
document.getElementById('truncate-test-modal-btn')?.addEventListener('click', () => {
|
||||
const lower = parseInt(document.getElementById('truncate-lower-input').value) - 1;
|
||||
const upper = parseInt(document.getElementById('truncate-upper-input').value) - 1;
|
||||
|
||||
|
||||
if (lower < 0 || upper >= totalImages || lower > upper) {
|
||||
alert('Invalid range. Please check your bounds.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.truncateStack_root) {
|
||||
const ok = window.truncateStack_root(lower, upper);
|
||||
const truncateFn = window[`truncateStack_${apiKey}`] || window.truncateStack_root;
|
||||
if (truncateFn) {
|
||||
const ok = truncateFn(lower, upper);
|
||||
if (ok) {
|
||||
document.getElementById('truncate-modal-output').innerHTML = `Preview shows images ${lower + 1} to ${upper + 1} (${upper - lower + 1} total).`;
|
||||
document.getElementById('truncate-modal-output').classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('truncate-execute-modal-btn')?.addEventListener('click', () => {
|
||||
const lower = parseInt(document.getElementById('truncate-lower-input').value) - 1;
|
||||
const upper = parseInt(document.getElementById('truncate-upper-input').value) - 1;
|
||||
const removeEmpty = document.getElementById('remove-empty-dicoms-check').checked;
|
||||
const downsample = document.getElementById('downsample-pixels-check').checked;
|
||||
document.body.addEventListener('htmx:configRequest', function (event) {
|
||||
const form = document.getElementById('series-optimize-form');
|
||||
if (!form || event.target !== form) {
|
||||
return;
|
||||
}
|
||||
syncOptimizeBounds();
|
||||
});
|
||||
|
||||
if (lower >= upper) {
|
||||
alert('The lower bound must be less than the upper bound.');
|
||||
document.body.addEventListener('htmx:afterSwap', function (event) {
|
||||
if (event.target && event.target.id === 'series-optimize-feedback') {
|
||||
const payload = document.getElementById('downsample-compare-payload');
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
const totalToKeep = upper - lower + 1;
|
||||
const confirmMsg = `Truncate series to ${totalToKeep} images?${removeEmpty ? '\n- Remove empty DICOMs' : ''}${downsample ? '\n- Downsample pixel data' : ''}`;
|
||||
|
||||
if (confirm(confirmMsg)) {
|
||||
// Build URL with optional parameters
|
||||
let truncateUrl = `{% url 'atlas:series_truncate' series.pk 0 0 %}`.replace('/0/0/', `/${lower}/${upper}/`);
|
||||
if (removeEmpty) truncateUrl += '?remove_empty=1';
|
||||
if (downsample) truncateUrl += (removeEmpty ? '&' : '?') + 'downsample=1';
|
||||
|
||||
htmx.ajax('GET', truncateUrl, '#truncate-modal-output');
|
||||
|
||||
// Close modal after a delay
|
||||
setTimeout(() => {
|
||||
const modal = bootstrap.Modal.getInstance(truncateModal);
|
||||
if (modal) modal.hide();
|
||||
}, 1500);
|
||||
const namedStacks = payload.dataset.namedStacks;
|
||||
if (!namedStacks) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Keep old inline handlers for compatibility
|
||||
$("#set-lower-truncation-bound-button").click(() => {
|
||||
$("#lower-truncation-bound-input").val(getCurrentStackPosition_root(0) + 1);
|
||||
});
|
||||
$("#set-upper-truncation-bound-button").click(() => {
|
||||
$("#upper-truncation-bound-input").val(getCurrentStackPosition_root(0) + 1);
|
||||
});
|
||||
$("#truncate-test-button").click(() => {
|
||||
const lower = parseInt($("#lower-truncation-bound-input").val()) - 1;
|
||||
const upper = parseInt($("#upper-truncation-bound-input").val()) - 1;
|
||||
const ok = window[`truncateStack_${apiKey}`](lower, upper);
|
||||
if (ok) {
|
||||
$("#truncate-output").text("The truncated images are shown above.");
|
||||
const compareWrap = document.getElementById('downsample-compare-wrap');
|
||||
const compareViewer = document.getElementById('downsample-compare-viewer');
|
||||
if (!compareWrap || !compareViewer) {
|
||||
return;
|
||||
}
|
||||
|
||||
compareWrap.classList.remove('d-none');
|
||||
compareViewer.dataset.namedStacks = namedStacks;
|
||||
compareViewer.dataset.autoCacheStack = 'false';
|
||||
|
||||
try {
|
||||
if (window.mountDicomViewers) {
|
||||
window.mountDicomViewers();
|
||||
}
|
||||
if (window.resizeDicomViewers) {
|
||||
window.resizeDicomViewers();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to mount downsample compare viewer', err);
|
||||
}
|
||||
}
|
||||
});
|
||||
$("#truncate-button").click(() => {
|
||||
lower_bound = parseInt($("#lower-truncation-bound-input").val()) - 1;
|
||||
upper_bound = parseInt($("#upper-truncation-bound-input").val()) - 1;
|
||||
|
||||
if (lower_bound >= upper_bound) {
|
||||
alert("The lower bound must be less than the upper bound.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(confirm(`Truncated series. This will leave ${upper_bound - lower_bound + 1} images.`) == true) {
|
||||
const truncateTemplate = `{% url 'atlas:series_truncate' series.id 0 0 %}`; // e.g. /atlas/series/920/truncate/0/0/
|
||||
const truncateUrl = truncateTemplate.replace('/0/0/', `/${lower_bound}/${upper_bound}/`);
|
||||
htmx.ajax("GET", truncateUrl, "#truncate-output")
|
||||
}
|
||||
})
|
||||
syncOptimizeBounds();
|
||||
|
||||
$("#add-finding-button").click(() => {
|
||||
$("#hidden-form").show()
|
||||
|
||||
Reference in New Issue
Block a user