760 lines
27 KiB
HTML
760 lines
27 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}DICOM Viewer{% if study %} - {{ study.patient.get_full_name }}{% endif %}{% endblock %}
|
|
|
|
{% block css %}
|
|
<style>
|
|
.dicom-viewer {
|
|
height: calc(100vh - 200px);
|
|
background: #000;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.viewer-toolbar {
|
|
background: rgba(0, 0, 0, 0.8);
|
|
color: white;
|
|
padding: 10px;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.viewer-controls {
|
|
background: rgba(0, 0, 0, 0.8);
|
|
color: white;
|
|
padding: 10px;
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.series-thumbnails {
|
|
background: rgba(0, 0, 0, 0.9);
|
|
color: white;
|
|
width: 200px;
|
|
position: absolute;
|
|
top: 60px;
|
|
bottom: 60px;
|
|
right: 0;
|
|
z-index: 1000;
|
|
overflow-y: auto;
|
|
padding: 10px;
|
|
}
|
|
|
|
.thumbnail {
|
|
width: 100%;
|
|
height: 80px;
|
|
background: #333;
|
|
margin-bottom: 10px;
|
|
cursor: pointer;
|
|
border: 2px solid transparent;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.thumbnail.active {
|
|
border-color: #007bff;
|
|
}
|
|
|
|
.image-container {
|
|
position: absolute;
|
|
top: 60px;
|
|
bottom: 60px;
|
|
left: 0;
|
|
right: 200px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.dicom-image {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
cursor: crosshair;
|
|
}
|
|
|
|
.measurements-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.measurement-line {
|
|
stroke: #ff0000;
|
|
stroke-width: 2;
|
|
fill: none;
|
|
}
|
|
|
|
.measurement-text {
|
|
fill: #ff0000;
|
|
font-size: 12px;
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
|
|
.patient-info {
|
|
position: absolute;
|
|
top: 70px;
|
|
left: 10px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
color: white;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
z-index: 1001;
|
|
}
|
|
|
|
.study-info {
|
|
position: absolute;
|
|
top: 70px;
|
|
right: 220px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
color: white;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
z-index: 1001;
|
|
}
|
|
|
|
.loading-spinner {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: white;
|
|
z-index: 1002;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex align-items-center mb-3">
|
|
<div>
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{% url 'radiology:dashboard' %}">Radiology</a></li>
|
|
{% if study %}
|
|
<li class="breadcrumb-item"><a href="{% url 'radiology:study_list' %}">Studies</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'radiology:study_detail' study.pk %}">{{ study.patient.get_full_name }}</a></li>
|
|
{% endif %}
|
|
<li class="breadcrumb-item active">DICOM Viewer</li>
|
|
</ol>
|
|
<h1 class="page-header mb-0">DICOM Viewer</h1>
|
|
</div>
|
|
<div class="ms-auto">
|
|
<button type="button" class="btn btn-outline-secondary me-2" onclick="toggleFullscreen()">
|
|
<i class="fas fa-expand me-1"></i>Fullscreen
|
|
</button>
|
|
{% if study %}
|
|
<a href="{% url 'radiology:study_detail' study.pk %}" class="btn btn-secondary">
|
|
<i class="fas fa-arrow-left me-2"></i>Back to Study
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-body p-0">
|
|
<div class="dicom-viewer" id="dicomViewer">
|
|
<!-- Toolbar -->
|
|
<div class="viewer-toolbar">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div class="d-flex align-items-center">
|
|
<div class="btn-group me-3" role="group">
|
|
<button type="button" class="btn btn-sm btn-outline-light" id="zoomInBtn" title="Zoom In">
|
|
<i class="fas fa-search-plus"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-light" id="zoomOutBtn" title="Zoom Out">
|
|
<i class="fas fa-search-minus"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-light" id="resetZoomBtn" title="Reset Zoom">
|
|
<i class="fas fa-expand-arrows-alt"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="btn-group me-3" role="group">
|
|
<button type="button" class="btn btn-sm btn-outline-light" id="panBtn" title="Pan">
|
|
<i class="fas fa-hand-paper"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-light" id="measureBtn" title="Measure">
|
|
<i class="fas fa-ruler"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-light" id="annotateBtn" title="Annotate">
|
|
<i class="fas fa-comment"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="btn-group me-3" role="group">
|
|
<button type="button" class="btn btn-sm btn-outline-light" id="windowLevelBtn" title="Window/Level">
|
|
<i class="fas fa-adjust"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-light" id="invertBtn" title="Invert">
|
|
<i class="fas fa-adjust"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-light" id="rotateBtn" title="Rotate">
|
|
<i class="fas fa-redo"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="btn-group" role="group">
|
|
<button type="button" class="btn btn-sm btn-outline-light" id="playBtn" title="Play/Pause">
|
|
<i class="fas fa-play"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-light" id="prevBtn" title="Previous">
|
|
<i class="fas fa-step-backward"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-light" id="nextBtn" title="Next">
|
|
<i class="fas fa-step-forward"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center">
|
|
<span class="me-3" id="imageInfo">Image 1 of 1</span>
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-sm btn-outline-light dropdown-toggle" data-bs-toggle="dropdown">
|
|
<i class="fas fa-cog me-1"></i>Settings
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="#" onclick="showPreferences()"><i class="fas fa-sliders-h me-2"></i>Preferences</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="showKeyboardShortcuts()"><i class="fas fa-keyboard me-2"></i>Keyboard Shortcuts</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item" href="#" onclick="exportImage()"><i class="fas fa-download me-2"></i>Export Image</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="printImage()"><i class="fas fa-print me-2"></i>Print</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Patient Information -->
|
|
{% if study %}
|
|
<div class="patient-info">
|
|
<div><strong>{{ study.patient.get_full_name }}</strong></div>
|
|
<div>ID: {{ study.patient.patient_id }}</div>
|
|
<div>DOB: {{ study.patient.date_of_birth|date:"M d, Y" }}</div>
|
|
<div>Gender: {{ study.patient.get_gender_display }}</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Study Information -->
|
|
{% if study %}
|
|
<div class="study-info">
|
|
<div><strong>{{ study.modality }}</strong></div>
|
|
<div>{{ study.study_date|date:"M d, Y" }}</div>
|
|
<div>{{ study.description|default:"No description" }}</div>
|
|
<div>Accession: {{ study.accession_number }}</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Loading Spinner -->
|
|
<div class="loading-spinner" id="loadingSpinner">
|
|
<div class="spinner-border text-light" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<div class="mt-2">Loading DICOM images...</div>
|
|
</div>
|
|
|
|
<!-- Image Container -->
|
|
<div class="image-container" id="imageContainer">
|
|
<canvas id="dicomCanvas" class="dicom-image"></canvas>
|
|
|
|
<!-- Measurements Overlay -->
|
|
<svg class="measurements-overlay" id="measurementsOverlay">
|
|
<!-- Measurement lines and annotations will be added here -->
|
|
</svg>
|
|
</div>
|
|
|
|
<!-- Series Thumbnails -->
|
|
<div class="series-thumbnails" id="seriesThumbnails">
|
|
<h6 class="mb-3">Series</h6>
|
|
{% if series_list %}
|
|
{% for series in series_list %}
|
|
<div class="thumbnail" data-series-id="{{ series.id }}" onclick="loadSeries('{{ series.id }}')">
|
|
<div class="p-2">
|
|
<div class="small fw-bold">{{ series.series_number }}</div>
|
|
<div class="small">{{ series.description|truncatechars:20 }}</div>
|
|
<div class="small text-muted">{{ series.image_count }} images</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="text-center text-muted">
|
|
<i class="fas fa-images fa-2x mb-2"></i>
|
|
<div>No series available</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Controls -->
|
|
<div class="viewer-controls">
|
|
<div class="row align-items-center">
|
|
<div class="col-md-4">
|
|
<div class="d-flex align-items-center">
|
|
<label class="form-label me-2 mb-0">Slice:</label>
|
|
<input type="range" class="form-range me-2" id="sliceSlider" min="1" max="1" value="1">
|
|
<span id="sliceInfo">1/1</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="d-flex align-items-center">
|
|
<label class="form-label me-2 mb-0">Window:</label>
|
|
<input type="range" class="form-range me-2" id="windowSlider" min="1" max="4000" value="400">
|
|
<span id="windowValue">400</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="d-flex align-items-center">
|
|
<label class="form-label me-2 mb-0">Level:</label>
|
|
<input type="range" class="form-range me-2" id="levelSlider" min="-1000" max="1000" value="40">
|
|
<span id="levelValue">40</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preferences Modal -->
|
|
<div class="modal fade" id="preferencesModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Viewer Preferences</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Default Window Preset</label>
|
|
<select class="form-select" id="defaultWindowPreset">
|
|
<option value="soft_tissue">Soft Tissue</option>
|
|
<option value="lung">Lung</option>
|
|
<option value="bone">Bone</option>
|
|
<option value="brain">Brain</option>
|
|
<option value="liver">Liver</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="autoPlay">
|
|
<label class="form-check-label" for="autoPlay">
|
|
Auto-play multi-frame series
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="showOverlays">
|
|
<label class="form-check-label" for="showOverlays">
|
|
Show DICOM overlays
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Interpolation</label>
|
|
<select class="form-select" id="interpolation">
|
|
<option value="nearest">Nearest Neighbor</option>
|
|
<option value="linear">Linear</option>
|
|
<option value="cubic">Cubic</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="savePreferences()">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Keyboard Shortcuts Modal -->
|
|
<div class="modal fade" id="shortcutsModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Keyboard Shortcuts</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<h6>Navigation</h6>
|
|
<ul class="list-unstyled">
|
|
<li><kbd>↑</kbd> <kbd>↓</kbd> Previous/Next slice</li>
|
|
<li><kbd>Page Up</kbd> <kbd>Page Down</kbd> Previous/Next series</li>
|
|
<li><kbd>Home</kbd> <kbd>End</kbd> First/Last slice</li>
|
|
<li><kbd>Space</kbd> Play/Pause</li>
|
|
</ul>
|
|
|
|
<h6>Zoom & Pan</h6>
|
|
<ul class="list-unstyled">
|
|
<li><kbd>+</kbd> <kbd>-</kbd> Zoom in/out</li>
|
|
<li><kbd>0</kbd> Reset zoom</li>
|
|
<li><kbd>Ctrl</kbd> + <kbd>Drag</kbd> Pan</li>
|
|
</ul>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6>Window/Level</h6>
|
|
<ul class="list-unstyled">
|
|
<li><kbd>W</kbd> Window width</li>
|
|
<li><kbd>L</kbd> Window level</li>
|
|
<li><kbd>I</kbd> Invert</li>
|
|
<li><kbd>R</kbd> Rotate</li>
|
|
</ul>
|
|
|
|
<h6>Tools</h6>
|
|
<ul class="list-unstyled">
|
|
<li><kbd>M</kbd> Measure</li>
|
|
<li><kbd>A</kbd> Annotate</li>
|
|
<li><kbd>F</kbd> Fullscreen</li>
|
|
<li><kbd>Esc</kbd> Exit fullscreen</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="{% static 'js/cornerstone.min.js' %}"></script>
|
|
<script src="{% static 'js/cornerstoneWADOImageLoader.min.js' %}"></script>
|
|
<script src="{% static 'js/cornerstoneTools.min.js' %}"></script>
|
|
|
|
<script>
|
|
let currentSeries = null;
|
|
let currentImageIndex = 0;
|
|
let isPlaying = false;
|
|
let playInterval = null;
|
|
let measurements = [];
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initializeDICOMViewer();
|
|
setupEventListeners();
|
|
|
|
{% if study %}
|
|
// Load first series if available
|
|
const firstThumbnail = document.querySelector('.thumbnail');
|
|
if (firstThumbnail) {
|
|
firstThumbnail.click();
|
|
}
|
|
{% endif %}
|
|
});
|
|
|
|
function initializeDICOMViewer() {
|
|
// Initialize Cornerstone
|
|
cornerstone.enable(document.getElementById('dicomCanvas'));
|
|
|
|
// Configure WADO Image Loader
|
|
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
|
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
|
|
|
|
// Initialize Cornerstone Tools
|
|
cornerstoneTools.external.cornerstone = cornerstone;
|
|
cornerstoneTools.external.Hammer = Hammer;
|
|
cornerstoneTools.init();
|
|
|
|
// Add tools
|
|
const element = document.getElementById('dicomCanvas');
|
|
cornerstoneTools.addTool(cornerstoneTools.ZoomTool);
|
|
cornerstoneTools.addTool(cornerstoneTools.PanTool);
|
|
cornerstoneTools.addTool(cornerstoneTools.LengthTool);
|
|
cornerstoneTools.addTool(cornerstoneTools.WwwcTool);
|
|
|
|
// Set default tool
|
|
cornerstoneTools.setToolActive('Wwwc', { mouseButtonMask: 1 });
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
// Toolbar buttons
|
|
document.getElementById('zoomInBtn').addEventListener('click', () => zoomIn());
|
|
document.getElementById('zoomOutBtn').addEventListener('click', () => zoomOut());
|
|
document.getElementById('resetZoomBtn').addEventListener('click', () => resetZoom());
|
|
document.getElementById('playBtn').addEventListener('click', () => togglePlay());
|
|
document.getElementById('prevBtn').addEventListener('click', () => previousImage());
|
|
document.getElementById('nextBtn').addEventListener('click', () => nextImage());
|
|
|
|
// Sliders
|
|
document.getElementById('sliceSlider').addEventListener('input', (e) => {
|
|
currentImageIndex = parseInt(e.target.value) - 1;
|
|
loadCurrentImage();
|
|
});
|
|
|
|
document.getElementById('windowSlider').addEventListener('input', (e) => {
|
|
updateWindowLevel();
|
|
});
|
|
|
|
document.getElementById('levelSlider').addEventListener('input', (e) => {
|
|
updateWindowLevel();
|
|
});
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', handleKeyboardShortcuts);
|
|
}
|
|
|
|
function loadSeries(seriesId) {
|
|
// Hide loading spinner and show it
|
|
document.getElementById('loadingSpinner').style.display = 'block';
|
|
|
|
// Update active thumbnail
|
|
document.querySelectorAll('.thumbnail').forEach(thumb => {
|
|
thumb.classList.remove('active');
|
|
});
|
|
document.querySelector(`[data-series-id="${seriesId}"]`).classList.add('active');
|
|
|
|
// Simulate loading series data
|
|
fetch(`/radiology/dicom/series/${seriesId}/`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
currentSeries = data;
|
|
currentImageIndex = 0;
|
|
updateSliceSlider();
|
|
loadCurrentImage();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading series:', error);
|
|
alert('Error loading DICOM series');
|
|
})
|
|
.finally(() => {
|
|
document.getElementById('loadingSpinner').style.display = 'none';
|
|
});
|
|
}
|
|
|
|
function loadCurrentImage() {
|
|
if (!currentSeries || !currentSeries.images) return;
|
|
|
|
const imageId = currentSeries.images[currentImageIndex];
|
|
const element = document.getElementById('dicomCanvas');
|
|
|
|
cornerstone.loadImage(imageId).then(image => {
|
|
cornerstone.displayImage(element, image);
|
|
updateImageInfo();
|
|
}).catch(error => {
|
|
console.error('Error loading image:', error);
|
|
});
|
|
}
|
|
|
|
function updateSliceSlider() {
|
|
if (!currentSeries) return;
|
|
|
|
const slider = document.getElementById('sliceSlider');
|
|
slider.max = currentSeries.images.length;
|
|
slider.value = currentImageIndex + 1;
|
|
|
|
updateImageInfo();
|
|
}
|
|
|
|
function updateImageInfo() {
|
|
if (!currentSeries) return;
|
|
|
|
const imageInfo = document.getElementById('imageInfo');
|
|
const sliceInfo = document.getElementById('sliceInfo');
|
|
|
|
imageInfo.textContent = `Image ${currentImageIndex + 1} of ${currentSeries.images.length}`;
|
|
sliceInfo.textContent = `${currentImageIndex + 1}/${currentSeries.images.length}`;
|
|
}
|
|
|
|
function updateWindowLevel() {
|
|
const windowValue = document.getElementById('windowSlider').value;
|
|
const levelValue = document.getElementById('levelSlider').value;
|
|
|
|
document.getElementById('windowValue').textContent = windowValue;
|
|
document.getElementById('levelValue').textContent = levelValue;
|
|
|
|
const element = document.getElementById('dicomCanvas');
|
|
const viewport = cornerstone.getViewport(element);
|
|
viewport.voi.windowWidth = parseInt(windowValue);
|
|
viewport.voi.windowCenter = parseInt(levelValue);
|
|
cornerstone.setViewport(element, viewport);
|
|
}
|
|
|
|
function zoomIn() {
|
|
const element = document.getElementById('dicomCanvas');
|
|
const viewport = cornerstone.getViewport(element);
|
|
viewport.scale += 0.25;
|
|
cornerstone.setViewport(element, viewport);
|
|
}
|
|
|
|
function zoomOut() {
|
|
const element = document.getElementById('dicomCanvas');
|
|
const viewport = cornerstone.getViewport(element);
|
|
viewport.scale = Math.max(0.25, viewport.scale - 0.25);
|
|
cornerstone.setViewport(element, viewport);
|
|
}
|
|
|
|
function resetZoom() {
|
|
const element = document.getElementById('dicomCanvas');
|
|
cornerstone.reset(element);
|
|
}
|
|
|
|
function togglePlay() {
|
|
if (isPlaying) {
|
|
stopPlay();
|
|
} else {
|
|
startPlay();
|
|
}
|
|
}
|
|
|
|
function startPlay() {
|
|
if (!currentSeries || currentSeries.images.length <= 1) return;
|
|
|
|
isPlaying = true;
|
|
document.getElementById('playBtn').innerHTML = '<i class="fas fa-pause"></i>';
|
|
|
|
playInterval = setInterval(() => {
|
|
nextImage();
|
|
}, 100); // 10 FPS
|
|
}
|
|
|
|
function stopPlay() {
|
|
isPlaying = false;
|
|
document.getElementById('playBtn').innerHTML = '<i class="fas fa-play"></i>';
|
|
|
|
if (playInterval) {
|
|
clearInterval(playInterval);
|
|
playInterval = null;
|
|
}
|
|
}
|
|
|
|
function previousImage() {
|
|
if (!currentSeries) return;
|
|
|
|
currentImageIndex = Math.max(0, currentImageIndex - 1);
|
|
document.getElementById('sliceSlider').value = currentImageIndex + 1;
|
|
loadCurrentImage();
|
|
}
|
|
|
|
function nextImage() {
|
|
if (!currentSeries) return;
|
|
|
|
currentImageIndex = Math.min(currentSeries.images.length - 1, currentImageIndex + 1);
|
|
|
|
// Loop back to beginning if playing
|
|
if (isPlaying && currentImageIndex === currentSeries.images.length - 1) {
|
|
currentImageIndex = 0;
|
|
}
|
|
|
|
document.getElementById('sliceSlider').value = currentImageIndex + 1;
|
|
loadCurrentImage();
|
|
}
|
|
|
|
function handleKeyboardShortcuts(event) {
|
|
switch(event.key) {
|
|
case 'ArrowUp':
|
|
event.preventDefault();
|
|
previousImage();
|
|
break;
|
|
case 'ArrowDown':
|
|
event.preventDefault();
|
|
nextImage();
|
|
break;
|
|
case ' ':
|
|
event.preventDefault();
|
|
togglePlay();
|
|
break;
|
|
case '+':
|
|
case '=':
|
|
event.preventDefault();
|
|
zoomIn();
|
|
break;
|
|
case '-':
|
|
event.preventDefault();
|
|
zoomOut();
|
|
break;
|
|
case '0':
|
|
event.preventDefault();
|
|
resetZoom();
|
|
break;
|
|
case 'f':
|
|
case 'F':
|
|
event.preventDefault();
|
|
toggleFullscreen();
|
|
break;
|
|
case 'Escape':
|
|
if (document.fullscreenElement) {
|
|
document.exitFullscreen();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
function toggleFullscreen() {
|
|
const viewer = document.getElementById('dicomViewer');
|
|
|
|
if (!document.fullscreenElement) {
|
|
viewer.requestFullscreen().catch(err => {
|
|
console.error('Error attempting to enable fullscreen:', err);
|
|
});
|
|
} else {
|
|
document.exitFullscreen();
|
|
}
|
|
}
|
|
|
|
function showPreferences() {
|
|
const modal = new bootstrap.Modal(document.getElementById('preferencesModal'));
|
|
modal.show();
|
|
}
|
|
|
|
function showKeyboardShortcuts() {
|
|
const modal = new bootstrap.Modal(document.getElementById('shortcutsModal'));
|
|
modal.show();
|
|
}
|
|
|
|
function savePreferences() {
|
|
// Save preferences to localStorage
|
|
const preferences = {
|
|
defaultWindowPreset: document.getElementById('defaultWindowPreset').value,
|
|
autoPlay: document.getElementById('autoPlay').checked,
|
|
showOverlays: document.getElementById('showOverlays').checked,
|
|
interpolation: document.getElementById('interpolation').value
|
|
};
|
|
|
|
localStorage.setItem('dicomViewerPreferences', JSON.stringify(preferences));
|
|
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('preferencesModal'));
|
|
modal.hide();
|
|
}
|
|
|
|
function exportImage() {
|
|
const element = document.getElementById('dicomCanvas');
|
|
const canvas = element.querySelector('canvas');
|
|
|
|
if (canvas) {
|
|
const link = document.createElement('a');
|
|
link.download = `dicom_image_${currentImageIndex + 1}.png`;
|
|
link.href = canvas.toDataURL();
|
|
link.click();
|
|
}
|
|
}
|
|
|
|
function printImage() {
|
|
const element = document.getElementById('dicomCanvas');
|
|
const canvas = element.querySelector('canvas');
|
|
|
|
if (canvas) {
|
|
const printWindow = window.open('', '_blank');
|
|
printWindow.document.write(`
|
|
<html>
|
|
<head><title>DICOM Image</title></head>
|
|
<body style="margin:0; text-align:center;">
|
|
<img src="${canvas.toDataURL()}" style="max-width:100%; max-height:100%;">
|
|
</body>
|
|
</html>
|
|
`);
|
|
printWindow.document.close();
|
|
printWindow.print();
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|