NeoScan_Physician/app/assets/dicom/dicom-viewer.html
2025-08-20 20:39:08 +05:30

734 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>DICOM Viewer - Fullscreen Mobile Friendly</title>
<style>
html, body {
margin: 0; padding: 0;
width: 100%; height: 100%;
background: #111; color: white; font-family: sans-serif;
overflow: hidden;
}
#dicomContainer {
position: relative;
width: 100%; height: 100%;
top: 0;
left: 0;
border-radius: 0;
overflow: visible;
}
#dicomImage {
width: 100%;
height: calc(100% - 100px);
background: black;
touch-action: none;
position: absolute;
top: 0;
left: 0;
border-radius: 0;
overflow: visible;
}
#dicomImage canvas {
max-width: 100%;
max-height: 100%;
position: relative;
top: 0;
border-radius: 0 !important;
}
/* Ensure cornerstone canvas shows original rectangular view */
.cornerstone-element {
position: relative !important;
top: 0 !important;
width: 100% !important;
height: 100% !important;
border-radius: 0 !important;
overflow: visible !important;
}
.cornerstone-canvas {
position: relative !important;
top: 0 !important;
border-radius: 0 !important;
max-width: 100% !important;
max-height: 100% !important;
clip-path: none !important;
mask: none !important;
}
/* Remove any circular clipping */
.cornerstone-element canvas,
#dicomImage canvas,
canvas {
border-radius: 0 !important;
clip-path: none !important;
mask: none !important;
shape-outside: none !important;
}
/* Ensure full rectangular display */
div[class*="cornerstone"] {
border-radius: 0 !important;
clip-path: none !important;
overflow: visible !important;
}
#controls {
position: absolute;
bottom: 15px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.8);
padding: 12px 16px;
border-radius: 12px;
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
}
@media (max-width: 768px) {
#controls {
bottom: 20px;
padding: 14px 18px;
border-radius: 16px;
}
#dicomImage {
height: calc(100% - 110px);
}
}
@media (max-width: 480px) {
#controls {
bottom: 25px;
padding: 16px 20px;
border-radius: 20px;
left: 10px;
right: 10px;
transform: none;
max-width: none;
}
#dicomImage {
height: calc(100% - 130px);
}
}
#frameControls, #actionControls {
margin-top: 8px;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
width: 90%;
}
#frameControls {
flex-wrap: nowrap;
width: 100%;
max-width: 240px;
gap: 5px;
}
@media (max-width: 768px) {
#frameControls, #actionControls {
gap: 10px;
margin-top: 10px;
width: 90%;
}
#frameControls {
flex-wrap: nowrap;
width: 100%;
max-width: 255px;
gap: 6px;
}
}
@media (max-width: 480px) {
#frameControls, #actionControls {
gap: 12px;
margin-top: 12px;
width: 90%;
}
#frameControls {
flex-wrap: nowrap;
width: 100%;
max-width: 270px;
gap: 8px;
}
}
button {
background: #222;
color: white;
border: 1px solid #555;
border-radius: 4px;
padding: 6px 10px;
font-size: 14px;
min-width: 34px;
min-height: 34px;
cursor: pointer;
touch-action: manipulation;
}
@media (max-width: 768px) {
button {
padding: 8px 12px;
font-size: 16px;
min-width: 35px;
min-height: 35px;
}
}
@media (max-width: 480px) {
button {
padding: 10px 14px;
font-size: 18px;
min-width: 37px;
min-height: 37px;
}
}
input[type=range] {
width: 135px;
height: 6px;
cursor: pointer;
flex: 1;
min-width: 100px;
-webkit-appearance: none;
appearance: none;
background: rgba(255,255,255,0.3);
border-radius: 3px;
outline: none;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
background: #2196F3;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
input[type=range]::-moz-range-thumb {
width: 16px;
height: 16px;
background: #2196F3;
border-radius: 50%;
cursor: pointer;
border: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
input[type=range]:disabled {
cursor: not-allowed;
}
input[type=range]:disabled::-webkit-slider-thumb {
background: #666;
cursor: not-allowed;
}
input[type=range]:disabled::-moz-range-thumb {
background: #666;
cursor: not-allowed;
}
@media (max-width: 768px) {
input[type=range] {
width: 155px;
height: 6px;
min-width: 120px;
}
input[type=range]::-webkit-slider-thumb {
width: 18px;
height: 18px;
}
input[type=range]::-moz-range-thumb {
width: 18px;
height: 18px;
}
}
@media (max-width: 480px) {
input[type=range] {
width: 170px;
height: 6px;
min-width: 140px;
}
input[type=range]::-webkit-slider-thumb {
width: 20px;
height: 20px;
}
input[type=range]::-moz-range-thumb {
width: 20px;
height: 20px;
}
}
input[type=file] {
display: none;
}
#frameInfo {
margin-top: 6px;
color: rgba(255,255,255,0.9);
font-size: 12px;
font-weight: 500;
text-align: center;
}
@media (max-width: 768px) {
#frameInfo {
font-size: 14px;
margin-top: 8px;
}
}
@media (max-width: 480px) {
#frameInfo {
font-size: 16px;
margin-top: 10px;
}
}
/* Loading Overlay Styles */
#loadingOverlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10000;
backdrop-filter: blur(5px);
}
#loadingOverlay.hidden {
display: none;
}
.loader {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid #2196F3;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
color: rgba(255, 255, 255, 0.9);
font-size: 16px;
font-weight: 500;
text-align: center;
margin-bottom: 10px;
}
.loading-subtext {
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
text-align: center;
max-width: 300px;
line-height: 1.4;
}
@media (max-width: 768px) {
.loader {
width: 60px;
height: 60px;
border-width: 5px;
}
.loading-text {
font-size: 18px;
}
.loading-subtext {
font-size: 16px;
}
}
@media (max-width: 480px) {
.loader {
width: 70px;
height: 70px;
border-width: 6px;
}
.loading-text {
font-size: 20px;
}
.loading-subtext {
font-size: 18px;
max-width: 280px;
}
}
</style>
</head>
<body>
<div id="dicomContainer">
<div id="dicomImage"></div>
<!-- Loading Overlay -->
<div id="loadingOverlay" class="hidden">
<div class="loader"></div>
<div class="loading-text">Loading DICOM Image...</div>
<div class="loading-subtext">Please wait while we process your medical image</div>
</div>
<div id="controls">
<div id="actionControls">
<button onclick="resetView()"></button>
<button onclick="zoomIn()"></button>
<button onclick="zoomOut()"></button>
<button onclick="document.getElementById('fileInput').click()">📂</button>
</div>
<div id="frameControls">
<button onclick="prevFrame()"></button>
<input type="range" id="frameSlider" min="1" max="2" value="1" disabled>
<button onclick="nextFrame()"></button>
</div>
<div id="frameInfo"></div>
</div>
</div>
<!-- Hidden file input -->
<input type="file" id="fileInput" accept=".dcm" multiple>
<!-- Load from URL (Hidden for WebView usage) -->
<input type="text" id="dicomUrl" placeholder="Enter DICOM URL" size="50" style="display: none;">
<button id="loadUrlBtn" style="display: none;">Load URL</button>
<!-- Libraries -->
<script src="https://cdn.jsdelivr.net/npm/cornerstone-core@2.6.1/dist/cornerstone.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dicom-parser/dist/dicomParser.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/cornerstone-wado-image-loader@4.13.2/dist/cornerstoneWADOImageLoader.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/cornerstone-tools@4.22.1/dist/cornerstoneTools.min.js"></script>
<script>
const element = document.getElementById('dicomImage');
const frameInfo = document.getElementById('frameInfo');
const frameSlider = document.getElementById('frameSlider');
const loadingOverlay = document.getElementById('loadingOverlay');
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
cornerstone.enable(element);
cornerstoneTools.init({ showSVGCursors: true });
// Touch tools
cornerstoneTools.addTool(cornerstoneTools.ZoomTouchPinchTool);
cornerstoneTools.addTool(cornerstoneTools.StackScrollMultiTouchTool);
cornerstoneTools.addTool(cornerstoneTools.WwwcRegionTool);
cornerstoneTools.setToolActive('ZoomTouchPinch', {});
cornerstoneTools.setToolActive('StackScrollMultiTouch', {});
cornerstoneTools.setToolActive('WwwcRegion', {});
// 🔑 Listen for messages from React Native WebView
window.addEventListener('message', (event) => {
try {
const message = event.data;
// Handle direct URL string
if (typeof message === 'string' && message.startsWith('http')) {
loadDicom("wadouri:" + message);
document.getElementById('dicomUrl').value = message;
// Send success message back to React Native
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'success',
message: 'DICOM URL received and loading started'
}));
}
return;
}
// Handle structured JSON message
try {
const parsedMessage = JSON.parse(message);
if (parsedMessage.type === 'loadDicom' && parsedMessage.data) {
loadDicom("wadouri:" + parsedMessage.data);
document.getElementById('dicomUrl').value = parsedMessage.data;
// Send success message back to React Native
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'success',
message: 'Structured DICOM message received and loading started'
}));
}
}
} catch (parseError) {
// Message is not JSON, treating as string
}
} catch (error) {
// Send error message back to React Native
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'error',
message: 'Failed to process message: ' + error.message
}));
}
}
});
// For Android WebView compatibility
document.addEventListener('message', (event) => {
window.dispatchEvent(new MessageEvent('message', { data: event.data }));
});
// Notify React Native that WebView is ready
window.addEventListener('load', () => {
// Initialize slider appearance
frameSlider.style.opacity = '0.6';
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'ready',
message: 'DICOM Viewer is ready to receive URLs'
}));
}
});
// 🔑 Resize observer for responsiveness
window.addEventListener('resize', () => {
cornerstone.resize(element, true);
});
// Loading overlay control functions
function showLoading(message = 'Loading DICOM Image...', subtext = 'Please wait while we process your medical image') {
if (loadingOverlay) {
loadingOverlay.querySelector('.loading-text').textContent = message;
loadingOverlay.querySelector('.loading-subtext').textContent = subtext;
loadingOverlay.classList.remove('hidden');
}
}
function hideLoading() {
if (loadingOverlay) {
loadingOverlay.classList.add('hidden');
}
}
let stack = null;
// File input
document.getElementById('fileInput').addEventListener('change', e => {
const files = Array.from(e.target.files);
if (!files.length) return;
// Show loading while processing files
if (files.length === 1) {
showLoading('Processing DICOM File...', 'Loading selected medical image...');
setTimeout(() => {
loadDicom(cornerstoneWADOImageLoader.wadouri.fileManager.add(files[0]));
}, 100);
} else {
showLoading('Processing DICOM Files...', `Loading ${files.length} selected medical images...`);
setTimeout(() => {
loadSeries(files.map(file => cornerstoneWADOImageLoader.wadouri.fileManager.add(file)));
}, 100);
}
});
// URL load
document.getElementById('loadUrlBtn').addEventListener('click', () => {
const url = document.getElementById('dicomUrl').value.trim();
if (!url) return;
showLoading('Loading from URL...', 'Fetching DICOM image from provided URL...');
setTimeout(() => {
loadDicom("wadouri:" + url);
}, 100);
});
// Load dicom
function loadDicom(imageId) {
// Show loading overlay
showLoading('Loading DICOM Image...', 'Processing medical image data, please wait...');
cornerstone.loadImage(imageId).then(image => {
// Hide loading overlay on successful load
hideLoading();
cornerstone.displayImage(element, image);
cornerstone.resize(element, true); // ensure fit on load
const numFrames = parseInt(image.data.string('x00280008') || '1', 10);
// Always setup frame controls, even for single frames
if (numFrames > 1) {
stack = { currentImageIdIndex: 0, imageIds: [] };
for (let i = 0; i < numFrames; i++) {
stack.imageIds.push(imageId + `&frame=${i}`);
}
setupStack(stack);
} else {
// Single frame - setup basic stack for consistency
stack = { currentImageIdIndex: 0, imageIds: [imageId] };
setupStack(stack, true); // Pass true to indicate single frame
}
// Notify React Native of successful load
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'success',
message: 'DICOM image loaded and displayed successfully',
data: {
frames: numFrames,
imageId: imageId
}
}));
}
}).catch(err => {
// Hide loading overlay on error
hideLoading();
// Notify React Native of error
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'error',
message: 'Failed to load DICOM image: ' + err.message || err,
data: {
imageId: imageId,
error: err.message || err.toString()
}
}));
}
});
}
function loadSeries(imageIds) {
// Show loading overlay for series
showLoading('Loading DICOM Series...', `Processing ${imageIds.length} images, please wait...`);
stack = { currentImageIdIndex: 0, imageIds };
cornerstone.loadImage(imageIds[0]).then(image => {
// Hide loading overlay on successful load
hideLoading();
cornerstone.displayImage(element, image);
cornerstone.resize(element, true);
setupStack(stack, imageIds.length === 1);
}).catch(err => {
// Hide loading overlay on error
hideLoading();
// Handle error silently or send to React Native
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'error',
message: 'Failed to load DICOM series: ' + err.message || err
}));
}
});
}
function setupStack(s, isSingleFrame = false) {
cornerstoneTools.addStackStateManager(element, ['stack']);
cornerstoneTools.addToolState(element, 'stack', s);
// Handle single frame slider display
if (isSingleFrame || s.imageIds.length === 1) {
frameSlider.min = 1;
frameSlider.max = 2; // Set max to 2 to show proper indicator
frameSlider.value = 1;
frameSlider.disabled = true; // Disable interaction for single frame
frameSlider.style.opacity = '0.6'; // Visual indicator it's disabled
} else {
frameSlider.min = 1;
frameSlider.max = s.imageIds.length;
frameSlider.value = 1;
frameSlider.disabled = false; // Enable interaction for multiple frames
frameSlider.style.opacity = '1'; // Full opacity for enabled state
}
updateFrameInfo(s);
// Remove existing event listeners to avoid duplicates
element.removeEventListener('cornerstonetoolsstackscroll', updateFrameInfo);
frameSlider.removeEventListener('input', handleSliderInput);
// Add event listeners only if not single frame
if (!isSingleFrame && s.imageIds.length > 1) {
element.addEventListener('cornerstonetoolsstackscroll', () => updateFrameInfo(s));
frameSlider.addEventListener('input', handleSliderInput);
}
}
function handleSliderInput() {
const index = parseInt(frameSlider.value) - 1;
changeFrame(index);
}
function updateFrameInfo(s) {
frameInfo.textContent = `Image ${s.currentImageIdIndex + 1} of ${s.imageIds.length}`;
frameSlider.value = s.currentImageIdIndex + 1;
}
function changeFrame(index) {
if (!stack) return;
if (index < 0 || index >= stack.imageIds.length) return;
stack.currentImageIdIndex = index;
cornerstone.loadImage(stack.imageIds[index]).then(img => {
cornerstone.displayImage(element, img);
cornerstone.resize(element, true);
updateFrameInfo(stack);
}).catch(err => {
// Handle frame change error silently
});
}
function prevFrame() { if (stack) changeFrame(stack.currentImageIdIndex - 1); }
function nextFrame() { if (stack) changeFrame(stack.currentImageIdIndex + 1); }
function resetView() { cornerstone.reset(element); }
function zoomIn() {
const viewport = cornerstone.getViewport(element);
viewport.scale *= 1.2;
cornerstone.setViewport(element, viewport);
}
function zoomOut() {
const viewport = cornerstone.getViewport(element);
viewport.scale /= 1.2;
cornerstone.setViewport(element, viewport);
}
</script>
</body>
</html>