503 lines
16 KiB
HTML
503 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
|
<title>DICOM Viewer</title>
|
|
<style>
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
height: 100%;
|
|
background: rgb(8, 0, 0);
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
#dicomImage {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
color: white;
|
|
}
|
|
.loading {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
color: white;
|
|
}
|
|
.spinner {
|
|
border: 4px solid #f3f3f3;
|
|
border-top: 4px solid #2196F3;
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
animation: spin 1s linear infinite;
|
|
margin-bottom: 20px;
|
|
}
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
.error {
|
|
color: #F44336;
|
|
text-align: center;
|
|
padding: 20px;
|
|
}
|
|
.success {
|
|
color: #4CAF50;
|
|
text-align: center;
|
|
padding: 20px;
|
|
}
|
|
.debug {
|
|
position: fixed;
|
|
top: 10px;
|
|
right: 10px;
|
|
background: rgba(0,0,0,0.8);
|
|
color: white;
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
font-size: 12px;
|
|
max-width: 300px;
|
|
z-index: 1000;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="dicomImage">
|
|
<div class="loading">
|
|
<div class="spinner"></div>
|
|
<div>Loading DICOM Viewer...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="debug" class="debug" style="display: none;">
|
|
<strong>Debug Info:</strong><br>
|
|
<div id="debugContent"></div>
|
|
</div>
|
|
|
|
<script>
|
|
// Global variables
|
|
let cornerstone = null;
|
|
let cornerstoneWADOImageLoader = null;
|
|
let dicomParser = null;
|
|
let isLoaded = false;
|
|
let currentDicomUrl = null;
|
|
let debugMode = true;
|
|
|
|
// Debug logging function
|
|
function debugLog(message, type = 'info') {
|
|
if (debugMode) {
|
|
console.log(`[DICOM Viewer] ${message}`);
|
|
const debugContent = document.getElementById('debugContent');
|
|
if (debugContent) {
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
debugContent.innerHTML += `<div>[${timestamp}] ${message}</div>`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show debug panel
|
|
function showDebug() {
|
|
if (debugMode) {
|
|
document.getElementById('debug').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
// Function to load external libraries with retry
|
|
async function loadLibraries() {
|
|
try {
|
|
debugLog('Starting library loading process...');
|
|
showDebug();
|
|
|
|
// Load DICOM Parser first
|
|
await loadDicomParser();
|
|
|
|
// Load Cornerstone Core
|
|
await loadCornerstoneCore();
|
|
|
|
// Load Cornerstone WADO Image Loader with fallback
|
|
await loadCornerstoneWADO();
|
|
|
|
// Initialize viewer
|
|
initializeViewer();
|
|
|
|
// If we already have a DICOM URL, load it
|
|
if (currentDicomUrl) {
|
|
loadDicomImage(currentDicomUrl);
|
|
}
|
|
|
|
} catch (error) {
|
|
debugLog(`Error loading libraries: ${error.message}`, 'error');
|
|
showError(`Failed to load DICOM viewer libraries: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Load DICOM Parser
|
|
function loadDicomParser() {
|
|
return new Promise((resolve, reject) => {
|
|
debugLog('Loading DICOM Parser...');
|
|
|
|
const dicomParserScript = document.createElement('script');
|
|
dicomParserScript.src = 'https://unpkg.com/dicom-parser@1.8.6/dist/dicomParser.min.js';
|
|
dicomParserScript.onload = () => {
|
|
dicomParser = window.dicomParser;
|
|
debugLog('DICOM Parser loaded successfully');
|
|
resolve();
|
|
};
|
|
dicomParserScript.onerror = (error) => {
|
|
debugLog(`Failed to load DICOM Parser: ${error}`, 'error');
|
|
reject(new Error('Failed to load DICOM Parser'));
|
|
};
|
|
|
|
// Set timeout for loading
|
|
const timeout = setTimeout(() => {
|
|
reject(new Error('DICOM Parser loading timeout'));
|
|
}, 10000);
|
|
|
|
dicomParserScript.onload = () => {
|
|
clearTimeout(timeout);
|
|
dicomParser = window.dicomParser;
|
|
debugLog('DICOM Parser loaded successfully');
|
|
resolve();
|
|
};
|
|
|
|
document.head.appendChild(dicomParserScript);
|
|
});
|
|
}
|
|
|
|
// Load Cornerstone Core
|
|
function loadCornerstoneCore() {
|
|
return new Promise((resolve, reject) => {
|
|
debugLog('Loading Cornerstone Core...');
|
|
|
|
const cornerstoneScript = document.createElement('script');
|
|
cornerstoneScript.src = 'https://unpkg.com/cornerstone-core@2.3.0/dist/cornerstone.js';
|
|
cornerstoneScript.onload = () => {
|
|
cornerstone = window.cornerstone;
|
|
debugLog('Cornerstone Core loaded successfully');
|
|
resolve();
|
|
};
|
|
cornerstoneScript.onerror = (error) => {
|
|
debugLog(`Failed to load Cornerstone Core: ${error}`, 'error');
|
|
reject(new Error('Failed to load Cornerstone Core'));
|
|
};
|
|
|
|
// Set timeout for loading
|
|
const timeout = setTimeout(() => {
|
|
reject(new Error('Cornerstone Core loading timeout'));
|
|
}, 10000);
|
|
|
|
cornerstoneScript.onload = () => {
|
|
clearTimeout(timeout);
|
|
cornerstone = window.cornerstone;
|
|
debugLog('Cornerstone Core loaded successfully');
|
|
resolve();
|
|
};
|
|
|
|
document.head.appendChild(cornerstoneScript);
|
|
});
|
|
}
|
|
|
|
// Load Cornerstone WADO Image Loader with fallback
|
|
function loadCornerstoneWADO() {
|
|
return new Promise((resolve, reject) => {
|
|
debugLog('Loading Cornerstone WADO Image Loader...');
|
|
|
|
// Try multiple sources for WADO loader
|
|
const wadoSources = [
|
|
'https://unpkg.com/cornerstone-wado-image-loader@4.17.1/dist/cornerstoneWADOImageLoader.js',
|
|
'https://unpkg.com/cornerstone-wado-image-loader@4.16.0/dist/cornerstoneWADOImageLoader.js',
|
|
'https://unpkg.com/cornerstone-wado-image-loader@4.15.0/dist/cornerstoneWADOImageLoader.js',
|
|
'https://cdn.jsdelivr.net/npm/cornerstone-wado-image-loader@4.0.1/dist/cornerstoneWADOImageLoader.js'
|
|
];
|
|
|
|
let currentSourceIndex = 0;
|
|
|
|
function tryNextSource() {
|
|
if (currentSourceIndex >= wadoSources.length) {
|
|
reject(new Error('All WADO Image Loader sources failed'));
|
|
return;
|
|
}
|
|
|
|
const currentSource = wadoSources[currentSourceIndex];
|
|
debugLog(`Trying WADO source ${currentSourceIndex + 1}: ${currentSource}`);
|
|
|
|
const wadoScript = document.createElement('script');
|
|
wadoScript.src = currentSource;
|
|
|
|
wadoScript.onload = () => {
|
|
try {
|
|
cornerstoneWADOImageLoader = window.cornerstoneWADOImageLoader;
|
|
if (cornerstoneWADOImageLoader && cornerstone) {
|
|
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
|
debugLog(`Cornerstone WADO Image Loader loaded successfully from: ${currentSource}`);
|
|
resolve();
|
|
} else {
|
|
throw new Error('WADO loader not properly initialized');
|
|
}
|
|
} catch (error) {
|
|
debugLog(`WADO loader initialization failed: ${error.message}`, 'error');
|
|
currentSourceIndex++;
|
|
tryNextSource();
|
|
}
|
|
};
|
|
|
|
wadoScript.onerror = (error) => {
|
|
debugLog(`Failed to load WADO from ${currentSource}: ${JSON.stringify(error)}`, 'error');
|
|
currentSourceIndex++;
|
|
tryNextSource();
|
|
};
|
|
|
|
// Set timeout for loading
|
|
const timeout = setTimeout(() => {
|
|
debugLog(`WADO loader timeout from ${currentSource}`, 'error');
|
|
currentSourceIndex++;
|
|
tryNextSource();
|
|
}, 8000);
|
|
|
|
wadoScript.onload = () => {
|
|
clearTimeout(timeout);
|
|
try {
|
|
cornerstoneWADOImageLoader = window.cornerstoneWADOImageLoader;
|
|
if (cornerstoneWADOImageLoader && cornerstone) {
|
|
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
|
debugLog(`Cornerstone WADO Image Loader loaded successfully from: ${currentSource}`);
|
|
resolve();
|
|
} else {
|
|
throw new Error('WADO loader not properly initialized');
|
|
}
|
|
} catch (error) {
|
|
debugLog(`WADO loader initialization failed: ${error.message}`, 'error');
|
|
currentSourceIndex++;
|
|
tryNextSource();
|
|
}
|
|
};
|
|
|
|
document.head.appendChild(wadoScript);
|
|
}
|
|
|
|
tryNextSource();
|
|
});
|
|
}
|
|
|
|
// Initialize the viewer
|
|
function initializeViewer() {
|
|
try {
|
|
const element = document.getElementById('dicomImage');
|
|
cornerstone.enable(element);
|
|
isLoaded = true;
|
|
debugLog('Cornerstone viewer initialized successfully');
|
|
} catch (error) {
|
|
debugLog(`Error initializing viewer: ${error.message}`, 'error');
|
|
showError(`Failed to initialize DICOM viewer: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Load DICOM image with better error handling
|
|
async function loadDicomImage(dicomUrl) {
|
|
if (!isLoaded) {
|
|
debugLog('Libraries not loaded yet, storing URL');
|
|
currentDicomUrl = dicomUrl;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
showLoading();
|
|
debugLog(`Loading DICOM image from: ${dicomUrl}`);
|
|
|
|
// Test if URL is accessible first
|
|
await testUrlAccessibility(dicomUrl);
|
|
|
|
// Try to load with DICOM Parser first for validation
|
|
try {
|
|
await validateDicomWithParser(dicomUrl);
|
|
debugLog('DICOM file validated with parser');
|
|
} catch (parserError) {
|
|
debugLog(`DICOM parser validation failed: ${parserError.message}`, 'warning');
|
|
// Continue anyway, as Cornerstone might still be able to handle it
|
|
}
|
|
|
|
const element = document.getElementById('dicomImage');
|
|
const image = await cornerstone.loadImage(`wadouri:${dicomUrl}`);
|
|
cornerstone.displayImage(element, image);
|
|
|
|
showSuccess('DICOM image loaded successfully');
|
|
debugLog('DICOM image loaded successfully');
|
|
|
|
// Send success message to React Native
|
|
sendMessageToReactNative({
|
|
type: 'success',
|
|
message: 'DICOM image loaded successfully'
|
|
});
|
|
|
|
} catch (error) {
|
|
debugLog(`Error loading DICOM image: ${error.message}`, 'error');
|
|
showError(`Failed to load DICOM image: ${error.message}`);
|
|
|
|
// Send error message to React Native
|
|
sendMessageToReactNative({
|
|
type: 'error',
|
|
message: `Failed to load DICOM image: ${error.message}`
|
|
});
|
|
}
|
|
}
|
|
|
|
// Validate DICOM file with parser
|
|
async function validateDicomWithParser(url) {
|
|
try {
|
|
debugLog('Validating DICOM file with parser...');
|
|
const response = await fetch(url);
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
|
|
if (!dicomParser) {
|
|
throw new Error('DICOM Parser not available');
|
|
}
|
|
|
|
const dataSet = dicomParser.parseDicom(arrayBuffer);
|
|
debugLog(`DICOM file validated. Patient: ${dataSet.string('x00100010') || 'Unknown'}`);
|
|
debugLog(`Modality: ${dataSet.string('x00080060') || 'Unknown'}`);
|
|
debugLog(`Study Date: ${dataSet.string('x00080020') || 'Unknown'}`);
|
|
|
|
return dataSet;
|
|
} catch (error) {
|
|
debugLog(`DICOM parser validation failed: ${error.message}`, 'warning');
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Test URL accessibility
|
|
async function testUrlAccessibility(url) {
|
|
try {
|
|
debugLog(`Testing URL accessibility: ${url}`);
|
|
const response = await fetch(url, { method: 'HEAD' });
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const contentType = response.headers.get('content-type');
|
|
debugLog(`Content-Type: ${contentType}`);
|
|
|
|
if (contentType && !contentType.includes('application/dicom') && !contentType.includes('image/')) {
|
|
debugLog(`Warning: Unexpected content type: ${contentType}`);
|
|
}
|
|
|
|
} catch (error) {
|
|
debugLog(`URL accessibility test failed: ${error.message}`, 'error');
|
|
throw new Error(`URL not accessible: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Send message to React Native
|
|
function sendMessageToReactNative(data) {
|
|
if (window.ReactNativeWebView) {
|
|
try {
|
|
const message = JSON.stringify(data);
|
|
window.ReactNativeWebView.postMessage(message);
|
|
debugLog(`Message sent to React Native: ${message}`);
|
|
} catch (error) {
|
|
debugLog(`Failed to send message to React Native: ${error.message}`, 'error');
|
|
}
|
|
} else {
|
|
debugLog('ReactNativeWebView not available');
|
|
}
|
|
}
|
|
|
|
// Show loading state
|
|
function showLoading() {
|
|
const element = document.getElementById('dicomImage');
|
|
element.innerHTML = `
|
|
<div class="loading">
|
|
<div class="spinner"></div>
|
|
<div>Loading DICOM Image...</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Show error state
|
|
function showError(message) {
|
|
const element = document.getElementById('dicomImage');
|
|
element.innerHTML = `
|
|
<div class="error">
|
|
<h3>Error</h3>
|
|
<p>${message}</p>
|
|
<p>Please check the DICOM URL and try again.</p>
|
|
<button onclick="retryLoading()" style="background: #2196F3; color: white; border: none; padding: 10px 20px; border-radius: 5px; margin-top: 10px;">Retry</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Show success state
|
|
function showSuccess(message) {
|
|
const element = document.getElementById('dicomImage');
|
|
element.innerHTML = `
|
|
<div class="success">
|
|
<h3>Success</h3>
|
|
<p>${message}</p>
|
|
</div>
|
|
`;
|
|
|
|
// Clear success message after 2 seconds
|
|
setTimeout(() => {
|
|
if (element.querySelector('.success')) {
|
|
element.innerHTML = '';
|
|
}
|
|
}, 2000);
|
|
}
|
|
|
|
// Retry loading
|
|
function retryLoading() {
|
|
if (currentDicomUrl) {
|
|
debugLog('Retrying DICOM image loading...');
|
|
loadDicomImage(currentDicomUrl);
|
|
}
|
|
}
|
|
|
|
// Listen for messages from React Native
|
|
window.addEventListener('message', function(event) {
|
|
try {
|
|
const dicomUrl = event.data;
|
|
debugLog(`Received DICOM URL from React Native: ${dicomUrl}`);
|
|
|
|
if (dicomUrl && typeof dicomUrl === 'string') {
|
|
currentDicomUrl = dicomUrl;
|
|
if (isLoaded) {
|
|
loadDicomImage(dicomUrl);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
debugLog(`Error processing message from React Native: ${error.message}`, 'error');
|
|
showError('Invalid message received from app');
|
|
}
|
|
});
|
|
|
|
// Start loading libraries when page loads
|
|
window.addEventListener('load', () => {
|
|
debugLog('Page loaded, starting library loading...');
|
|
loadLibraries();
|
|
});
|
|
|
|
// Send ready message to React Native
|
|
sendMessageToReactNative({
|
|
type: 'ready',
|
|
message: 'DICOM viewer HTML loaded with DICOM Parser support'
|
|
});
|
|
|
|
// Global error handler
|
|
window.addEventListener('error', function(event) {
|
|
debugLog(`Global error: ${event.error?.message || event.message}`, 'error');
|
|
});
|
|
|
|
// Unhandled promise rejection handler
|
|
window.addEventListener('unhandledrejection', function(event) {
|
|
debugLog(`Unhandled promise rejection: ${event.reason}`, 'error');
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|