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

727 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, user-scalable=no">
<title>DICOM Viewer - Mobile Friendly</title>
<style>
#dicomImage {
width: 100%;
max-width: 512px;
height: 400px;
margin: 10px auto;
border: 1px solid #333;
background: black;
touch-action: none;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.preview-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 20px;
text-align: center;
min-height: 300px;
}
.preview-icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.7;
display: block;
}
.preview-text {
font-size: 16px;
color: #aaa;
margin-bottom: 8px;
display: block;
width: 100%;
}
.preview-subtext {
font-size: 14px;
color: #666;
line-height: 1.4;
display: block;
width: 100%;
}
.status {
background: #1a1a1a;
padding: 10px;
margin: 10px 0;
border-radius: 8px;
font-size: 14px;
width: 100%;
box-sizing: border-box;
}
.loading {
display: none;
color: #2196F3;
font-weight: bold;
}
.error {
color: #f44336;
background: #2d1b1b;
padding: 10px;
border-radius: 6px;
margin: 10px 0;
width: 100%;
box-sizing: border-box;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
align-items: center;
margin: 10px 0;
width: 100%;
box-sizing: border-box;
}
.control-group {
display: flex;
gap: 5px;
align-items: center;
flex-wrap: wrap;
justify-content: center;
}
.btn {
background: #2196F3;
color: white;
border: none;
padding: 8px 12px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
touch-action: manipulation;
min-height: 44px;
min-width: 44px;
white-space: nowrap;
}
.btn:active {
background: #1976D2;
transform: scale(0.95);
}
.btn.secondary {
background: #666;
}
.btn.danger {
background: #f44336;
}
.btn.success {
background: #4caf50;
}
.btn:disabled {
background: #555;
opacity: 0.6;
cursor: not-allowed;
}
.zoom-controls {
display: flex;
gap: 5px;
align-items: center;
}
.zoom-btn {
width: 44px;
height: 44px;
border-radius: 22px;
font-size: 18px;
font-weight: bold;
}
.frame-controls {
display: flex;
gap: 5px;
align-items: center;
}
.frame-nav {
display: flex;
gap: 5px;
align-items: center;
}
.frame-counter {
background: #333;
padding: 8px 12px;
border-radius: 6px;
min-width: 80px;
font-size: 14px;
text-align: center;
}
.header {
position: sticky;
top: 0;
background: #1a1a1a;
padding: 10px;
z-index: 100;
border-bottom: 1px solid #333;
width: 100%;
box-sizing: border-box;
}
.header h2 {
margin: 0 0 10px 0;
font-size: 20px;
}
body {
background: #111;
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
text-align: center;
margin: 0;
padding: 0;
overflow-x: hidden;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
* {
box-sizing: border-box;
}
@media (max-width: 480px) {
.controls {
flex-direction: column;
gap: 10px;
padding: 0 10px;
}
.control-group {
justify-content: center;
width: 100%;
}
.btn {
min-height: 48px;
min-width: 48px;
font-size: 16px;
flex: 1;
max-width: 120px;
}
.zoom-btn {
width: 48px;
height: 48px;
flex: none;
}
.preview-icon {
font-size: 48px;
}
.preview-text {
font-size: 14px;
}
.preview-subtext {
font-size: 12px;
}
.header {
padding: 15px 10px;
}
.header h2 {
font-size: 18px;
}
#dicomImage {
height: 300px;
margin: 10px;
max-width: calc(100% - 20px);
}
.preview-container {
min-height: 250px;
padding: 15px;
}
.status {
margin: 10px;
padding: 15px;
}
.frame-counter {
min-width: 60px;
font-size: 11px;
}
}
@media (max-width: 360px) {
.controls {
gap: 8px;
}
.btn {
min-height: 44px;
min-width: 44px;
font-size: 14px;
padding: 6px 8px;
}
.zoom-btn {
width: 44px;
height: 44px;
}
#dicomImage {
height: 250px;
}
.preview-container {
min-height: 200px;
padding: 10px;
}
.preview-icon {
font-size: 40px;
}
.preview-text {
font-size: 13px;
}
.preview-subtext {
font-size: 11px;
}
}
/* Touch feedback for mobile */
@media (hover: none) and (pointer: coarse) {
.btn:active {
transform: scale(0.95);
background: #1976D2;
}
}
/* Loading animation */
.loading {
display: inline-block;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
</head>
<body>
<div class="header">
<h2>DICOM Viewer</h2>
<div class="status">
<span id="statusText">Ready to load DICOM files</span>
<span class="loading" id="loadingText">Loading...</span>
</div>
</div>
<div class="controls">
<div class="control-group">
<button class="btn secondary" onclick="resetView()">Reset View</button>
<button class="btn danger" onclick="clearView()">Clear</button>
</div>
<div class="control-group zoom-controls">
<button class="btn zoom-btn" onclick="zoomIn()">+</button>
<button class="btn zoom-btn" onclick="zoomOut()"></button>
<button class="btn secondary" onclick="fitToWindow()">Fit</button>
</div>
<div class="control-group frame-controls">
<div class="frame-nav">
<button class="btn secondary" onclick="previousFrame()" id="prevFrameBtn" disabled></button>
<div class="frame-counter" id="frameInfo">No images</div>
<button class="btn secondary" onclick="nextFrame()" id="nextFrameBtn" disabled></button>
</div>
</div>
</div>
<div id="dicomImage">
<div class="preview-container">
<div class="preview-icon">🩻</div>
<div class="preview-text">DICOM Viewer</div>
<div class="preview-subtext">No image loaded<br>DICOM files will be loaded from parent component</div>
</div>
</div>
<!-- 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 statusText = document.getElementById('statusText');
const loadingText = document.getElementById('loadingText');
const prevFrameBtn = document.getElementById('prevFrameBtn');
const nextFrameBtn = document.getElementById('nextFrameBtn');
let currentStack = null;
let currentImage = null;
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
cornerstone.enable(element);
cornerstoneTools.init({ showSVGCursors: false });
// Add mobile-friendly tools
cornerstoneTools.addTool(cornerstoneTools.PanMultiTouchTool);
cornerstoneTools.addTool(cornerstoneTools.ZoomTouchPinchTool);
cornerstoneTools.addTool(cornerstoneTools.StackScrollMultiTouchTool);
cornerstoneTools.addTool(cornerstoneTools.WwwcRegionTool);
// Activate touch tools
cornerstoneTools.setToolActive('PanMultiTouch', {});
cornerstoneTools.setToolActive('ZoomTouchPinch', {});
cornerstoneTools.setToolActive('StackScrollMultiTouch', {});
cornerstoneTools.setToolActive('WwwcRegion', {});
// Function to load DICOM from parent component
function loadDicomFromParent(imageId) {
showLoading();
loadDicom(imageId);
}
// Function to load series from parent component
function loadSeriesFromParent(imageIds) {
showLoading();
loadSeries(imageIds);
}
// Listen for messages from parent component (React Native)
window.addEventListener('message', function(event) {
try {
const message = event.data;
console.log('Received message from parent:', message);
console.log('Message type:', typeof message);
console.log('Message length:', message ? message.length : 'undefined');
// If message is a string URL, treat it as a DICOM URL
if (typeof message === 'string' && message.trim()) {
const url = message.trim();
console.log('Processing string message as URL:', url);
if (url.startsWith('http') || url.startsWith('wadouri:')) {
console.log('Loading DICOM from parent URL:', url);
showLoading();
loadDicom(url);
} else {
console.log('Invalid URL format:', url);
showError('Invalid URL format received from parent: ' + url);
}
}
// If message is an object with URL property
else if (typeof message === 'object' && message.url) {
console.log('Loading DICOM from parent object:', message.url);
showLoading();
loadDicom(message.url);
}
// If message is an object with imageIds property (for series)
else if (typeof message === 'object' && message.imageIds && Array.isArray(message.imageIds)) {
console.log('Loading series from parent:', message.imageIds);
showLoading();
loadSeries(message.imageIds);
}
// If message has a type and data
else if (typeof message === 'object' && message.type === 'loadDicom' && message.data) {
console.log('Loading DICOM from parent with type:', message.data);
showLoading();
if (Array.isArray(message.data)) {
loadSeries(message.data);
} else {
loadDicom(message.data);
}
}
else {
console.log('Message format not recognized:', message);
console.log('Message keys:', message && typeof message === 'object' ? Object.keys(message) : 'N/A');
}
} catch (error) {
console.error('Error processing message from parent:', error);
showError('Error processing message from parent: ' + error.message);
}
});
// Also listen for postMessage calls directly
window.addEventListener('load', function() {
console.log('DICOM Viewer loaded and ready to receive messages');
console.log('Window ReactNativeWebView available:', !!window.ReactNativeWebView);
// Notify parent that viewer is ready
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'ready',
message: 'DICOM Viewer is ready to receive URLs'
}));
console.log('Sent ready message to parent');
}
// Also try to notify via postMessage
try {
window.parent.postMessage({
type: 'ready',
message: 'DICOM Viewer is ready to receive URLs'
}, '*');
console.log('Sent ready message via postMessage');
} catch (e) {
console.log('Could not send postMessage:', e);
}
});
// Debug function to test loading
function testLoadDicom() {
console.log('Testing DICOM loading...');
const testUrl = 'https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm';
console.log('Test URL:', testUrl);
showLoading();
loadDicom(testUrl);
}
// Add test button to header for debugging
document.addEventListener('DOMContentLoaded', function() {
const header = document.querySelector('.header');
if (header) {
const testButton = document.createElement('button');
testButton.textContent = 'Test Load';
testButton.className = 'btn success';
testButton.style.marginLeft = '10px';
testButton.onclick = testLoadDicom;
header.appendChild(testButton);
console.log('Added test button');
}
});
function showLoading() {
loadingText.style.display = 'inline';
statusText.style.display = 'none';
}
function hideLoading() {
loadingText.style.display = 'none';
statusText.style.display = 'inline';
}
function showError(message) {
hideLoading();
statusText.textContent = message;
statusText.style.color = '#f44336';
}
function showSuccess(message) {
hideLoading();
statusText.textContent = message;
statusText.style.color = '#4caf50';
}
function loadDicom(imageId) {
console.log('loadDicom called with:', imageId);
// Clear the preview container
element.innerHTML = '';
// Show loading state
showLoading();
cornerstone.loadImage(imageId).then(image => {
console.log('DICOM image loaded successfully:', image);
currentImage = image;
// Display the image
cornerstone.displayImage(element, image);
console.log('Image displayed on element');
const numFrames = parseInt(image.data.string('x00280008') || '1', 10);
console.log('Number of frames:', numFrames);
if (numFrames > 1) {
const stack = { currentImageIdIndex: 0, imageIds: [] };
for (let i = 0; i < numFrames; i++) {
stack.imageIds.push(imageId + `&frame=${i}`);
}
console.log('Setting up stack with frames:', stack.imageIds.length);
setupStack(stack);
} else {
currentStack = null;
updateFrameInfo();
}
showSuccess('DICOM loaded successfully');
fitToWindow();
console.log('DICOM loading completed');
}).catch(err => {
console.error('Error loading DICOM:', err);
console.error('Error details:', {
message: err.message,
stack: err.stack,
name: err.name
});
// Show error in the viewer
element.innerHTML = `
<div class="preview-container">
<div class="preview-icon">❌</div>
<div class="preview-text">Error Loading DICOM</div>
<div class="preview-subtext">${err.message}<br>URL: ${imageId}</div>
</div>
`;
showError('Error loading DICOM: ' + err.message);
});
}
function loadSeries(imageIds) {
const stack = { currentImageIdIndex: 0, imageIds };
cornerstone.loadImage(imageIds[0]).then(image => {
currentImage = image;
cornerstone.displayImage(element, image);
setupStack(stack);
showSuccess(`Series loaded: ${imageIds.length} images`);
fitToWindow();
}).catch(err => {
showError('Error loading series: ' + err.message);
});
}
function setupStack(stack) {
currentStack = stack;
cornerstoneTools.addStackStateManager(element, ['stack']);
cornerstoneTools.addToolState(element, 'stack', stack);
updateFrameInfo();
element.addEventListener('cornerstonetoolsstackscroll', () => {
updateFrameInfo();
});
}
function updateFrameInfo() {
if (currentStack) {
frameInfo.textContent = `${currentStack.currentImageIdIndex + 1} / ${currentStack.imageIds.length}`;
prevFrameBtn.disabled = currentStack.currentImageIdIndex === 0;
nextFrameBtn.disabled = currentStack.currentImageIdIndex === currentStack.imageIds.length - 1;
} else {
frameInfo.textContent = 'No images';
prevFrameBtn.disabled = true;
nextFrameBtn.disabled = true;
}
}
function previousFrame() {
if (currentStack && currentStack.currentImageIdIndex > 0) {
currentStack.currentImageIdIndex--;
cornerstone.loadImage(currentStack.imageIds[currentStack.currentImageIdIndex]).then(image => {
cornerstone.displayImage(element, image);
updateFrameInfo();
});
}
}
function nextFrame() {
if (currentStack && currentStack.currentImageIdIndex < currentStack.imageIds.length - 1) {
currentStack.currentImageIdIndex++;
cornerstone.loadImage(currentStack.imageIds[currentStack.currentImageIdIndex]).then(image => {
cornerstone.displayImage(element, image);
updateFrameInfo();
});
}
}
function zoomIn() {
const viewport = cornerstone.getViewport(element);
viewport.scale *= 1.5;
cornerstone.setViewport(element, viewport);
}
function zoomOut() {
const viewport = cornerstone.getViewport(element);
viewport.scale /= 1.5;
cornerstone.setViewport(element, viewport);
}
function fitToWindow() {
if (currentImage) {
cornerstone.fitToWindow(element);
}
}
function resetView() {
if (currentImage) {
cornerstone.reset(element);
fitToWindow();
}
}
function clearView() {
cornerstone.disable(element);
cornerstone.enable(element);
currentStack = null;
currentImage = null;
updateFrameInfo();
statusText.textContent = 'Ready to load DICOM files';
statusText.style.color = 'white';
// Show preview again
element.innerHTML = `
<div class="preview-container">
<div class="preview-icon">🩻</div>
<div class="preview-text">DICOM Viewer</div>
<div class="preview-subtext">No image loaded<br>DICOM files will be loaded from parent component</div>
</div>
`;
// Reactivate tools
cornerstoneTools.setToolActive('PanMultiTouch', {});
cornerstoneTools.setToolActive('ZoomTouchPinch', {});
cornerstoneTools.setToolActive('StackScrollMultiTouch', {});
cornerstoneTools.setToolActive('WwwcRegion', {});
}
// Initialize
updateFrameInfo();
// Expose functions for parent component
window.DicomViewer = {
loadDicom: loadDicomFromParent,
loadSeries: loadSeriesFromParent,
resetView: resetView,
clearView: clearView,
zoomIn: zoomIn,
zoomOut: zoomOut,
fitToWindow: fitToWindow,
previousFrame: previousFrame,
nextFrame: nextFrame
};
</script>
</body>
</html>