930 lines
36 KiB
HTML
930 lines
36 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}DICOM Image Viewer - Radiology{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="content">
|
|
<div class="container-fluid">
|
|
<!-- Page Header -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="page-header">
|
|
<div class="page-title">
|
|
<h4>DICOM Image Viewer</h4>
|
|
<h6>Advanced medical image viewing and analysis</h6>
|
|
</div>
|
|
<div class="page-btn">
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-primary" onclick="openStudy()">
|
|
<i class="fa fa-folder-open"></i> Open Study
|
|
</button>
|
|
<button type="button" class="btn btn-info" onclick="compareStudies()">
|
|
<i class="fa fa-columns"></i> Compare
|
|
</button>
|
|
<button type="button" class="btn btn-success" onclick="exportImages()">
|
|
<i class="fa fa-download"></i> Export
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Study Information -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">Study Information</h5>
|
|
<div class="card-tools">
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="toggleStudyInfo()">
|
|
<i class="fa fa-info-circle"></i> Study Info
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleSeriesList()">
|
|
<i class="fa fa-list"></i> Series
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-warning" onclick="toggleMeasurements()">
|
|
<i class="fa fa-ruler"></i> Measurements
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body" id="studyInfoPanel">
|
|
<div class="row">
|
|
<div class="col-md-3">
|
|
<strong>Patient:</strong> {{ study.patient_name|default:"John Smith" }}<br>
|
|
<strong>MRN:</strong> {{ study.patient_mrn|default:"123456" }}<br>
|
|
<strong>DOB:</strong> {{ study.patient_dob|default:"1980-05-15"|date:"M d, Y" }}<br>
|
|
<strong>Sex:</strong> {{ study.patient_sex|default:"M" }}
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>Study Date:</strong> {{ study.study_date|default:"Jan 15, 2024"|date:"M d, Y" }}<br>
|
|
<strong>Study Time:</strong> {{ study.study_time|default:"14:30" }}<br>
|
|
<strong>Accession #:</strong> {{ study.accession_number|default:"ACC123456" }}<br>
|
|
<strong>Study ID:</strong> {{ study.study_id|default:"STU001" }}
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>Modality:</strong> {{ study.modality|default:"CT" }}<br>
|
|
<strong>Body Part:</strong> {{ study.body_part|default:"Chest" }}<br>
|
|
<strong>Protocol:</strong> {{ study.protocol|default:"Chest CT with Contrast" }}<br>
|
|
<strong>Institution:</strong> {{ study.institution|default:"General Hospital" }}
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>Referring Physician:</strong> {{ study.referring_physician|default:"Dr. Johnson" }}<br>
|
|
<strong>Reading Radiologist:</strong> {{ study.radiologist|default:"Dr. Smith" }}<br>
|
|
<strong>Study Status:</strong> <span class="badge bg-success">{{ study.status|default:"Completed" }}</span><br>
|
|
<strong>Series Count:</strong> {{ study.series_count|default:3 }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Viewer Layout -->
|
|
<div class="row">
|
|
<!-- Series Navigator -->
|
|
<div class="col-md-2">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h6 class="card-title">Series Navigator</h6>
|
|
</div>
|
|
<div class="card-body p-2">
|
|
<div class="series-list" id="seriesList">
|
|
<div class="series-item active" onclick="loadSeries(1)">
|
|
<div class="series-thumbnail">
|
|
<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiBmaWxsPSIjZjBmMGYwIi8+CjxwYXRoIGQ9Ik0yMCAyMEg0NFY0NEgyMFYyMFoiIGZpbGw9IiM2NjY2NjYiLz4KPC9zdmc+" alt="Series 1">
|
|
</div>
|
|
<div class="series-info">
|
|
<small><strong>Series 1</strong></small><br>
|
|
<small class="text-muted">Axial</small><br>
|
|
<small class="text-muted">120 images</small>
|
|
</div>
|
|
</div>
|
|
<div class="series-item" onclick="loadSeries(2)">
|
|
<div class="series-thumbnail">
|
|
<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiBmaWxsPSIjZjBmMGYwIi8+CjxwYXRoIGQ9Ik0yMCAyMEg0NFY0NEgyMFYyMFoiIGZpbGw9IiM4ODg4ODgiLz4KPC9zdmc+" alt="Series 2">
|
|
</div>
|
|
<div class="series-info">
|
|
<small><strong>Series 2</strong></small><br>
|
|
<small class="text-muted">Coronal</small><br>
|
|
<small class="text-muted">80 images</small>
|
|
</div>
|
|
</div>
|
|
<div class="series-item" onclick="loadSeries(3)">
|
|
<div class="series-thumbnail">
|
|
<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiBmaWxsPSIjZjBmMGYwIi8+CjxwYXRoIGQ9Ik0yMCAyMEg0NFY0NEgyMFYyMFoiIGZpbGw9IiNhYWFhYWEiLz4KPC9zdmc+" alt="Series 3">
|
|
</div>
|
|
<div class="series-info">
|
|
<small><strong>Series 3</strong></small><br>
|
|
<small class="text-muted">Sagittal</small><br>
|
|
<small class="text-muted">60 images</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Image Navigator -->
|
|
<div class="card mt-3">
|
|
<div class="card-header">
|
|
<h6 class="card-title">Image Navigator</h6>
|
|
</div>
|
|
<div class="card-body p-2">
|
|
<div class="image-navigator">
|
|
<div class="navigator-controls mb-2">
|
|
<button class="btn btn-sm btn-outline-primary" onclick="firstImage()">
|
|
<i class="fa fa-step-backward"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="previousImage()">
|
|
<i class="fa fa-backward"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="playImages()" id="playBtn">
|
|
<i class="fa fa-play"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="nextImage()">
|
|
<i class="fa fa-forward"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="lastImage()">
|
|
<i class="fa fa-step-forward"></i>
|
|
</button>
|
|
</div>
|
|
<div class="image-slider mb-2">
|
|
<input type="range" class="form-range" id="imageSlider" min="1" max="120" value="60" onchange="goToImage(this.value)">
|
|
</div>
|
|
<div class="image-info text-center">
|
|
<small>Image <span id="currentImage">60</span> of <span id="totalImages">120</span></small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Viewport -->
|
|
<div class="col-md-8">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h6 class="card-title">Image Viewport</h6>
|
|
<div class="card-tools">
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="resetView()">
|
|
<i class="fa fa-home"></i> Reset
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="fitToWindow()">
|
|
<i class="fa fa-expand-arrows-alt"></i> Fit
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="actualSize()">
|
|
<i class="fa fa-search"></i> 1:1
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleFullscreen()">
|
|
<i class="fa fa-expand"></i> Fullscreen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="image-viewport" id="imageViewport">
|
|
<!-- Main DICOM Image Display -->
|
|
<div class="dicom-image-container">
|
|
<canvas id="dicomCanvas" width="800" height="600"></canvas>
|
|
|
|
<!-- Image Overlay Information -->
|
|
<div class="image-overlay top-left">
|
|
<div class="overlay-text">
|
|
<div>{{ study.patient_name|default:"Smith, John" }}</div>
|
|
<div>{{ study.patient_mrn|default:"MRN: 123456" }}</div>
|
|
<div>{{ study.study_date|default:"2024-01-15" }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="image-overlay top-right">
|
|
<div class="overlay-text">
|
|
<div>{{ study.institution|default:"General Hospital" }}</div>
|
|
<div>{{ study.modality|default:"CT" }} - {{ study.body_part|default:"Chest" }}</div>
|
|
<div>Series: <span id="currentSeries">1</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="image-overlay bottom-left">
|
|
<div class="overlay-text">
|
|
<div>W: <span id="windowWidth">400</span></div>
|
|
<div>L: <span id="windowLevel">40</span></div>
|
|
<div>Zoom: <span id="zoomLevel">100</span>%</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="image-overlay bottom-right">
|
|
<div class="overlay-text">
|
|
<div>Slice: <span id="slicePosition">60</span>/<span id="totalSlices">120</span></div>
|
|
<div>Thickness: <span id="sliceThickness">5.0</span>mm</div>
|
|
<div>Pixel: <span id="pixelValue">-</span> HU</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tools Panel -->
|
|
<div class="col-md-2">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h6 class="card-title">Tools</h6>
|
|
</div>
|
|
<div class="card-body p-2">
|
|
<div class="tool-group mb-3">
|
|
<h6 class="tool-group-title">Navigation</h6>
|
|
<div class="btn-group-vertical w-100">
|
|
<button class="btn btn-sm btn-outline-primary active" onclick="selectTool('pan')" data-tool="pan">
|
|
<i class="fa fa-hand-paper"></i> Pan
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="selectTool('zoom')" data-tool="zoom">
|
|
<i class="fa fa-search-plus"></i> Zoom
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="selectTool('window')" data-tool="window">
|
|
<i class="fa fa-adjust"></i> Window/Level
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tool-group mb-3">
|
|
<h6 class="tool-group-title">Measurements</h6>
|
|
<div class="btn-group-vertical w-100">
|
|
<button class="btn btn-sm btn-outline-success" onclick="selectTool('length')" data-tool="length">
|
|
<i class="fa fa-ruler"></i> Length
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-success" onclick="selectTool('angle')" data-tool="angle">
|
|
<i class="fa fa-drafting-compass"></i> Angle
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-success" onclick="selectTool('area')" data-tool="area">
|
|
<i class="fa fa-vector-square"></i> Area
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-success" onclick="selectTool('roi')" data-tool="roi">
|
|
<i class="fa fa-circle"></i> ROI
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tool-group mb-3">
|
|
<h6 class="tool-group-title">Annotations</h6>
|
|
<div class="btn-group-vertical w-100">
|
|
<button class="btn btn-sm btn-outline-warning" onclick="selectTool('arrow')" data-tool="arrow">
|
|
<i class="fa fa-arrow-right"></i> Arrow
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-warning" onclick="selectTool('text')" data-tool="text">
|
|
<i class="fa fa-font"></i> Text
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-warning" onclick="selectTool('freehand')" data-tool="freehand">
|
|
<i class="fa fa-pencil-alt"></i> Freehand
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tool-group">
|
|
<h6 class="tool-group-title">Actions</h6>
|
|
<div class="btn-group-vertical w-100">
|
|
<button class="btn btn-sm btn-outline-danger" onclick="clearAnnotations()">
|
|
<i class="fa fa-eraser"></i> Clear All
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-info" onclick="saveAnnotations()">
|
|
<i class="fa fa-save"></i> Save
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Window/Level Presets -->
|
|
<div class="card mt-3">
|
|
<div class="card-header">
|
|
<h6 class="card-title">W/L Presets</h6>
|
|
</div>
|
|
<div class="card-body p-2">
|
|
<div class="preset-buttons">
|
|
<button class="btn btn-sm btn-outline-secondary w-100 mb-1" onclick="applyPreset('soft_tissue')">
|
|
Soft Tissue
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-secondary w-100 mb-1" onclick="applyPreset('lung')">
|
|
Lung
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-secondary w-100 mb-1" onclick="applyPreset('bone')">
|
|
Bone
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-secondary w-100 mb-1" onclick="applyPreset('brain')">
|
|
Brain
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-secondary w-100 mb-1" onclick="applyPreset('liver')">
|
|
Liver
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-secondary w-100" onclick="applyPreset('mediastinum')">
|
|
Mediastinum
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Measurements Panel -->
|
|
<div class="row" id="measurementsPanel" style="display: none;">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">Measurements & Annotations</h5>
|
|
<div class="card-tools">
|
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="exportMeasurements()">
|
|
<i class="fa fa-download"></i> Export
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-sm" id="measurementsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Type</th>
|
|
<th>Series</th>
|
|
<th>Image</th>
|
|
<th>Value</th>
|
|
<th>Description</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><span class="badge bg-success">Length</span></td>
|
|
<td>1</td>
|
|
<td>45</td>
|
|
<td>23.4 mm</td>
|
|
<td>Nodule diameter</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="goToMeasurement(1)">
|
|
<i class="fa fa-eye"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger" onclick="deleteMeasurement(1)">
|
|
<i class="fa fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td><span class="badge bg-success">Area</span></td>
|
|
<td>1</td>
|
|
<td>52</td>
|
|
<td>145.2 mm²</td>
|
|
<td>Lesion area</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="goToMeasurement(2)">
|
|
<i class="fa fa-eye"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger" onclick="deleteMeasurement(2)">
|
|
<i class="fa fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let currentTool = 'pan';
|
|
let currentSeries = 1;
|
|
let currentImage = 60;
|
|
let totalImages = 120;
|
|
let isPlaying = false;
|
|
let playInterval;
|
|
let zoomLevel = 100;
|
|
let windowWidth = 400;
|
|
let windowLevel = 40;
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initializeViewer();
|
|
setupCanvasEvents();
|
|
});
|
|
|
|
function initializeViewer() {
|
|
const canvas = document.getElementById('dicomCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Draw placeholder DICOM image
|
|
drawPlaceholderImage(ctx);
|
|
|
|
// Update UI elements
|
|
updateImageInfo();
|
|
}
|
|
|
|
function drawPlaceholderImage(ctx) {
|
|
const canvas = ctx.canvas;
|
|
|
|
// Clear canvas
|
|
ctx.fillStyle = '#000000';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Draw simulated medical image
|
|
ctx.fillStyle = '#333333';
|
|
ctx.fillRect(100, 100, 600, 400);
|
|
|
|
// Add some simulated anatomical structures
|
|
ctx.fillStyle = '#666666';
|
|
ctx.beginPath();
|
|
ctx.arc(400, 300, 80, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
|
|
ctx.fillStyle = '#999999';
|
|
ctx.beginPath();
|
|
ctx.arc(350, 250, 30, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(450, 350, 25, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
|
|
// Add grid lines for reference
|
|
ctx.strokeStyle = '#222222';
|
|
ctx.lineWidth = 1;
|
|
for (let i = 0; i < canvas.width; i += 50) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(i, 0);
|
|
ctx.lineTo(i, canvas.height);
|
|
ctx.stroke();
|
|
}
|
|
for (let i = 0; i < canvas.height; i += 50) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, i);
|
|
ctx.lineTo(canvas.width, i);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
function setupCanvasEvents() {
|
|
const canvas = document.getElementById('dicomCanvas');
|
|
|
|
canvas.addEventListener('mousedown', handleMouseDown);
|
|
canvas.addEventListener('mousemove', handleMouseMove);
|
|
canvas.addEventListener('mouseup', handleMouseUp);
|
|
canvas.addEventListener('wheel', handleWheel);
|
|
canvas.addEventListener('contextmenu', e => e.preventDefault());
|
|
}
|
|
|
|
function handleMouseDown(e) {
|
|
console.log(`Mouse down with tool: ${currentTool}`);
|
|
// Tool-specific mouse down handling would go here
|
|
}
|
|
|
|
function handleMouseMove(e) {
|
|
const rect = e.target.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
|
|
// Update pixel value display (simulated)
|
|
const pixelValue = Math.floor(Math.random() * 200 - 100);
|
|
document.getElementById('pixelValue').textContent = pixelValue;
|
|
}
|
|
|
|
function handleMouseUp(e) {
|
|
console.log(`Mouse up with tool: ${currentTool}`);
|
|
// Tool-specific mouse up handling would go here
|
|
}
|
|
|
|
function handleWheel(e) {
|
|
e.preventDefault();
|
|
|
|
if (e.deltaY > 0) {
|
|
nextImage();
|
|
} else {
|
|
previousImage();
|
|
}
|
|
}
|
|
|
|
function loadSeries(seriesNumber) {
|
|
currentSeries = seriesNumber;
|
|
|
|
// Update active series in navigator
|
|
document.querySelectorAll('.series-item').forEach(item => {
|
|
item.classList.remove('active');
|
|
});
|
|
document.querySelectorAll('.series-item')[seriesNumber - 1].classList.add('active');
|
|
|
|
// Update series info
|
|
document.getElementById('currentSeries').textContent = seriesNumber;
|
|
|
|
// Update image count based on series
|
|
const imageCounts = [120, 80, 60];
|
|
totalImages = imageCounts[seriesNumber - 1];
|
|
currentImage = Math.floor(totalImages / 2);
|
|
|
|
document.getElementById('totalImages').textContent = totalImages;
|
|
document.getElementById('totalSlices').textContent = totalImages;
|
|
document.getElementById('imageSlider').max = totalImages;
|
|
document.getElementById('imageSlider').value = currentImage;
|
|
|
|
updateImageInfo();
|
|
|
|
console.log(`Loaded series ${seriesNumber}`);
|
|
}
|
|
|
|
function selectTool(toolName) {
|
|
currentTool = toolName;
|
|
|
|
// Update active tool button
|
|
document.querySelectorAll('[data-tool]').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
document.querySelector(`[data-tool="${toolName}"]`).classList.add('active');
|
|
|
|
// Change cursor based on tool
|
|
const canvas = document.getElementById('dicomCanvas');
|
|
const cursors = {
|
|
'pan': 'grab',
|
|
'zoom': 'zoom-in',
|
|
'window': 'crosshair',
|
|
'length': 'crosshair',
|
|
'angle': 'crosshair',
|
|
'area': 'crosshair',
|
|
'roi': 'crosshair',
|
|
'arrow': 'crosshair',
|
|
'text': 'text',
|
|
'freehand': 'crosshair'
|
|
};
|
|
|
|
canvas.style.cursor = cursors[toolName] || 'default';
|
|
|
|
console.log(`Selected tool: ${toolName}`);
|
|
}
|
|
|
|
function firstImage() {
|
|
currentImage = 1;
|
|
updateImageInfo();
|
|
console.log('Moved to first image');
|
|
}
|
|
|
|
function previousImage() {
|
|
if (currentImage > 1) {
|
|
currentImage--;
|
|
updateImageInfo();
|
|
}
|
|
}
|
|
|
|
function nextImage() {
|
|
if (currentImage < totalImages) {
|
|
currentImage++;
|
|
updateImageInfo();
|
|
}
|
|
}
|
|
|
|
function lastImage() {
|
|
currentImage = totalImages;
|
|
updateImageInfo();
|
|
console.log('Moved to last image');
|
|
}
|
|
|
|
function goToImage(imageNumber) {
|
|
currentImage = parseInt(imageNumber);
|
|
updateImageInfo();
|
|
}
|
|
|
|
function playImages() {
|
|
const playBtn = document.getElementById('playBtn');
|
|
|
|
if (isPlaying) {
|
|
clearInterval(playInterval);
|
|
playBtn.innerHTML = '<i class="fa fa-play"></i>';
|
|
isPlaying = false;
|
|
} else {
|
|
playInterval = setInterval(() => {
|
|
if (currentImage < totalImages) {
|
|
nextImage();
|
|
} else {
|
|
currentImage = 1;
|
|
updateImageInfo();
|
|
}
|
|
}, 200);
|
|
playBtn.innerHTML = '<i class="fa fa-pause"></i>';
|
|
isPlaying = true;
|
|
}
|
|
}
|
|
|
|
function updateImageInfo() {
|
|
document.getElementById('currentImage').textContent = currentImage;
|
|
document.getElementById('slicePosition').textContent = currentImage;
|
|
document.getElementById('imageSlider').value = currentImage;
|
|
|
|
// Simulate image loading
|
|
const ctx = document.getElementById('dicomCanvas').getContext('2d');
|
|
drawPlaceholderImage(ctx);
|
|
}
|
|
|
|
function resetView() {
|
|
zoomLevel = 100;
|
|
document.getElementById('zoomLevel').textContent = zoomLevel;
|
|
console.log('View reset to default');
|
|
}
|
|
|
|
function fitToWindow() {
|
|
zoomLevel = 85;
|
|
document.getElementById('zoomLevel').textContent = zoomLevel;
|
|
console.log('Image fitted to window');
|
|
}
|
|
|
|
function actualSize() {
|
|
zoomLevel = 100;
|
|
document.getElementById('zoomLevel').textContent = zoomLevel;
|
|
console.log('Image set to actual size');
|
|
}
|
|
|
|
function toggleFullscreen() {
|
|
const viewport = document.getElementById('imageViewport');
|
|
|
|
if (!document.fullscreenElement) {
|
|
viewport.requestFullscreen().then(() => {
|
|
console.log('Entered fullscreen mode');
|
|
});
|
|
} else {
|
|
document.exitFullscreen().then(() => {
|
|
console.log('Exited fullscreen mode');
|
|
});
|
|
}
|
|
}
|
|
|
|
function applyPreset(presetName) {
|
|
const presets = {
|
|
'soft_tissue': { width: 400, level: 40 },
|
|
'lung': { width: 1500, level: -600 },
|
|
'bone': { width: 1800, level: 400 },
|
|
'brain': { width: 80, level: 40 },
|
|
'liver': { width: 150, level: 30 },
|
|
'mediastinum': { width: 350, level: 50 }
|
|
};
|
|
|
|
const preset = presets[presetName];
|
|
if (preset) {
|
|
windowWidth = preset.width;
|
|
windowLevel = preset.level;
|
|
|
|
document.getElementById('windowWidth').textContent = windowWidth;
|
|
document.getElementById('windowLevel').textContent = windowLevel;
|
|
|
|
console.log(`Applied ${presetName} preset: W=${windowWidth}, L=${windowLevel}`);
|
|
}
|
|
}
|
|
|
|
function toggleStudyInfo() {
|
|
const panel = document.getElementById('studyInfoPanel');
|
|
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
|
}
|
|
|
|
function toggleSeriesList() {
|
|
const panel = document.getElementById('seriesList');
|
|
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
|
}
|
|
|
|
function toggleMeasurements() {
|
|
const panel = document.getElementById('measurementsPanel');
|
|
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
|
}
|
|
|
|
function clearAnnotations() {
|
|
if (confirm('Clear all annotations and measurements?')) {
|
|
console.log('All annotations cleared');
|
|
alert('All annotations and measurements have been cleared.');
|
|
}
|
|
}
|
|
|
|
function saveAnnotations() {
|
|
console.log('Saving annotations...');
|
|
alert('Annotations and measurements saved successfully.');
|
|
}
|
|
|
|
function goToMeasurement(measurementId) {
|
|
console.log(`Navigating to measurement ${measurementId}`);
|
|
alert(`Navigating to measurement ${measurementId}`);
|
|
}
|
|
|
|
function deleteMeasurement(measurementId) {
|
|
if (confirm('Delete this measurement?')) {
|
|
console.log(`Deleting measurement ${measurementId}`);
|
|
alert(`Measurement ${measurementId} deleted.`);
|
|
}
|
|
}
|
|
|
|
function exportMeasurements() {
|
|
console.log('Exporting measurements...');
|
|
alert('Measurements exported to CSV file.');
|
|
}
|
|
|
|
function openStudy() {
|
|
console.log('Opening study browser...');
|
|
alert('Study browser would open here to select a different study.');
|
|
}
|
|
|
|
function compareStudies() {
|
|
console.log('Opening study comparison...');
|
|
alert('Study comparison interface would open here.');
|
|
}
|
|
|
|
function exportImages() {
|
|
const format = prompt('Export format:\n1. DICOM\n2. JPEG\n3. PNG\n4. TIFF\n\nEnter number:');
|
|
|
|
if (format && ['1', '2', '3', '4'].includes(format)) {
|
|
const formats = {
|
|
'1': 'DICOM',
|
|
'2': 'JPEG',
|
|
'3': 'PNG',
|
|
'4': 'TIFF'
|
|
};
|
|
|
|
console.log(`Exporting images as: ${formats[format]}`);
|
|
alert(`Images exported as ${formats[format]} format.`);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.image-viewport {
|
|
background-color: #000;
|
|
min-height: 600px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.dicom-image-container {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 600px;
|
|
}
|
|
|
|
#dicomCanvas {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.image-overlay {
|
|
position: absolute;
|
|
color: #00ff00;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
background-color: rgba(0, 0, 0, 0.7);
|
|
padding: 5px;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.image-overlay.top-left {
|
|
top: 10px;
|
|
left: 10px;
|
|
}
|
|
|
|
.image-overlay.top-right {
|
|
top: 10px;
|
|
right: 10px;
|
|
}
|
|
|
|
.image-overlay.bottom-left {
|
|
bottom: 10px;
|
|
left: 10px;
|
|
}
|
|
|
|
.image-overlay.bottom-right {
|
|
bottom: 10px;
|
|
right: 10px;
|
|
}
|
|
|
|
.overlay-text div {
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.series-list {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.series-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 8px;
|
|
margin-bottom: 8px;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.series-item:hover {
|
|
background-color: #f8f9fa;
|
|
border-color: #007bff;
|
|
}
|
|
|
|
.series-item.active {
|
|
background-color: #e3f2fd;
|
|
border-color: #007bff;
|
|
}
|
|
|
|
.series-thumbnail {
|
|
width: 40px;
|
|
height: 40px;
|
|
margin-right: 8px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 2px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.series-thumbnail img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.series-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.image-navigator {
|
|
text-align: center;
|
|
}
|
|
|
|
.navigator-controls {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.navigator-controls .btn {
|
|
flex: 1;
|
|
margin: 0 1px;
|
|
}
|
|
|
|
.tool-group {
|
|
border-bottom: 1px solid #dee2e6;
|
|
padding-bottom: 1rem;
|
|
}
|
|
|
|
.tool-group:last-child {
|
|
border-bottom: none;
|
|
padding-bottom: 0;
|
|
}
|
|
|
|
.tool-group-title {
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.btn-group-vertical .btn {
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.btn-group-vertical .btn:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.preset-buttons .btn {
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.image-overlay {
|
|
font-size: 10px;
|
|
padding: 3px;
|
|
}
|
|
|
|
.series-item {
|
|
flex-direction: column;
|
|
text-align: center;
|
|
}
|
|
|
|
.series-thumbnail {
|
|
margin-right: 0;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.navigator-controls .btn {
|
|
padding: 0.25rem 0.5rem;
|
|
font-size: 0.75rem;
|
|
}
|
|
}
|
|
|
|
/* Fullscreen styles */
|
|
.image-viewport:fullscreen {
|
|
background-color: #000;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.image-viewport:fullscreen #dicomCanvas {
|
|
max-width: 100vw;
|
|
max-height: 100vh;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|