1123 lines
45 KiB
HTML
1123 lines
45 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Study Comparison - 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>Study Comparison</h4>
|
|
<h6>Compare current and prior imaging studies for comprehensive analysis</h6>
|
|
</div>
|
|
<div class="page-btn">
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-primary" onclick="loadStudyForComparison()">
|
|
<i class="fa fa-plus"></i> Add Study
|
|
</button>
|
|
<button type="button" class="btn btn-info" onclick="syncViewports()">
|
|
<i class="fa fa-sync"></i> Sync Views
|
|
</button>
|
|
<button type="button" class="btn btn-success" onclick="generateComparisonReport()">
|
|
<i class="fa fa-file-alt"></i> Generate Report
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Study Selection -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">Study Selection & Information</h5>
|
|
<div class="card-tools">
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="swapStudies()">
|
|
<i class="fa fa-exchange-alt"></i> Swap
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="resetComparison()">
|
|
<i class="fa fa-undo"></i> Reset
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<!-- Current Study -->
|
|
<div class="col-md-6">
|
|
<div class="study-panel current-study">
|
|
<div class="study-header">
|
|
<h6 class="text-primary">Current Study</h6>
|
|
<span class="badge bg-primary">{{ current_study.study_date|default:"Jan 15, 2024" }}</span>
|
|
</div>
|
|
<div class="study-info">
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<p><strong>Patient:</strong> {{ current_study.patient_name|default:"John Smith" }}</p>
|
|
<p><strong>MRN:</strong> {{ current_study.patient_mrn|default:"123456" }}</p>
|
|
<p><strong>Study Date:</strong> {{ current_study.study_date|default:"Jan 15, 2024" }}</p>
|
|
<p><strong>Modality:</strong> {{ current_study.modality|default:"CT" }}</p>
|
|
</div>
|
|
<div class="col-6">
|
|
<p><strong>Body Part:</strong> {{ current_study.body_part|default:"Chest" }}</p>
|
|
<p><strong>Protocol:</strong> {{ current_study.protocol|default:"Chest CT with Contrast" }}</p>
|
|
<p><strong>Accession:</strong> {{ current_study.accession|default:"ACC123456" }}</p>
|
|
<p><strong>Series:</strong> {{ current_study.series_count|default:3 }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Prior Study -->
|
|
<div class="col-md-6">
|
|
<div class="study-panel prior-study">
|
|
<div class="study-header">
|
|
<h6 class="text-success">Prior Study</h6>
|
|
<span class="badge bg-success">{{ prior_study.study_date|default:"Dec 10, 2023" }}</span>
|
|
</div>
|
|
<div class="study-info">
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<p><strong>Patient:</strong> {{ prior_study.patient_name|default:"John Smith" }}</p>
|
|
<p><strong>MRN:</strong> {{ prior_study.patient_mrn|default:"123456" }}</p>
|
|
<p><strong>Study Date:</strong> {{ prior_study.study_date|default:"Dec 10, 2023" }}</p>
|
|
<p><strong>Modality:</strong> {{ prior_study.modality|default:"CT" }}</p>
|
|
</div>
|
|
<div class="col-6">
|
|
<p><strong>Body Part:</strong> {{ prior_study.body_part|default:"Chest" }}</p>
|
|
<p><strong>Protocol:</strong> {{ prior_study.protocol|default:"Chest CT with Contrast" }}</p>
|
|
<p><strong>Accession:</strong> {{ prior_study.accession|default:"ACC098765" }}</p>
|
|
<p><strong>Series:</strong> {{ prior_study.series_count|default:3 }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="study-actions">
|
|
<button class="btn btn-sm btn-outline-success" onclick="selectPriorStudy()">
|
|
<i class="fa fa-search"></i> Select Different Study
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Time Interval -->
|
|
<div class="row mt-3">
|
|
<div class="col-12">
|
|
<div class="time-interval-info text-center">
|
|
<div class="interval-display">
|
|
<i class="fa fa-clock text-warning"></i>
|
|
<strong>Time Interval: {{ time_interval|default:"36 days" }}</strong>
|
|
<small class="text-muted">({{ interval_details|default:"5 weeks, 1 day" }})</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Comparison Viewer -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">Image Comparison</h5>
|
|
<div class="card-tools">
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-sm btn-outline-primary active" data-layout="side-by-side" onclick="changeLayout('side-by-side')">
|
|
<i class="fa fa-columns"></i> Side by Side
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-primary" data-layout="overlay" onclick="changeLayout('overlay')">
|
|
<i class="fa fa-layer-group"></i> Overlay
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-primary" data-layout="flicker" onclick="changeLayout('flicker')">
|
|
<i class="fa fa-exchange-alt"></i> Flicker
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-primary" data-layout="quad" onclick="changeLayout('quad')">
|
|
<i class="fa fa-th"></i> Quad View
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="comparison-container" id="comparisonContainer">
|
|
<!-- Side by Side Layout (Default) -->
|
|
<div class="layout-side-by-side" id="sideBySideLayout">
|
|
<div class="row g-0">
|
|
<div class="col-md-6">
|
|
<div class="viewport-container current-viewport">
|
|
<div class="viewport-header">
|
|
<h6>Current Study - {{ current_study.study_date|default:"Jan 15, 2024" }}</h6>
|
|
<div class="viewport-controls">
|
|
<button class="btn btn-sm btn-outline-light" onclick="resetCurrentView()">
|
|
<i class="fa fa-home"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-light" onclick="fitCurrentToWindow()">
|
|
<i class="fa fa-expand-arrows-alt"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="viewport-content">
|
|
<canvas id="currentCanvas" width="600" height="500"></canvas>
|
|
<div class="viewport-overlay">
|
|
<div class="overlay-info top-left">
|
|
<div>{{ current_study.patient_name|default:"Smith, John" }}</div>
|
|
<div>{{ current_study.study_date|default:"2024-01-15" }}</div>
|
|
<div>Series: <span id="currentSeries">1</span></div>
|
|
</div>
|
|
<div class="overlay-info bottom-right">
|
|
<div>Image: <span id="currentImage">60</span>/120</div>
|
|
<div>W: <span id="currentWindow">400</span></div>
|
|
<div>L: <span id="currentLevel">40</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="viewport-container prior-viewport">
|
|
<div class="viewport-header">
|
|
<h6>Prior Study - {{ prior_study.study_date|default:"Dec 10, 2023" }}</h6>
|
|
<div class="viewport-controls">
|
|
<button class="btn btn-sm btn-outline-light" onclick="resetPriorView()">
|
|
<i class="fa fa-home"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-light" onclick="fitPriorToWindow()">
|
|
<i class="fa fa-expand-arrows-alt"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="viewport-content">
|
|
<canvas id="priorCanvas" width="600" height="500"></canvas>
|
|
<div class="viewport-overlay">
|
|
<div class="overlay-info top-left">
|
|
<div>{{ prior_study.patient_name|default:"Smith, John" }}</div>
|
|
<div>{{ prior_study.study_date|default:"2023-12-10" }}</div>
|
|
<div>Series: <span id="priorSeries">1</span></div>
|
|
</div>
|
|
<div class="overlay-info bottom-right">
|
|
<div>Image: <span id="priorImage">60</span>/115</div>
|
|
<div>W: <span id="priorWindow">400</span></div>
|
|
<div>L: <span id="priorLevel">40</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Overlay Layout -->
|
|
<div class="layout-overlay" id="overlayLayout" style="display: none;">
|
|
<div class="overlay-viewport">
|
|
<div class="viewport-header">
|
|
<h6>Overlay Comparison</h6>
|
|
<div class="overlay-controls">
|
|
<label class="form-label">Opacity:</label>
|
|
<input type="range" class="form-range" id="overlayOpacity" min="0" max="100" value="50" onchange="updateOverlayOpacity(this.value)">
|
|
<span id="opacityValue">50%</span>
|
|
</div>
|
|
</div>
|
|
<div class="viewport-content">
|
|
<canvas id="overlayCanvas" width="800" height="600"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Flicker Layout -->
|
|
<div class="layout-flicker" id="flickerLayout" style="display: none;">
|
|
<div class="flicker-viewport">
|
|
<div class="viewport-header">
|
|
<h6>Flicker Comparison</h6>
|
|
<div class="flicker-controls">
|
|
<button class="btn btn-sm btn-outline-primary" id="flickerBtn" onclick="toggleFlicker()">
|
|
<i class="fa fa-play"></i> Start Flicker
|
|
</button>
|
|
<label class="form-label">Speed:</label>
|
|
<input type="range" class="form-range" id="flickerSpeed" min="100" max="2000" value="500" onchange="updateFlickerSpeed(this.value)">
|
|
</div>
|
|
</div>
|
|
<div class="viewport-content">
|
|
<canvas id="flickerCanvas" width="800" height="600"></canvas>
|
|
<div class="flicker-indicator">
|
|
<span id="flickerStatus">Current Study</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quad View Layout -->
|
|
<div class="layout-quad" id="quadLayout" style="display: none;">
|
|
<div class="row g-1">
|
|
<div class="col-6">
|
|
<div class="quad-viewport">
|
|
<div class="quad-header">Current - Axial</div>
|
|
<canvas id="quadCanvas1" width="400" height="300"></canvas>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="quad-viewport">
|
|
<div class="quad-header">Prior - Axial</div>
|
|
<canvas id="quadCanvas2" width="400" height="300"></canvas>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="quad-viewport">
|
|
<div class="quad-header">Current - Coronal</div>
|
|
<canvas id="quadCanvas3" width="400" height="300"></canvas>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="quad-viewport">
|
|
<div class="quad-header">Prior - Coronal</div>
|
|
<canvas id="quadCanvas4" width="400" height="300"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Navigation and Tools -->
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h6 class="card-title">Navigation Controls</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<h6>Current Study Navigation</h6>
|
|
<div class="navigation-controls">
|
|
<div class="btn-group mb-2">
|
|
<button class="btn btn-sm btn-outline-primary" onclick="currentFirstImage()">
|
|
<i class="fa fa-step-backward"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="currentPreviousImage()">
|
|
<i class="fa fa-backward"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="currentNextImage()">
|
|
<i class="fa fa-forward"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="currentLastImage()">
|
|
<i class="fa fa-step-forward"></i>
|
|
</button>
|
|
</div>
|
|
<input type="range" class="form-range" id="currentSlider" min="1" max="120" value="60" onchange="goToCurrentImage(this.value)">
|
|
<div class="text-center">
|
|
<small>Image <span id="currentImageNum">60</span> of 120</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6>Prior Study Navigation</h6>
|
|
<div class="navigation-controls">
|
|
<div class="btn-group mb-2">
|
|
<button class="btn btn-sm btn-outline-success" onclick="priorFirstImage()">
|
|
<i class="fa fa-step-backward"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-success" onclick="priorPreviousImage()">
|
|
<i class="fa fa-backward"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-success" onclick="priorNextImage()">
|
|
<i class="fa fa-forward"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-success" onclick="priorLastImage()">
|
|
<i class="fa fa-step-forward"></i>
|
|
</button>
|
|
</div>
|
|
<input type="range" class="form-range" id="priorSlider" min="1" max="115" value="60" onchange="goToPriorImage(this.value)">
|
|
<div class="text-center">
|
|
<small>Image <span id="priorImageNum">60</span> of 115</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mt-3">
|
|
<div class="col-12">
|
|
<div class="sync-controls text-center">
|
|
<div class="form-check form-check-inline">
|
|
<input class="form-check-input" type="checkbox" id="syncScroll" checked onchange="toggleSyncScroll()">
|
|
<label class="form-check-label" for="syncScroll">Sync Scrolling</label>
|
|
</div>
|
|
<div class="form-check form-check-inline">
|
|
<input class="form-check-input" type="checkbox" id="syncWindow" checked onchange="toggleSyncWindow()">
|
|
<label class="form-check-label" for="syncWindow">Sync Window/Level</label>
|
|
</div>
|
|
<div class="form-check form-check-inline">
|
|
<input class="form-check-input" type="checkbox" id="syncZoom" checked onchange="toggleSyncZoom()">
|
|
<label class="form-check-label" for="syncZoom">Sync Zoom/Pan</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h6 class="card-title">Comparison Tools</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="tool-group mb-3">
|
|
<h6>Measurements</h6>
|
|
<button class="btn btn-sm btn-outline-primary w-100 mb-2" onclick="addMeasurement()">
|
|
<i class="fa fa-ruler"></i> Add Measurement
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-info w-100 mb-2" onclick="compareMeasurements()">
|
|
<i class="fa fa-chart-line"></i> Compare Measurements
|
|
</button>
|
|
</div>
|
|
|
|
<div class="tool-group mb-3">
|
|
<h6>Annotations</h6>
|
|
<button class="btn btn-sm btn-outline-warning w-100 mb-2" onclick="addAnnotation()">
|
|
<i class="fa fa-comment"></i> Add Annotation
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-secondary w-100 mb-2" onclick="linkAnnotations()">
|
|
<i class="fa fa-link"></i> Link Annotations
|
|
</button>
|
|
</div>
|
|
|
|
<div class="tool-group">
|
|
<h6>Analysis</h6>
|
|
<button class="btn btn-sm btn-outline-success w-100 mb-2" onclick="calculateChanges()">
|
|
<i class="fa fa-calculator"></i> Calculate Changes
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-info w-100" onclick="generateChangeMap()">
|
|
<i class="fa fa-map"></i> Generate Change Map
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Findings Summary -->
|
|
<div class="card mt-3">
|
|
<div class="card-header">
|
|
<h6 class="card-title">Comparison Findings</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="findings-list">
|
|
<div class="finding-item mb-2">
|
|
<div class="finding-header">
|
|
<span class="badge bg-warning">Changed</span>
|
|
<small class="text-muted">Nodule size</small>
|
|
</div>
|
|
<div class="finding-details">
|
|
<small>Previous: 8.2 mm → Current: 9.1 mm</small><br>
|
|
<small class="text-warning">+11% increase</small>
|
|
</div>
|
|
</div>
|
|
<div class="finding-item mb-2">
|
|
<div class="finding-header">
|
|
<span class="badge bg-success">Stable</span>
|
|
<small class="text-muted">Cardiac size</small>
|
|
</div>
|
|
<div class="finding-details">
|
|
<small>No significant change</small>
|
|
</div>
|
|
</div>
|
|
<div class="finding-item">
|
|
<div class="finding-header">
|
|
<span class="badge bg-info">New</span>
|
|
<small class="text-muted">Ground glass opacity</small>
|
|
</div>
|
|
<div class="finding-details">
|
|
<small>New finding in right lower lobe</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Comparison Report -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">Comparison Report</h5>
|
|
<div class="card-tools">
|
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="saveComparison()">
|
|
<i class="fa fa-save"></i> Save
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-outline-success" onclick="exportComparison()">
|
|
<i class="fa fa-download"></i> Export
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<textarea class="form-control" rows="6" placeholder="Enter comparison findings and analysis...">COMPARISON:
|
|
Current study ({{ current_study.study_date|default:"January 15, 2024" }}) compared to prior study ({{ prior_study.study_date|default:"December 10, 2023" }}).
|
|
|
|
INTERVAL CHANGES:
|
|
1. The previously identified 8.2 mm nodule in the right upper lobe has increased in size to 9.1 mm, representing an 11% increase.
|
|
2. New ground glass opacity in the right lower lobe, measuring approximately 15 mm.
|
|
3. Cardiac silhouette remains stable in size and configuration.
|
|
4. No new pleural effusions or pneumothorax.
|
|
|
|
IMPRESSION:
|
|
1. Interval growth of right upper lobe nodule - recommend follow-up in 3 months or consider tissue sampling.
|
|
2. New ground glass opacity in right lower lobe - clinical correlation recommended.</textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let currentLayout = 'side-by-side';
|
|
let syncScroll = true;
|
|
let syncWindow = true;
|
|
let syncZoom = true;
|
|
let flickerInterval;
|
|
let isFlickering = false;
|
|
let currentImageIndex = 60;
|
|
let priorImageIndex = 60;
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initializeCanvases();
|
|
setupEventListeners();
|
|
});
|
|
|
|
function initializeCanvases() {
|
|
// Initialize all canvases with placeholder images
|
|
const canvases = ['currentCanvas', 'priorCanvas', 'overlayCanvas', 'flickerCanvas', 'quadCanvas1', 'quadCanvas2', 'quadCanvas3', 'quadCanvas4'];
|
|
|
|
canvases.forEach(canvasId => {
|
|
const canvas = document.getElementById(canvasId);
|
|
if (canvas) {
|
|
const ctx = canvas.getContext('2d');
|
|
drawPlaceholderImage(ctx, canvasId.includes('prior') || canvasId.includes('2') || canvasId.includes('4'));
|
|
}
|
|
});
|
|
}
|
|
|
|
function drawPlaceholderImage(ctx, isPrior = false) {
|
|
const canvas = ctx.canvas;
|
|
|
|
// Clear canvas
|
|
ctx.fillStyle = '#000000';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Draw simulated medical image
|
|
ctx.fillStyle = isPrior ? '#2a2a2a' : '#333333';
|
|
ctx.fillRect(50, 50, canvas.width - 100, canvas.height - 100);
|
|
|
|
// Add some simulated anatomical structures
|
|
const centerX = canvas.width / 2;
|
|
const centerY = canvas.height / 2;
|
|
|
|
// Main structure
|
|
ctx.fillStyle = isPrior ? '#555555' : '#666666';
|
|
ctx.beginPath();
|
|
ctx.arc(centerX, centerY, 60, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
|
|
// Smaller structures
|
|
ctx.fillStyle = isPrior ? '#777777' : '#999999';
|
|
ctx.beginPath();
|
|
ctx.arc(centerX - 30, centerY - 20, isPrior ? 15 : 20, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(centerX + 25, centerY + 25, isPrior ? 12 : 18, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
|
|
// Add study identifier
|
|
ctx.fillStyle = '#00ff00';
|
|
ctx.font = '12px monospace';
|
|
ctx.fillText(isPrior ? 'PRIOR' : 'CURRENT', 10, 20);
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
// Canvas mouse events for synchronized interaction
|
|
const currentCanvas = document.getElementById('currentCanvas');
|
|
const priorCanvas = document.getElementById('priorCanvas');
|
|
|
|
if (currentCanvas && priorCanvas) {
|
|
currentCanvas.addEventListener('wheel', (e) => handleCanvasWheel(e, 'current'));
|
|
priorCanvas.addEventListener('wheel', (e) => handleCanvasWheel(e, 'prior'));
|
|
}
|
|
}
|
|
|
|
function handleCanvasWheel(e, viewport) {
|
|
e.preventDefault();
|
|
|
|
if (viewport === 'current') {
|
|
if (e.deltaY > 0) {
|
|
currentNextImage();
|
|
} else {
|
|
currentPreviousImage();
|
|
}
|
|
} else {
|
|
if (syncScroll) {
|
|
if (e.deltaY > 0) {
|
|
priorNextImage();
|
|
} else {
|
|
priorPreviousImage();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function changeLayout(layout) {
|
|
// Hide all layouts
|
|
document.querySelectorAll('[class^="layout-"]').forEach(el => {
|
|
el.style.display = 'none';
|
|
});
|
|
|
|
// Show selected layout
|
|
document.getElementById(layout.replace('-', '') + 'Layout').style.display = 'block';
|
|
|
|
// Update active button
|
|
document.querySelectorAll('[data-layout]').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
document.querySelector(`[data-layout="${layout}"]`).classList.add('active');
|
|
|
|
currentLayout = layout;
|
|
console.log(`Changed layout to: ${layout}`);
|
|
|
|
// Initialize layout-specific functionality
|
|
if (layout === 'overlay') {
|
|
updateOverlayView();
|
|
} else if (layout === 'flicker') {
|
|
updateFlickerView();
|
|
} else if (layout === 'quad') {
|
|
updateQuadView();
|
|
}
|
|
}
|
|
|
|
function updateOverlayView() {
|
|
const canvas = document.getElementById('overlayCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Draw overlay comparison
|
|
drawPlaceholderImage(ctx, false);
|
|
|
|
// Add overlay effect
|
|
ctx.globalAlpha = 0.5;
|
|
drawPlaceholderImage(ctx, true);
|
|
ctx.globalAlpha = 1.0;
|
|
}
|
|
|
|
function updateOverlayOpacity(value) {
|
|
document.getElementById('opacityValue').textContent = value + '%';
|
|
updateOverlayView();
|
|
console.log(`Overlay opacity set to: ${value}%`);
|
|
}
|
|
|
|
function updateFlickerView() {
|
|
const canvas = document.getElementById('flickerCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
drawPlaceholderImage(ctx, false);
|
|
}
|
|
|
|
function toggleFlicker() {
|
|
const flickerBtn = document.getElementById('flickerBtn');
|
|
|
|
if (isFlickering) {
|
|
clearInterval(flickerInterval);
|
|
flickerBtn.innerHTML = '<i class="fa fa-play"></i> Start Flicker';
|
|
isFlickering = false;
|
|
} else {
|
|
const speed = document.getElementById('flickerSpeed').value;
|
|
flickerInterval = setInterval(() => {
|
|
const canvas = document.getElementById('flickerCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
const status = document.getElementById('flickerStatus');
|
|
|
|
if (status.textContent === 'Current Study') {
|
|
drawPlaceholderImage(ctx, true);
|
|
status.textContent = 'Prior Study';
|
|
} else {
|
|
drawPlaceholderImage(ctx, false);
|
|
status.textContent = 'Current Study';
|
|
}
|
|
}, parseInt(speed));
|
|
|
|
flickerBtn.innerHTML = '<i class="fa fa-pause"></i> Stop Flicker';
|
|
isFlickering = true;
|
|
}
|
|
}
|
|
|
|
function updateFlickerSpeed(value) {
|
|
if (isFlickering) {
|
|
clearInterval(flickerInterval);
|
|
toggleFlicker(); // Restart with new speed
|
|
}
|
|
}
|
|
|
|
function updateQuadView() {
|
|
const canvases = ['quadCanvas1', 'quadCanvas2', 'quadCanvas3', 'quadCanvas4'];
|
|
canvases.forEach((canvasId, index) => {
|
|
const canvas = document.getElementById(canvasId);
|
|
const ctx = canvas.getContext('2d');
|
|
drawPlaceholderImage(ctx, index % 2 === 1);
|
|
});
|
|
}
|
|
|
|
// Navigation functions
|
|
function currentFirstImage() {
|
|
currentImageIndex = 1;
|
|
updateCurrentImage();
|
|
}
|
|
|
|
function currentPreviousImage() {
|
|
if (currentImageIndex > 1) {
|
|
currentImageIndex--;
|
|
updateCurrentImage();
|
|
if (syncScroll) priorPreviousImage();
|
|
}
|
|
}
|
|
|
|
function currentNextImage() {
|
|
if (currentImageIndex < 120) {
|
|
currentImageIndex++;
|
|
updateCurrentImage();
|
|
if (syncScroll) priorNextImage();
|
|
}
|
|
}
|
|
|
|
function currentLastImage() {
|
|
currentImageIndex = 120;
|
|
updateCurrentImage();
|
|
}
|
|
|
|
function priorFirstImage() {
|
|
priorImageIndex = 1;
|
|
updatePriorImage();
|
|
}
|
|
|
|
function priorPreviousImage() {
|
|
if (priorImageIndex > 1) {
|
|
priorImageIndex--;
|
|
updatePriorImage();
|
|
}
|
|
}
|
|
|
|
function priorNextImage() {
|
|
if (priorImageIndex < 115) {
|
|
priorImageIndex++;
|
|
updatePriorImage();
|
|
}
|
|
}
|
|
|
|
function priorLastImage() {
|
|
priorImageIndex = 115;
|
|
updatePriorImage();
|
|
}
|
|
|
|
function goToCurrentImage(imageNum) {
|
|
currentImageIndex = parseInt(imageNum);
|
|
updateCurrentImage();
|
|
if (syncScroll) {
|
|
priorImageIndex = Math.min(parseInt(imageNum), 115);
|
|
updatePriorImage();
|
|
}
|
|
}
|
|
|
|
function goToPriorImage(imageNum) {
|
|
priorImageIndex = parseInt(imageNum);
|
|
updatePriorImage();
|
|
}
|
|
|
|
function updateCurrentImage() {
|
|
document.getElementById('currentImageNum').textContent = currentImageIndex;
|
|
document.getElementById('currentSlider').value = currentImageIndex;
|
|
document.getElementById('currentImage').textContent = currentImageIndex;
|
|
|
|
// Redraw canvas with new image
|
|
const canvas = document.getElementById('currentCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
drawPlaceholderImage(ctx, false);
|
|
}
|
|
|
|
function updatePriorImage() {
|
|
document.getElementById('priorImageNum').textContent = priorImageIndex;
|
|
document.getElementById('priorSlider').value = priorImageIndex;
|
|
document.getElementById('priorImage').textContent = priorImageIndex;
|
|
|
|
// Redraw canvas with new image
|
|
const canvas = document.getElementById('priorCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
drawPlaceholderImage(ctx, true);
|
|
}
|
|
|
|
// Sync functions
|
|
function toggleSyncScroll() {
|
|
syncScroll = document.getElementById('syncScroll').checked;
|
|
console.log(`Sync scrolling: ${syncScroll}`);
|
|
}
|
|
|
|
function toggleSyncWindow() {
|
|
syncWindow = document.getElementById('syncWindow').checked;
|
|
console.log(`Sync window/level: ${syncWindow}`);
|
|
}
|
|
|
|
function toggleSyncZoom() {
|
|
syncZoom = document.getElementById('syncZoom').checked;
|
|
console.log(`Sync zoom/pan: ${syncZoom}`);
|
|
}
|
|
|
|
// Viewport control functions
|
|
function resetCurrentView() {
|
|
console.log('Reset current view');
|
|
updateCurrentImage();
|
|
}
|
|
|
|
function resetPriorView() {
|
|
console.log('Reset prior view');
|
|
updatePriorImage();
|
|
}
|
|
|
|
function fitCurrentToWindow() {
|
|
console.log('Fit current to window');
|
|
}
|
|
|
|
function fitPriorToWindow() {
|
|
console.log('Fit prior to window');
|
|
}
|
|
|
|
// Study management functions
|
|
function loadStudyForComparison() {
|
|
console.log('Loading study for comparison...');
|
|
alert('Study browser would open here to select a study for comparison.');
|
|
}
|
|
|
|
function selectPriorStudy() {
|
|
console.log('Selecting different prior study...');
|
|
alert('Prior study selection interface would open here.');
|
|
}
|
|
|
|
function swapStudies() {
|
|
console.log('Swapping current and prior studies...');
|
|
alert('Studies have been swapped. Current is now prior and vice versa.');
|
|
}
|
|
|
|
function resetComparison() {
|
|
if (confirm('Reset comparison? This will clear all annotations and measurements.')) {
|
|
console.log('Resetting comparison...');
|
|
alert('Comparison has been reset.');
|
|
}
|
|
}
|
|
|
|
function syncViewports() {
|
|
console.log('Synchronizing viewports...');
|
|
alert('Viewports synchronized. Both studies are now showing the same slice level and window/level settings.');
|
|
}
|
|
|
|
// Tool functions
|
|
function addMeasurement() {
|
|
console.log('Adding measurement...');
|
|
alert('Measurement tool activated. Click and drag on either image to create a measurement.');
|
|
}
|
|
|
|
function compareMeasurements() {
|
|
console.log('Comparing measurements...');
|
|
alert('Measurement comparison table would be displayed here.');
|
|
}
|
|
|
|
function addAnnotation() {
|
|
console.log('Adding annotation...');
|
|
alert('Annotation tool activated. Click on either image to add an annotation.');
|
|
}
|
|
|
|
function linkAnnotations() {
|
|
console.log('Linking annotations...');
|
|
alert('Select annotations on both studies to link them for comparison.');
|
|
}
|
|
|
|
function calculateChanges() {
|
|
console.log('Calculating changes...');
|
|
alert('Automated change detection is analyzing the studies. Results will be displayed shortly.');
|
|
}
|
|
|
|
function generateChangeMap() {
|
|
console.log('Generating change map...');
|
|
alert('Change map is being generated. This will highlight areas of difference between the studies.');
|
|
}
|
|
|
|
// Report functions
|
|
function generateComparisonReport() {
|
|
console.log('Generating comparison report...');
|
|
alert('Automated comparison report is being generated based on the findings and measurements.');
|
|
}
|
|
|
|
function saveComparison() {
|
|
console.log('Saving comparison...');
|
|
alert('Comparison session saved successfully.');
|
|
}
|
|
|
|
function exportComparison() {
|
|
const format = prompt('Export format:\n1. PDF Report\n2. DICOM with annotations\n3. Image snapshots\n\nEnter number:');
|
|
|
|
if (format && ['1', '2', '3'].includes(format)) {
|
|
const formats = {
|
|
'1': 'PDF Report',
|
|
'2': 'DICOM with annotations',
|
|
'3': 'Image snapshots'
|
|
};
|
|
|
|
console.log(`Exporting comparison as: ${formats[format]}`);
|
|
alert(`Comparison exported as ${formats[format]}.`);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.study-panel {
|
|
border: 2px solid #dee2e6;
|
|
border-radius: 8px;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.current-study {
|
|
border-color: #007bff;
|
|
background-color: #f8f9ff;
|
|
}
|
|
|
|
.prior-study {
|
|
border-color: #28a745;
|
|
background-color: #f8fff8;
|
|
}
|
|
|
|
.study-header {
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 1px solid #dee2e6;
|
|
}
|
|
|
|
.study-info p {
|
|
margin-bottom: 0.25rem;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.time-interval-info {
|
|
background-color: #fff3cd;
|
|
border: 1px solid #ffeaa7;
|
|
border-radius: 6px;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.interval-display {
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.comparison-container {
|
|
min-height: 600px;
|
|
background-color: #000;
|
|
}
|
|
|
|
.viewport-container {
|
|
position: relative;
|
|
height: 500px;
|
|
border: 1px solid #333;
|
|
}
|
|
|
|
.viewport-header {
|
|
background-color: #333;
|
|
color: white;
|
|
padding: 0.5rem;
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
}
|
|
|
|
.viewport-header h6 {
|
|
margin: 0;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.viewport-controls {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.viewport-content {
|
|
position: relative;
|
|
height: calc(100% - 40px);
|
|
background-color: #000;
|
|
}
|
|
|
|
.viewport-content canvas {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.viewport-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.overlay-info {
|
|
position: absolute;
|
|
color: #00ff00;
|
|
font-family: monospace;
|
|
font-size: 11px;
|
|
background-color: rgba(0, 0, 0, 0.7);
|
|
padding: 4px;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.overlay-info.top-left {
|
|
top: 8px;
|
|
left: 8px;
|
|
}
|
|
|
|
.overlay-info.bottom-right {
|
|
bottom: 8px;
|
|
right: 8px;
|
|
}
|
|
|
|
.overlay-info div {
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.overlay-viewport, .flicker-viewport {
|
|
position: relative;
|
|
height: 600px;
|
|
background-color: #000;
|
|
}
|
|
|
|
.overlay-controls, .flicker-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.flicker-indicator {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
background-color: rgba(0, 0, 0, 0.8);
|
|
color: white;
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 4px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.quad-viewport {
|
|
position: relative;
|
|
height: 300px;
|
|
border: 1px solid #333;
|
|
background-color: #000;
|
|
}
|
|
|
|
.quad-header {
|
|
background-color: #333;
|
|
color: white;
|
|
padding: 0.25rem 0.5rem;
|
|
font-size: 0.8rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.quad-viewport canvas {
|
|
width: 100%;
|
|
height: calc(100% - 28px);
|
|
object-fit: contain;
|
|
}
|
|
|
|
.navigation-controls {
|
|
text-align: center;
|
|
}
|
|
|
|
.sync-controls {
|
|
background-color: #f8f9fa;
|
|
padding: 1rem;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.tool-group {
|
|
border-bottom: 1px solid #dee2e6;
|
|
padding-bottom: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.tool-group:last-child {
|
|
border-bottom: none;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.tool-group h6 {
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.finding-item {
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 4px;
|
|
padding: 0.5rem;
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.finding-header {
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.finding-details {
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.viewport-header {
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.overlay-controls, .flicker-controls {
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.sync-controls .form-check-inline {
|
|
display: block;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.comparison-container {
|
|
min-height: 400px;
|
|
}
|
|
|
|
.viewport-container {
|
|
height: 400px;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|