Marwan Alwali 0a037d3d9d update
2025-09-01 11:26:11 +03:00

1550 lines
46 KiB
HTML

{% extends 'base.html' %}
{% load static %}
{% block title %}DICOM Analysis - {{ file.filename }}{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<style>
.analysis-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 0.5rem;
padding: 2rem;
margin-bottom: 2rem;
}
.analysis-layout {
display: grid;
grid-template-columns: 1fr 350px;
gap: 2rem;
margin-bottom: 2rem;
}
.viewer-section {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
overflow: hidden;
position: relative;
}
.viewer-header {
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
padding: 1rem 1.5rem;
display: flex;
justify-content: between;
align-items: center;
}
.viewer-controls {
display: flex;
gap: 0.5rem;
align-items: center;
}
.control-group {
display: flex;
gap: 0.25rem;
align-items: center;
padding: 0.25rem 0.5rem;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
}
.control-btn {
padding: 0.375rem 0.5rem;
border: none;
background: transparent;
border-radius: 0.25rem;
cursor: pointer;
transition: all 0.2s;
color: #6c757d;
}
.control-btn:hover, .control-btn.active {
background: #007bff;
color: white;
}
.viewer-canvas {
position: relative;
background: #000;
min-height: 500px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.dicom-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
cursor: crosshair;
}
.image-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
.measurement-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
.measurement-line {
stroke: #00ff00;
stroke-width: 2;
fill: none;
}
.measurement-text {
fill: #00ff00;
font-size: 12px;
font-family: Arial, sans-serif;
}
.image-info-overlay {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-family: monospace;
}
.windowing-controls {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 0.5rem;
border-radius: 0.25rem;
display: flex;
gap: 1rem;
align-items: center;
}
.windowing-slider {
width: 100px;
}
.analysis-panel {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.panel-section {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
overflow: hidden;
}
.section-header {
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
padding: 1rem 1.5rem;
font-weight: 600;
color: #495057;
display: flex;
justify-content: between;
align-items: center;
}
.section-content {
padding: 1.5rem;
}
.analysis-tabs {
border-bottom: 1px solid #dee2e6;
margin-bottom: 1rem;
}
.nav-tabs .nav-link {
border: none;
border-bottom: 2px solid transparent;
color: #6c757d;
font-weight: 600;
padding: 0.75rem 1rem;
font-size: 0.875rem;
}
.nav-tabs .nav-link.active {
color: #007bff;
border-bottom-color: #007bff;
background: none;
}
.measurement-item {
display: flex;
justify-content: between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #f1f3f4;
}
.measurement-item:last-child {
border-bottom: none;
}
.measurement-label {
font-weight: 600;
color: #495057;
font-size: 0.875rem;
}
.measurement-value {
color: #28a745;
font-weight: bold;
font-size: 0.875rem;
}
.measurement-actions {
display: flex;
gap: 0.25rem;
}
.btn-measurement {
padding: 0.25rem 0.5rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.75rem;
}
.btn-edit { background: #fff3e0; color: #f57c00; }
.btn-delete { background: #ffebee; color: #d32f2f; }
.histogram-container {
width: 100%;
height: 200px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
position: relative;
}
.histogram-canvas {
width: 100%;
height: 100%;
}
.roi-list {
max-height: 200px;
overflow-y: auto;
}
.roi-item {
display: flex;
justify-content: between;
align-items: center;
padding: 0.75rem;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
margin-bottom: 0.5rem;
background: #f8f9fa;
}
.roi-info {
flex: 1;
}
.roi-name {
font-weight: 600;
color: #495057;
margin-bottom: 0.25rem;
}
.roi-stats {
font-size: 0.75rem;
color: #6c757d;
}
.roi-actions {
display: flex;
gap: 0.25rem;
}
.analysis-results {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
padding: 1rem;
margin-bottom: 1rem;
}
.result-item {
display: flex;
justify-content: between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #dee2e6;
}
.result-item:last-child {
border-bottom: none;
}
.result-label {
font-weight: 600;
color: #495057;
}
.result-value {
color: #007bff;
font-weight: bold;
}
.tool-palette {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
margin-bottom: 1rem;
}
.tool-btn {
padding: 0.75rem;
border: 1px solid #dee2e6;
background: white;
border-radius: 0.25rem;
cursor: pointer;
transition: all 0.2s;
text-align: center;
font-size: 0.75rem;
color: #495057;
}
.tool-btn:hover, .tool-btn.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.tool-icon {
font-size: 1.25rem;
margin-bottom: 0.25rem;
display: block;
}
.export-options {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
}
.export-btn {
padding: 0.75rem;
border: 1px solid #dee2e6;
background: white;
border-radius: 0.25rem;
cursor: pointer;
transition: all 0.2s;
text-align: center;
font-size: 0.875rem;
color: #495057;
}
.export-btn:hover {
background: #f8f9fa;
border-color: #007bff;
}
.progress-indicator {
background: #e9ecef;
border-radius: 0.25rem;
height: 4px;
overflow: hidden;
margin-top: 0.5rem;
}
.progress-bar {
background: #007bff;
height: 100%;
transition: width 0.3s ease;
}
.annotation-list {
max-height: 150px;
overflow-y: auto;
}
.annotation-item {
padding: 0.5rem;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
margin-bottom: 0.5rem;
background: white;
font-size: 0.875rem;
}
.annotation-text {
color: #495057;
margin-bottom: 0.25rem;
}
.annotation-meta {
font-size: 0.75rem;
color: #6c757d;
}
@media (max-width: 1200px) {
.analysis-layout {
grid-template-columns: 1fr;
gap: 1.5rem;
}
}
@media (max-width: 768px) {
.analysis-header {
padding: 1.5rem;
}
.viewer-controls {
flex-wrap: wrap;
gap: 0.25rem;
}
.control-group {
flex-direction: column;
gap: 0.25rem;
}
.tool-palette {
grid-template-columns: repeat(2, 1fr);
}
.windowing-controls {
flex-direction: column;
gap: 0.5rem;
}
}
@media print {
.analysis-panel, .viewer-controls, .btn {
display: none !important;
}
.analysis-layout {
grid-template-columns: 1fr;
}
.section-header {
background: none;
border-bottom: 2px solid #000;
color: #000;
}
}
</style>
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<!-- Page Header -->
<div class="d-flex align-items-center mb-3">
<div>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'radiology:dashboard' %}">Radiology</a></li>
<li class="breadcrumb-item"><a href="{% url 'radiology:dicom_file_list' %}">DICOM Files</a></li>
<li class="breadcrumb-item active">Analysis</li>
</ol>
<h1 class="page-header mb-0">
<i class="fas fa-search me-2"></i>DICOM Analysis
</h1>
</div>
<div class="ms-auto">
<button type="button" class="btn btn-outline-secondary me-2" onclick="resetAnalysis()">
<i class="fas fa-undo me-1"></i>Reset
</button>
<button type="button" class="btn btn-outline-info me-2" onclick="saveAnalysis()">
<i class="fas fa-save me-1"></i>Save Analysis
</button>
<button type="button" class="btn btn-outline-success me-2" onclick="generateReport()">
<i class="fas fa-file-alt me-1"></i>Generate Report
</button>
<button type="button" class="btn btn-primary" onclick="exportResults()">
<i class="fas fa-download me-1"></i>Export Results
</button>
</div>
</div>
<!-- Analysis Header -->
<div class="analysis-header">
<div class="row align-items-center">
<div class="col-md-8">
<h3 class="mb-2">{{ file.filename }}</h3>
<p class="mb-2">{{ file.patient_name|default:"Unknown Patient" }} - {{ file.study_description|default:"No description" }}</p>
<div class="d-flex align-items-center gap-3">
<span class="badge bg-primary">{{ file.modality|default:"Unknown" }}</span>
<span class="badge bg-info">{{ file.rows|default:0 }}x{{ file.columns|default:0 }}</span>
<span class="badge bg-success">{{ file.file_size|filesizeformat }}</span>
</div>
</div>
<div class="col-md-4 text-md-end">
<div class="text-white-50 mb-1">Analysis Started</div>
<div class="h6 mb-2" id="analysis-time">{{ analysis.started_at|date:"M d, Y g:i A"|default:"Just now" }}</div>
<div class="text-white-50">Status: <span id="analysis-status">In Progress</span></div>
</div>
</div>
</div>
<div class="analysis-layout">
<!-- DICOM Viewer -->
<div class="viewer-section">
<div class="viewer-header">
<div class="d-flex align-items-center gap-2">
<i class="fas fa-eye me-2"></i>
<span class="fw-bold">DICOM Viewer</span>
<span class="badge bg-secondary" id="zoom-level">100%</span>
</div>
<div class="viewer-controls">
<!-- Zoom Controls -->
<div class="control-group">
<button type="button" class="control-btn" onclick="zoomIn()" title="Zoom In">
<i class="fas fa-search-plus"></i>
</button>
<button type="button" class="control-btn" onclick="zoomOut()" title="Zoom Out">
<i class="fas fa-search-minus"></i>
</button>
<button type="button" class="control-btn" onclick="resetZoom()" title="Reset Zoom">
<i class="fas fa-expand-arrows-alt"></i>
</button>
</div>
<!-- Pan Controls -->
<div class="control-group">
<button type="button" class="control-btn active" onclick="setTool('pan')" title="Pan">
<i class="fas fa-hand-paper"></i>
</button>
<button type="button" class="control-btn" onclick="setTool('measure')" title="Measure">
<i class="fas fa-ruler"></i>
</button>
<button type="button" class="control-btn" onclick="setTool('roi')" title="ROI">
<i class="fas fa-draw-polygon"></i>
</button>
</div>
<!-- Window/Level -->
<div class="control-group">
<button type="button" class="control-btn" onclick="autoWindow()" title="Auto Window">
<i class="fas fa-adjust"></i>
</button>
<button type="button" class="control-btn" onclick="invertImage()" title="Invert">
<i class="fas fa-adjust"></i>
</button>
</div>
<!-- Presets -->
<div class="control-group">
<select class="form-select form-select-sm" onchange="applyPreset(this.value)">
<option value="">Window Presets</option>
<option value="lung">Lung</option>
<option value="bone">Bone</option>
<option value="soft_tissue">Soft Tissue</option>
<option value="brain">Brain</option>
<option value="liver">Liver</option>
</select>
</div>
</div>
</div>
<div class="viewer-canvas" id="viewer-canvas">
{% if file.thumbnail %}
<img src="{{ file.thumbnail.url }}" alt="DICOM Image" class="dicom-image" id="dicom-image">
{% else %}
<div class="text-center text-muted">
<i class="fas fa-file-medical fa-3x mb-3"></i>
<h5>Loading DICOM Image...</h5>
</div>
{% endif %}
<!-- Image Info Overlay -->
<div class="image-info-overlay" id="image-info">
<div>Patient: {{ file.patient_name|default:"Unknown" }}</div>
<div>Study: {{ file.study_date|date:"M d, Y"|default:"N/A" }}</div>
<div>Series: {{ file.series_number|default:"N/A" }}</div>
<div>Instance: {{ file.instance_number|default:"N/A" }}</div>
</div>
<!-- Windowing Controls -->
<div class="windowing-controls">
<div>
<label class="form-label mb-1">Window</label>
<input type="range" class="windowing-slider" id="window-width"
min="1" max="4000" value="400" onchange="updateWindowing()">
<div class="text-center" id="window-value">400</div>
</div>
<div>
<label class="form-label mb-1">Level</label>
<input type="range" class="windowing-slider" id="window-level"
min="-1000" max="3000" value="40" onchange="updateWindowing()">
<div class="text-center" id="level-value">40</div>
</div>
</div>
<!-- Measurement Overlay -->
<svg class="measurement-overlay" id="measurement-overlay">
<!-- Measurements will be drawn here -->
</svg>
</div>
</div>
<!-- Analysis Panel -->
<div class="analysis-panel">
<!-- Tools -->
<div class="panel-section">
<div class="section-header">
<i class="fas fa-tools me-2"></i>Analysis Tools
</div>
<div class="section-content">
<div class="tool-palette">
<button type="button" class="tool-btn active" onclick="selectTool('pointer')" data-tool="pointer">
<i class="fas fa-mouse-pointer tool-icon"></i>
Pointer
</button>
<button type="button" class="tool-btn" onclick="selectTool('measure')" data-tool="measure">
<i class="fas fa-ruler tool-icon"></i>
Measure
</button>
<button type="button" class="tool-btn" onclick="selectTool('angle')" data-tool="angle">
<i class="fas fa-drafting-compass tool-icon"></i>
Angle
</button>
<button type="button" class="tool-btn" onclick="selectTool('roi')" data-tool="roi">
<i class="fas fa-draw-polygon tool-icon"></i>
ROI
</button>
<button type="button" class="tool-btn" onclick="selectTool('ellipse')" data-tool="ellipse">
<i class="fas fa-circle tool-icon"></i>
Ellipse
</button>
<button type="button" class="tool-btn" onclick="selectTool('annotation')" data-tool="annotation">
<i class="fas fa-comment tool-icon"></i>
Annotate
</button>
</div>
</div>
</div>
<!-- Measurements -->
<div class="panel-section">
<div class="section-header">
<div>
<i class="fas fa-ruler me-2"></i>Measurements
</div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="clearMeasurements()">
<i class="fas fa-trash me-1"></i>Clear All
</button>
</div>
<div class="section-content">
<div id="measurements-list">
<!-- Measurements will be populated here -->
<div class="text-muted text-center py-3">
<i class="fas fa-ruler fa-2x mb-2"></i>
<div>No measurements yet</div>
<small>Use the measurement tools to add measurements</small>
</div>
</div>
</div>
</div>
<!-- ROI Analysis -->
<div class="panel-section">
<div class="section-header">
<div>
<i class="fas fa-chart-area me-2"></i>ROI Analysis
</div>
<button type="button" class="btn btn-outline-success btn-sm" onclick="analyzeROIs()">
<i class="fas fa-play me-1"></i>Analyze
</button>
</div>
<div class="section-content">
<div class="roi-list" id="roi-list">
<!-- ROIs will be populated here -->
<div class="text-muted text-center py-3">
<i class="fas fa-draw-polygon fa-2x mb-2"></i>
<div>No ROIs defined</div>
<small>Draw regions of interest for analysis</small>
</div>
</div>
</div>
</div>
<!-- Analysis Results -->
<div class="panel-section">
<div class="section-header">
<i class="fas fa-chart-bar me-2"></i>Analysis Results
</div>
<div class="section-content">
<ul class="nav nav-tabs analysis-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#statistics-tab">Statistics</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#histogram-tab">Histogram</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#annotations-tab">Notes</a>
</li>
</ul>
<div class="tab-content">
<!-- Statistics Tab -->
<div class="tab-pane fade show active" id="statistics-tab">
<div class="analysis-results" id="statistics-results">
<div class="result-item">
<div class="result-label">Mean Intensity:</div>
<div class="result-value" id="mean-intensity">-</div>
</div>
<div class="result-item">
<div class="result-label">Standard Deviation:</div>
<div class="result-value" id="std-deviation">-</div>
</div>
<div class="result-item">
<div class="result-label">Min/Max Values:</div>
<div class="result-value" id="min-max-values">-</div>
</div>
<div class="result-item">
<div class="result-label">Area (pixels):</div>
<div class="result-value" id="area-pixels">-</div>
</div>
<div class="result-item">
<div class="result-label">Area (mm²):</div>
<div class="result-value" id="area-mm">-</div>
</div>
</div>
</div>
<!-- Histogram Tab -->
<div class="tab-pane fade" id="histogram-tab">
<div class="histogram-container">
<canvas class="histogram-canvas" id="histogram-canvas"></canvas>
</div>
<div class="mt-2">
<small class="text-muted">Pixel intensity distribution</small>
</div>
</div>
<!-- Annotations Tab -->
<div class="tab-pane fade" id="annotations-tab">
<div class="mb-3">
<textarea class="form-control" id="new-annotation" rows="2"
placeholder="Add a note or annotation..."></textarea>
<button type="button" class="btn btn-primary btn-sm mt-2" onclick="addAnnotation()">
<i class="fas fa-plus me-1"></i>Add Note
</button>
</div>
<div class="annotation-list" id="annotation-list">
<!-- Annotations will be populated here -->
</div>
</div>
</div>
</div>
</div>
<!-- Export Options -->
<div class="panel-section">
<div class="section-header">
<i class="fas fa-download me-2"></i>Export Options
</div>
<div class="section-content">
<div class="export-options">
<button type="button" class="export-btn" onclick="exportImage()">
<i class="fas fa-image mb-2"></i><br>
Export Image
</button>
<button type="button" class="export-btn" onclick="exportMeasurements()">
<i class="fas fa-table mb-2"></i><br>
Export Data
</button>
<button type="button" class="export-btn" onclick="exportReport()">
<i class="fas fa-file-pdf mb-2"></i><br>
PDF Report
</button>
<button type="button" class="export-btn" onclick="exportDICOM()">
<i class="fas fa-file-medical mb-2"></i><br>
DICOM SR
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Measurement Details Modal -->
<div class="modal fade" id="measurementModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-ruler me-2"></i>Measurement Details
</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">Label</label>
<input type="text" class="form-control" id="measurement-label" placeholder="Enter measurement label">
</div>
<div class="mb-3">
<label class="form-label">Notes</label>
<textarea class="form-control" id="measurement-notes" rows="3"
placeholder="Add notes about this measurement"></textarea>
</div>
<div class="row">
<div class="col-md-6">
<label class="form-label">Length (pixels)</label>
<input type="text" class="form-control" id="measurement-pixels" readonly>
</div>
<div class="col-md-6">
<label class="form-label">Length (mm)</label>
<input type="text" class="form-control" id="measurement-mm" readonly>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
<i class="fas fa-times me-1"></i>Cancel
</button>
<button type="button" class="btn btn-primary" onclick="saveMeasurement()">
<i class="fas fa-save me-1"></i>Save
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
let currentTool = 'pointer';
let measurements = [];
let rois = [];
let annotations = [];
let zoomLevel = 1.0;
let panOffset = { x: 0, y: 0 };
let isDrawing = false;
let currentMeasurement = null;
$(document).ready(function() {
initializeViewer();
loadExistingAnalysis();
// Set up canvas event listeners
const canvas = document.getElementById('viewer-canvas');
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('wheel', handleWheel);
// Update analysis time
updateAnalysisTime();
setInterval(updateAnalysisTime, 60000); // Update every minute
});
function initializeViewer() {
// Initialize DICOM viewer
const image = document.getElementById('dicom-image');
if (image) {
image.onload = function() {
updateImageInfo();
generateHistogram();
};
}
}
function loadExistingAnalysis() {
// Load existing analysis data if available
fetch(`/radiology/dicom/{{ file.id }}/analysis/data/`, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success && data.analysis) {
measurements = data.analysis.measurements || [];
rois = data.analysis.rois || [];
annotations = data.analysis.annotations || [];
updateMeasurementsList();
updateROIList();
updateAnnotationsList();
}
})
.catch(error => {
console.error('Error loading analysis data:', error);
});
}
function selectTool(tool) {
currentTool = tool;
// Update tool buttons
document.querySelectorAll('.tool-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`[data-tool="${tool}"]`).classList.add('active');
// Update cursor
const canvas = document.getElementById('viewer-canvas');
switch (tool) {
case 'measure':
canvas.style.cursor = 'crosshair';
break;
case 'roi':
canvas.style.cursor = 'crosshair';
break;
case 'annotation':
canvas.style.cursor = 'text';
break;
default:
canvas.style.cursor = 'default';
}
}
function setTool(tool) {
selectTool(tool);
// Update viewer control buttons
document.querySelectorAll('.control-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
}
function handleMouseDown(event) {
const rect = event.target.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
switch (currentTool) {
case 'measure':
startMeasurement(x, y);
break;
case 'roi':
startROI(x, y);
break;
case 'annotation':
addAnnotationAtPoint(x, y);
break;
case 'pan':
startPan(x, y);
break;
}
}
function handleMouseMove(event) {
if (!isDrawing) return;
const rect = event.target.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
switch (currentTool) {
case 'measure':
updateMeasurement(x, y);
break;
case 'roi':
updateROI(x, y);
break;
case 'pan':
updatePan(x, y);
break;
}
}
function handleMouseUp(event) {
if (!isDrawing) return;
const rect = event.target.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
switch (currentTool) {
case 'measure':
finishMeasurement(x, y);
break;
case 'roi':
finishROI(x, y);
break;
case 'pan':
finishPan();
break;
}
isDrawing = false;
}
function handleWheel(event) {
event.preventDefault();
if (event.deltaY < 0) {
zoomIn();
} else {
zoomOut();
}
}
function startMeasurement(x, y) {
isDrawing = true;
currentMeasurement = {
id: Date.now(),
type: 'line',
start: { x, y },
end: { x, y },
label: '',
notes: ''
};
}
function updateMeasurement(x, y) {
if (currentMeasurement) {
currentMeasurement.end = { x, y };
drawMeasurements();
}
}
function finishMeasurement(x, y) {
if (currentMeasurement) {
currentMeasurement.end = { x, y };
// Calculate distance
const dx = currentMeasurement.end.x - currentMeasurement.start.x;
const dy = currentMeasurement.end.y - currentMeasurement.start.y;
const distance = Math.sqrt(dx * dx + dy * dy);
currentMeasurement.distance_pixels = distance;
currentMeasurement.distance_mm = distance * getPixelSpacing(); // Convert to mm
measurements.push(currentMeasurement);
updateMeasurementsList();
drawMeasurements();
currentMeasurement = null;
}
}
function startROI(x, y) {
isDrawing = true;
// ROI implementation would go here
}
function updateROI(x, y) {
// ROI update implementation
}
function finishROI(x, y) {
// ROI finish implementation
}
function startPan(x, y) {
isDrawing = true;
// Pan implementation
}
function updatePan(x, y) {
// Pan update implementation
}
function finishPan() {
// Pan finish implementation
}
function addAnnotationAtPoint(x, y) {
const text = prompt('Enter annotation text:');
if (text) {
const annotation = {
id: Date.now(),
x: x,
y: y,
text: text,
timestamp: new Date().toISOString()
};
annotations.push(annotation);
updateAnnotationsList();
drawAnnotations();
}
}
function drawMeasurements() {
const overlay = document.getElementById('measurement-overlay');
// Clear existing measurements
overlay.innerHTML = '';
// Draw all measurements
measurements.forEach(measurement => {
if (measurement.type === 'line') {
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', measurement.start.x);
line.setAttribute('y1', measurement.start.y);
line.setAttribute('x2', measurement.end.x);
line.setAttribute('y2', measurement.end.y);
line.setAttribute('class', 'measurement-line');
overlay.appendChild(line);
// Add measurement text
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', (measurement.start.x + measurement.end.x) / 2);
text.setAttribute('y', (measurement.start.y + measurement.end.y) / 2 - 5);
text.setAttribute('class', 'measurement-text');
text.textContent = `${measurement.distance_mm.toFixed(1)} mm`;
overlay.appendChild(text);
}
});
// Draw current measurement if drawing
if (currentMeasurement) {
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', currentMeasurement.start.x);
line.setAttribute('y1', currentMeasurement.start.y);
line.setAttribute('x2', currentMeasurement.end.x);
line.setAttribute('y2', currentMeasurement.end.y);
line.setAttribute('class', 'measurement-line');
overlay.appendChild(line);
}
}
function drawAnnotations() {
// Implementation for drawing annotations
}
function updateMeasurementsList() {
const list = document.getElementById('measurements-list');
if (measurements.length === 0) {
list.innerHTML = `
<div class="text-muted text-center py-3">
<i class="fas fa-ruler fa-2x mb-2"></i>
<div>No measurements yet</div>
<small>Use the measurement tools to add measurements</small>
</div>
`;
return;
}
let html = '';
measurements.forEach((measurement, index) => {
html += `
<div class="measurement-item">
<div>
<div class="measurement-label">
${measurement.label || `Measurement ${index + 1}`}
</div>
<div class="measurement-value">
${measurement.distance_mm.toFixed(1)} mm (${measurement.distance_pixels.toFixed(0)} px)
</div>
</div>
<div class="measurement-actions">
<button type="button" class="btn-measurement btn-edit" onclick="editMeasurement(${measurement.id})">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn-measurement btn-delete" onclick="deleteMeasurement(${measurement.id})">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`;
});
list.innerHTML = html;
}
function updateROIList() {
const list = document.getElementById('roi-list');
if (rois.length === 0) {
list.innerHTML = `
<div class="text-muted text-center py-3">
<i class="fas fa-draw-polygon fa-2x mb-2"></i>
<div>No ROIs defined</div>
<small>Draw regions of interest for analysis</small>
</div>
`;
return;
}
// ROI list implementation
}
function updateAnnotationsList() {
const list = document.getElementById('annotation-list');
let html = '';
annotations.forEach(annotation => {
html += `
<div class="annotation-item">
<div class="annotation-text">${annotation.text}</div>
<div class="annotation-meta">
${new Date(annotation.timestamp).toLocaleString()}
</div>
</div>
`;
});
list.innerHTML = html;
}
function zoomIn() {
zoomLevel *= 1.2;
updateZoom();
}
function zoomOut() {
zoomLevel /= 1.2;
updateZoom();
}
function resetZoom() {
zoomLevel = 1.0;
updateZoom();
}
function updateZoom() {
const image = document.getElementById('dicom-image');
if (image) {
image.style.transform = `scale(${zoomLevel})`;
}
document.getElementById('zoom-level').textContent = `${Math.round(zoomLevel * 100)}%`;
}
function updateWindowing() {
const windowWidth = document.getElementById('window-width').value;
const windowLevel = document.getElementById('window-level').value;
document.getElementById('window-value').textContent = windowWidth;
document.getElementById('level-value').textContent = windowLevel;
// Apply windowing to image (implementation would depend on DICOM library)
applyWindowLevel(windowWidth, windowLevel);
}
function applyWindowLevel(width, level) {
// Implementation for applying window/level to DICOM image
}
function applyPreset(preset) {
const presets = {
lung: { width: 1500, level: -600 },
bone: { width: 2000, level: 300 },
soft_tissue: { width: 400, level: 40 },
brain: { width: 80, level: 40 },
liver: { width: 150, level: 30 }
};
if (presets[preset]) {
document.getElementById('window-width').value = presets[preset].width;
document.getElementById('window-level').value = presets[preset].level;
updateWindowing();
}
}
function autoWindow() {
// Auto window/level implementation
showAlert('Auto windowing applied', 'info');
}
function invertImage() {
const image = document.getElementById('dicom-image');
if (image.style.filter.includes('invert')) {
image.style.filter = image.style.filter.replace('invert(1)', '');
} else {
image.style.filter += ' invert(1)';
}
}
function clearMeasurements() {
if (measurements.length === 0) {
showAlert('No measurements to clear', 'info');
return;
}
if (confirm('Are you sure you want to clear all measurements?')) {
measurements = [];
updateMeasurementsList();
drawMeasurements();
showAlert('All measurements cleared', 'success');
}
}
function deleteMeasurement(id) {
measurements = measurements.filter(m => m.id !== id);
updateMeasurementsList();
drawMeasurements();
}
function editMeasurement(id) {
const measurement = measurements.find(m => m.id === id);
if (measurement) {
document.getElementById('measurement-label').value = measurement.label || '';
document.getElementById('measurement-notes').value = measurement.notes || '';
document.getElementById('measurement-pixels').value = measurement.distance_pixels.toFixed(0);
document.getElementById('measurement-mm').value = measurement.distance_mm.toFixed(1);
// Store current measurement ID for saving
window.currentEditingMeasurement = id;
new bootstrap.Modal(document.getElementById('measurementModal')).show();
}
}
function saveMeasurement() {
const id = window.currentEditingMeasurement;
const measurement = measurements.find(m => m.id === id);
if (measurement) {
measurement.label = document.getElementById('measurement-label').value;
measurement.notes = document.getElementById('measurement-notes').value;
updateMeasurementsList();
bootstrap.Modal.getInstance(document.getElementById('measurementModal')).hide();
showAlert('Measurement updated', 'success');
}
}
function analyzeROIs() {
if (rois.length === 0) {
showAlert('No ROIs to analyze', 'warning');
return;
}
// ROI analysis implementation
showAlert('ROI analysis started', 'info');
}
function generateHistogram() {
const canvas = document.getElementById('histogram-canvas');
const ctx = canvas.getContext('2d');
// Generate sample histogram data
const data = [];
const labels = [];
for (let i = 0; i < 256; i++) {
labels.push(i);
data.push(Math.random() * 1000);
}
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Pixel Count',
data: data,
borderColor: '#007bff',
backgroundColor: 'rgba(0, 123, 255, 0.1)',
borderWidth: 1,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
title: {
display: true,
text: 'Pixel Intensity'
}
},
y: {
title: {
display: true,
text: 'Count'
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
function addAnnotation() {
const text = document.getElementById('new-annotation').value.trim();
if (!text) {
showAlert('Please enter annotation text', 'warning');
return;
}
const annotation = {
id: Date.now(),
text: text,
timestamp: new Date().toISOString()
};
annotations.push(annotation);
updateAnnotationsList();
document.getElementById('new-annotation').value = '';
showAlert('Annotation added', 'success');
}
function updateImageInfo() {
// Update image information overlay
const info = document.getElementById('image-info');
// Implementation would update with actual DICOM metadata
}
function updateAnalysisTime() {
const timeElement = document.getElementById('analysis-time');
const statusElement = document.getElementById('analysis-status');
// Update with current time
timeElement.textContent = new Date().toLocaleString();
statusElement.textContent = 'Active';
}
function getPixelSpacing() {
// Return pixel spacing in mm (would come from DICOM metadata)
return 0.5; // Default 0.5mm per pixel
}
function resetAnalysis() {
if (confirm('Are you sure you want to reset the entire analysis? This will clear all measurements, ROIs, and annotations.')) {
measurements = [];
rois = [];
annotations = [];
updateMeasurementsList();
updateROIList();
updateAnnotationsList();
drawMeasurements();
showAlert('Analysis reset', 'success');
}
}
function saveAnalysis() {
const analysisData = {
measurements: measurements,
rois: rois,
annotations: annotations,
timestamp: new Date().toISOString()
};
fetch(`/radiology/dicom/{{ file.id }}/analysis/save/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
'Content-Type': 'application/json'
},
body: JSON.stringify(analysisData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('Analysis saved successfully', 'success');
} else {
showAlert('Error saving analysis', 'danger');
}
})
.catch(error => {
showAlert('Error saving analysis', 'danger');
});
}
function generateReport() {
window.open(`/radiology/dicom/{{ file.id }}/analysis/report/`, '_blank');
}
function exportResults() {
window.open(`/radiology/dicom/{{ file.id }}/analysis/export/`, '_blank');
}
function exportImage() {
// Export current view as image
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const image = document.getElementById('dicom-image');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
ctx.drawImage(image, 0, 0);
// Download the image
const link = document.createElement('a');
link.download = `${file.filename}_analysis.png`;
link.href = canvas.toDataURL();
link.click();
}
function exportMeasurements() {
// Export measurements as CSV
let csv = 'Label,Distance (mm),Distance (pixels),Notes\n';
measurements.forEach((measurement, index) => {
csv += `"${measurement.label || `Measurement ${index + 1}`}",${measurement.distance_mm.toFixed(1)},${measurement.distance_pixels.toFixed(0)},"${measurement.notes || ''}"\n`;
});
const blob = new Blob([csv], { type: 'text/csv' });
const link = document.createElement('a');
link.download = `${file.filename}_measurements.csv`;
link.href = URL.createObjectURL(blob);
link.click();
}
function exportReport() {
window.open(`/radiology/dicom/{{ file.id }}/analysis/report/pdf/`, '_blank');
}
function exportDICOM() {
window.open(`/radiology/dicom/{{ file.id }}/analysis/dicom-sr/`, '_blank');
}
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 1060; min-width: 300px;';
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
</script>
{% endblock %}