727 lines
18 KiB
HTML
727 lines
18 KiB
HTML
<!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>
|