ai prediction detil screen added
This commit is contained in:
parent
413a1d74de
commit
692a8156da
502
app/assets/dicom/dicom-viewer.html
Normal file
502
app/assets/dicom/dicom-viewer.html
Normal file
@ -0,0 +1,502 @@
|
||||
<!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>
|
||||
443
app/assets/dicom/test-dicom-viewer.html
Normal file
443
app/assets/dicom/test-dicom-viewer.html
Normal file
@ -0,0 +1,443 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DICOM Viewer Test</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.test-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.url-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.load-button {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.load-button:hover {
|
||||
background: #1976D2;
|
||||
}
|
||||
.viewer-container {
|
||||
margin-top: 20px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
min-height: 400px;
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
background: #f0f0f0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
font-weight: bold;
|
||||
}
|
||||
#dicomImage {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background: #000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
}
|
||||
.error {
|
||||
color: #F44336;
|
||||
background: #FFEBEE;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.success {
|
||||
color: #4CAF50;
|
||||
background: #E8F5E8;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.warning {
|
||||
color: #FF9800;
|
||||
background: #FFF3E0;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.sample-urls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.sample-url {
|
||||
background: #E3F2FD;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #BBDEFB;
|
||||
}
|
||||
.sample-url:hover {
|
||||
background: #BBDEFB;
|
||||
}
|
||||
.dicom-info {
|
||||
background: #F5F5F5;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 15px 0;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>DICOM Viewer Test</h1>
|
||||
<p>Test the DICOM viewer functionality in your browser before using it in React Native.</p>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>Sample DICOM URLs</h3>
|
||||
<div class="sample-urls">
|
||||
<div class="sample-url" onclick="loadSampleUrl('https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm')">
|
||||
<strong>Sample 1:</strong><br>
|
||||
LIDC-IDRI-0001
|
||||
</div>
|
||||
<div class="sample-url" onclick="loadSampleUrl('https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-002.dcm')">
|
||||
<strong>Sample 2:</strong><br>
|
||||
LIDC-IDRI-0001
|
||||
</div>
|
||||
<div class="sample-url" onclick="loadSampleUrl('https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-003.dcm')">
|
||||
<strong>Sample 3:</strong><br>
|
||||
LIDC-IDRI-0001
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>Custom DICOM URL</h3>
|
||||
<input type="text" id="customUrl" class="url-input" placeholder="Enter DICOM URL here..." />
|
||||
<button onclick="loadCustomUrl()" class="load-button">Load DICOM Image</button>
|
||||
</div>
|
||||
|
||||
<div class="viewer-container">
|
||||
<div class="status" id="status">Ready to load DICOM image</div>
|
||||
<div id="dicomImage">
|
||||
<div>Click a sample URL above or enter a custom URL to load a DICOM image</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="dicomInfo" class="dicom-info" style="display: none;">
|
||||
<strong>DICOM Information:</strong><br>
|
||||
<div id="dicomInfoContent"></div>
|
||||
</div>
|
||||
|
||||
<div id="messages"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let cornerstone = null;
|
||||
let cornerstoneWADOImageLoader = null;
|
||||
let dicomParser = null;
|
||||
let isLoaded = false;
|
||||
|
||||
// Load sample URL
|
||||
function loadSampleUrl(url) {
|
||||
document.getElementById('customUrl').value = url;
|
||||
loadDicomImage(url);
|
||||
}
|
||||
|
||||
// Load custom URL
|
||||
function loadCustomUrl() {
|
||||
const url = document.getElementById('customUrl').value.trim();
|
||||
if (url) {
|
||||
loadDicomImage(url);
|
||||
} else {
|
||||
showMessage('Please enter a valid URL', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Show message
|
||||
function showMessage(message, type = 'info') {
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = type;
|
||||
messageDiv.textContent = message;
|
||||
messagesDiv.appendChild(messageDiv);
|
||||
|
||||
// Remove message after 5 seconds
|
||||
setTimeout(() => {
|
||||
messageDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Update status
|
||||
function updateStatus(message) {
|
||||
document.getElementById('status').textContent = message;
|
||||
}
|
||||
|
||||
// Show DICOM info
|
||||
function showDicomInfo(info) {
|
||||
const infoDiv = document.getElementById('dicomInfo');
|
||||
const contentDiv = document.getElementById('dicomInfoContent');
|
||||
|
||||
if (info) {
|
||||
contentDiv.innerHTML = info;
|
||||
infoDiv.style.display = 'block';
|
||||
} else {
|
||||
infoDiv.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Load libraries
|
||||
async function loadLibraries() {
|
||||
try {
|
||||
updateStatus('Loading DICOM viewer libraries...');
|
||||
|
||||
// Load DICOM Parser first
|
||||
await loadScript('https://unpkg.com/dicom-parser@1.8.6/dist/dicomParser.min.js');
|
||||
dicomParser = window.dicomParser;
|
||||
showMessage('DICOM Parser loaded successfully', 'success');
|
||||
|
||||
// Load Cornerstone Core
|
||||
await loadScript('https://unpkg.com/cornerstone-core@2.3.0/dist/cornerstone.js');
|
||||
cornerstone = window.cornerstone;
|
||||
|
||||
// Load Cornerstone WADO Image Loader with fallback
|
||||
await loadCornerstoneWADO();
|
||||
|
||||
isLoaded = true;
|
||||
updateStatus('Libraries loaded successfully');
|
||||
showMessage('All DICOM viewer libraries loaded successfully', 'success');
|
||||
|
||||
// Initialize viewer
|
||||
const element = document.getElementById('dicomImage');
|
||||
cornerstone.enable(element);
|
||||
|
||||
} catch (error) {
|
||||
updateStatus('Failed to load libraries');
|
||||
showMessage(`Failed to load libraries: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Load script
|
||||
function loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.onload = resolve;
|
||||
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
// Load Cornerstone WADO Image Loader with fallback
|
||||
function loadCornerstoneWADO() {
|
||||
return new Promise((resolve, reject) => {
|
||||
updateStatus('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.17.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];
|
||||
updateStatus(`Trying WADO source ${currentSourceIndex + 1}: ${currentSource.split('/').pop()}`);
|
||||
|
||||
const wadoScript = document.createElement('script');
|
||||
wadoScript.src = currentSource;
|
||||
|
||||
wadoScript.onload = () => {
|
||||
try {
|
||||
cornerstoneWADOImageLoader = window.cornerstoneWADOImageLoader;
|
||||
if (cornerstoneWADOImageLoader && cornerstone) {
|
||||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||||
showMessage(`WADO Image Loader loaded successfully from: ${currentSource.split('/').pop()}`, 'success');
|
||||
resolve();
|
||||
} else {
|
||||
throw new Error('WADO loader not properly initialized');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage(`WADO loader initialization failed: ${error.message}`, 'warning');
|
||||
currentSourceIndex++;
|
||||
tryNextSource();
|
||||
}
|
||||
};
|
||||
|
||||
wadoScript.onerror = (error) => {
|
||||
showMessage(`Failed to load WADO from ${currentSource.split('/').pop()}`, 'warning');
|
||||
currentSourceIndex++;
|
||||
tryNextSource();
|
||||
};
|
||||
|
||||
// Set timeout for loading
|
||||
const timeout = setTimeout(() => {
|
||||
showMessage(`WADO loader timeout from ${currentSource.split('/').pop()}`, 'warning');
|
||||
currentSourceIndex++;
|
||||
tryNextSource();
|
||||
}, 8000);
|
||||
|
||||
wadoScript.onload = () => {
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
cornerstoneWADOImageLoader = window.cornerstoneWADOImageLoader;
|
||||
if (cornerstoneWADOImageLoader && cornerstone) {
|
||||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||||
showMessage(`WADO Image Loader loaded successfully from: ${currentSource.split('/').pop()}`, 'success');
|
||||
resolve();
|
||||
} else {
|
||||
throw new Error('WADO loader not properly initialized');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage(`WADO loader initialization failed: ${error.message}`, 'warning');
|
||||
currentSourceIndex++;
|
||||
tryNextSource();
|
||||
}
|
||||
};
|
||||
|
||||
document.head.appendChild(wadoScript);
|
||||
}
|
||||
|
||||
tryNextSource();
|
||||
});
|
||||
}
|
||||
|
||||
// Load DICOM image
|
||||
async function loadDicomImage(url) {
|
||||
if (!isLoaded) {
|
||||
showMessage('Libraries not loaded yet, please wait...', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
updateStatus('Loading DICOM image...');
|
||||
showMessage(`Loading DICOM image from: ${url}`, 'info');
|
||||
|
||||
// Test URL accessibility
|
||||
await testUrl(url);
|
||||
|
||||
// Validate DICOM with parser
|
||||
let dicomInfo = null;
|
||||
try {
|
||||
dicomInfo = await validateDicomWithParser(url);
|
||||
showMessage('DICOM file validated successfully', 'success');
|
||||
} catch (parserError) {
|
||||
showMessage(`DICOM validation warning: ${parserError.message}`, 'warning');
|
||||
}
|
||||
|
||||
const element = document.getElementById('dicomImage');
|
||||
const image = await cornerstone.loadImage(`wadouri:${url}`);
|
||||
cornerstone.displayImage(element, image);
|
||||
|
||||
updateStatus('DICOM image loaded successfully');
|
||||
showMessage('DICOM image loaded successfully!', 'success');
|
||||
|
||||
// Display DICOM information if available
|
||||
if (dicomInfo) {
|
||||
displayDicomInfo(dicomInfo);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
updateStatus('Failed to load DICOM image');
|
||||
showMessage(`Failed to load DICOM image: ${error.message}`, 'error');
|
||||
showDicomInfo(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate DICOM with parser
|
||||
async function validateDicomWithParser(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
|
||||
if (!dicomParser) {
|
||||
throw new Error('DICOM Parser not available');
|
||||
}
|
||||
|
||||
const dataSet = dicomParser.parseDicom(arrayBuffer);
|
||||
return dataSet;
|
||||
} catch (error) {
|
||||
throw new Error(`DICOM validation failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Display DICOM information
|
||||
function displayDicomInfo(dataSet) {
|
||||
try {
|
||||
const info = [
|
||||
`Patient Name: ${dataSet.string('x00100010') || 'Unknown'}`,
|
||||
`Patient ID: ${dataSet.string('x00100020') || 'Unknown'}`,
|
||||
`Modality: ${dataSet.string('x00080060') || 'Unknown'}`,
|
||||
`Study Date: ${dataSet.string('x00080020') || 'Unknown'}`,
|
||||
`Study Description: ${dataSet.string('x00081030') || 'Unknown'}`,
|
||||
`Manufacturer: ${dataSet.string('x00080070') || 'Unknown'}`,
|
||||
`Image Size: ${dataSet.uint16('x00280010') || 'Unknown'} x ${dataSet.uint16('x00280011') || 'Unknown'}`,
|
||||
`Bits Allocated: ${dataSet.uint16('x00280100') || 'Unknown'}`,
|
||||
`Samples per Pixel: ${dataSet.uint16('x00280002') || 'Unknown'}`
|
||||
].join('<br>');
|
||||
|
||||
showDicomInfo(info);
|
||||
} catch (error) {
|
||||
console.error('Error displaying DICOM info:', error);
|
||||
showDicomInfo('Error displaying DICOM information');
|
||||
}
|
||||
}
|
||||
|
||||
// Test URL accessibility
|
||||
async function testUrl(url) {
|
||||
try {
|
||||
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');
|
||||
console.log('Content-Type:', contentType);
|
||||
|
||||
if (contentType && !contentType.includes('application/dicom') && !contentType.includes('image/')) {
|
||||
console.warn('Warning: Unexpected content type:', contentType);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`URL not accessible: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when page loads
|
||||
window.addEventListener('load', loadLibraries);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -13,7 +13,7 @@ import { theme } from '../../../theme';
|
||||
|
||||
// Import screens
|
||||
import { AIPredictionsScreen, AIPredictionDetailScreen } from '../screens';
|
||||
import { ComingSoonScreen } from '../../../shared/components';
|
||||
import { ComingSoonScreen, DicomViewer } from '../../../shared/components';
|
||||
|
||||
// Import types
|
||||
import type { AIPredictionStackParamList } from './navigationTypes';
|
||||
@ -152,7 +152,7 @@ const AIPredictionStackNavigator: React.FC = () => {
|
||||
{/* AI Prediction Details Screen */}
|
||||
<Stack.Screen
|
||||
name="AIPredictionDetails"
|
||||
component={AIPredictionDetailScreen}
|
||||
component={() => <DicomViewer dicomUrl={'https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm'} />}
|
||||
options={({ navigation, route }) => ({
|
||||
title: 'Create Suggestion',
|
||||
headerLeft: () => (
|
||||
|
||||
@ -543,8 +543,8 @@ const AIPredictionDetailScreen: React.FC<AIPredictionDetailsScreenProps> = ({
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
||||
|
||||
<TouchableWithoutFeedback onPress={closeAllDropdowns}>
|
||||
<>
|
||||
<TouchableWithoutFeedback onPress={closeAllDropdowns} disabled={!showSuggestionTypeDropdown && !showPriorityDropdown}>
|
||||
<View style={{flex:1}}>
|
||||
{/* Enhanced Header */}
|
||||
{renderHeader()}
|
||||
|
||||
@ -860,7 +860,7 @@ const AIPredictionDetailScreen: React.FC<AIPredictionDetailsScreenProps> = ({
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
</>
|
||||
</View>
|
||||
|
||||
</TouchableWithoutFeedback>
|
||||
|
||||
|
||||
@ -470,15 +470,15 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
||||
/>
|
||||
}
|
||||
// Performance optimizations
|
||||
removeClippedSubviews={true}
|
||||
maxToRenderPerBatch={10}
|
||||
windowSize={10}
|
||||
initialNumToRender={8}
|
||||
getItemLayout={(data, index) => ({
|
||||
length: 120, // Approximate height of PatientCard
|
||||
offset: 120 * index,
|
||||
index,
|
||||
})}
|
||||
// removeClippedSubviews={true}
|
||||
// maxToRenderPerBatch={10}
|
||||
// windowSize={10}
|
||||
// initialNumToRender={8}
|
||||
// getItemLayout={(data, index) => ({
|
||||
// length: 120, // Approximate height of PatientCard
|
||||
// offset: 120 * index,
|
||||
// index,
|
||||
// })}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
207
app/shared/components/DICOM_VIEWER_README.md
Normal file
207
app/shared/components/DICOM_VIEWER_README.md
Normal file
@ -0,0 +1,207 @@
|
||||
# DICOM Viewer Component
|
||||
|
||||
## Overview
|
||||
The DICOM Viewer component is a React Native component that uses WebView to display DICOM medical imaging files. It integrates with Cornerstone.js and Cornerstone WADO Image Loader for robust DICOM file handling.
|
||||
|
||||
## Features
|
||||
- ✅ WebView-based DICOM rendering
|
||||
- ✅ Cornerstone.js integration for medical imaging
|
||||
- ✅ Support for remote DICOM URLs
|
||||
- ✅ Loading states and error handling
|
||||
- ✅ Real-time communication with React Native
|
||||
- ✅ Responsive design for mobile devices
|
||||
|
||||
## Components
|
||||
|
||||
### 1. DicomViewer
|
||||
The main DICOM viewer component.
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
interface DicomViewerProps {
|
||||
dicomUrl: string; // URL to the DICOM file
|
||||
onError?: (error: string) => void; // Error callback
|
||||
onLoad?: () => void; // Load success callback
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { DicomViewer } from '../shared/components';
|
||||
|
||||
<DicomViewer
|
||||
dicomUrl="https://example.com/sample.dcm"
|
||||
onError={(error) => console.error('DICOM Error:', error)}
|
||||
onLoad={() => console.log('DICOM loaded successfully')}
|
||||
/>
|
||||
```
|
||||
|
||||
### 2. DicomViewerTest
|
||||
A test component for testing different DICOM URLs and debugging issues.
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { DicomViewerTest } from '../shared/components';
|
||||
|
||||
<DicomViewerTest />
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. WebView Setup
|
||||
- Loads a local HTML file (`dicom-viewer.html`)
|
||||
- Enables JavaScript and DOM storage
|
||||
- Allows file access and universal access from file URLs
|
||||
|
||||
### 2. Library Loading
|
||||
- Dynamically loads Cornerstone.js from CDN
|
||||
- Loads Cornerstone WADO Image Loader
|
||||
- Initializes the viewer when libraries are ready
|
||||
|
||||
### 3. DICOM Processing
|
||||
- Receives DICOM URL from React Native via postMessage
|
||||
- Uses Cornerstone to load and display the DICOM image
|
||||
- Handles errors and success states
|
||||
|
||||
### 4. Communication
|
||||
- Sends status messages back to React Native
|
||||
- Reports loading, success, and error states
|
||||
- Enables debugging and user feedback
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Black Screen Issues
|
||||
|
||||
#### 1. Check Console Logs
|
||||
Open the React Native debugger and check for:
|
||||
- WebView loading errors
|
||||
- JavaScript execution errors
|
||||
- Network request failures
|
||||
|
||||
#### 2. Verify DICOM URL
|
||||
- Ensure the URL is accessible from the device
|
||||
- Check if the URL returns a valid DICOM file
|
||||
- Verify CORS settings if loading from a different domain
|
||||
|
||||
#### 3. Library Loading Issues
|
||||
- Check internet connectivity (libraries load from CDN)
|
||||
- Verify the HTML file path is correct
|
||||
- Check WebView permissions and settings
|
||||
|
||||
#### 4. Platform-Specific Issues
|
||||
|
||||
**Android:**
|
||||
- Ensure `allowFileAccess` is enabled
|
||||
- Check if the HTML file is in the correct assets folder
|
||||
- Verify WebView permissions in AndroidManifest.xml
|
||||
|
||||
**iOS:**
|
||||
- Check WebView configuration in Info.plist
|
||||
- Ensure JavaScript is enabled
|
||||
- Verify file access permissions
|
||||
|
||||
### Common Error Messages
|
||||
|
||||
#### "Failed to load DICOM viewer libraries"
|
||||
- Check internet connectivity
|
||||
- Verify CDN URLs are accessible
|
||||
- Check WebView JavaScript settings
|
||||
|
||||
#### "Failed to load DICOM image"
|
||||
- Verify DICOM URL is accessible
|
||||
- Check if the file is a valid DICOM format
|
||||
- Ensure the server supports CORS
|
||||
|
||||
#### "Invalid message received from app"
|
||||
- Check the message format being sent
|
||||
- Verify the postMessage implementation
|
||||
- Check WebView message handling
|
||||
|
||||
## Testing
|
||||
|
||||
### 1. Use Sample URLs
|
||||
The test component includes sample DICOM URLs that are known to work:
|
||||
- Sample DICOM 1-3 from OHIF examples
|
||||
|
||||
### 2. Test Custom URLs
|
||||
- Enter your own DICOM URLs
|
||||
- Test with different file formats
|
||||
- Verify error handling
|
||||
|
||||
### 3. Debug Mode
|
||||
- Check console logs in React Native debugger
|
||||
- Monitor WebView messages
|
||||
- Use the test component for isolated testing
|
||||
|
||||
## Performance Tips
|
||||
|
||||
### 1. Image Optimization
|
||||
- Use compressed DICOM files when possible
|
||||
- Consider implementing progressive loading
|
||||
- Cache frequently accessed images
|
||||
|
||||
### 2. Memory Management
|
||||
- Dispose of WebView when not in use
|
||||
- Monitor memory usage with large DICOM files
|
||||
- Implement proper cleanup in useEffect
|
||||
|
||||
### 3. Network Optimization
|
||||
- Use CDN for DICOM files when possible
|
||||
- Implement retry logic for failed requests
|
||||
- Consider offline caching for critical images
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### 1. URL Validation
|
||||
- Validate DICOM URLs before loading
|
||||
- Implement URL whitelisting if needed
|
||||
- Sanitize user input for custom URLs
|
||||
|
||||
### 2. WebView Security
|
||||
- Limit WebView permissions to minimum required
|
||||
- Implement proper origin whitelisting
|
||||
- Monitor for malicious content
|
||||
|
||||
### 3. Data Privacy
|
||||
- Ensure DICOM files don't contain PHI
|
||||
- Implement proper data handling protocols
|
||||
- Follow HIPAA compliance guidelines
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### 1. Offline Support
|
||||
- Bundle Cornerstone libraries locally
|
||||
- Implement offline DICOM caching
|
||||
- Support for local DICOM files
|
||||
|
||||
### 2. Advanced Features
|
||||
- Multi-planar reconstruction (MPR)
|
||||
- Measurement tools
|
||||
- Annotation capabilities
|
||||
- 3D rendering support
|
||||
|
||||
### 3. Performance Improvements
|
||||
- WebAssembly integration
|
||||
- GPU acceleration
|
||||
- Progressive image loading
|
||||
- Background processing
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions:
|
||||
1. Check this README for common solutions
|
||||
2. Review console logs and error messages
|
||||
3. Test with sample URLs first
|
||||
4. Verify WebView configuration
|
||||
5. Check platform-specific requirements
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `react-native-webview`: WebView component
|
||||
- `cornerstone-core`: Medical imaging library
|
||||
- `cornerstone-wado-image-loader`: DICOM file loader
|
||||
|
||||
## License
|
||||
|
||||
Design & Developed by Tech4Biz Solutions
|
||||
Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
306
app/shared/components/DicomViewer.tsx
Normal file
306
app/shared/components/DicomViewer.tsx
Normal file
@ -0,0 +1,306 @@
|
||||
/*
|
||||
* File: DicomViewer.tsx
|
||||
* Description: DICOM viewer component using WebView for medical imaging
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { WebView, WebViewMessageEvent } from 'react-native-webview';
|
||||
import { Platform, View, Text, StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native';
|
||||
|
||||
// Interface for component props
|
||||
interface DicomViewerProps {
|
||||
dicomUrl: string;
|
||||
onError?: (error: string) => void;
|
||||
onLoad?: () => void;
|
||||
debugMode?: boolean;
|
||||
}
|
||||
|
||||
// Interface for WebView reference
|
||||
interface WebViewRef {
|
||||
postMessage: (message: string) => void;
|
||||
reload: () => void;
|
||||
}
|
||||
|
||||
export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = false }: DicomViewerProps): React.ReactElement {
|
||||
const webViewRef = useRef<WebViewRef>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const [debugInfo, setDebugInfo] = useState<string[]>([]);
|
||||
const [webViewReady, setWebViewReady] = useState(false);
|
||||
|
||||
// Debug logging function
|
||||
const debugLog = (message: string) => {
|
||||
if (debugMode) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logMessage = `[${timestamp}] ${message}`;
|
||||
console.log(logMessage);
|
||||
setDebugInfo(prev => [...prev.slice(-9), logMessage]); // Keep last 10 messages
|
||||
}
|
||||
};
|
||||
|
||||
// Handle WebView load events
|
||||
const handleLoadStart = () => {
|
||||
debugLog('WebView load started');
|
||||
setIsLoading(true);
|
||||
setHasError(false);
|
||||
};
|
||||
|
||||
const handleLoadEnd = () => {
|
||||
debugLog('WebView load ended');
|
||||
setIsLoading(false);
|
||||
setWebViewReady(true);
|
||||
onLoad?.();
|
||||
};
|
||||
|
||||
const handleError = (error: any) => {
|
||||
debugLog(`WebView error: ${JSON.stringify(error)}`);
|
||||
setIsLoading(false);
|
||||
setHasError(true);
|
||||
onError?.(error?.nativeEvent?.description || 'Failed to load DICOM viewer');
|
||||
};
|
||||
|
||||
const handleMessage = (event: WebViewMessageEvent) => {
|
||||
try {
|
||||
const message = event.nativeEvent.data;
|
||||
debugLog(`Message from WebView: ${message}`);
|
||||
|
||||
// Try to parse JSON message
|
||||
if (typeof message === 'string') {
|
||||
try {
|
||||
const parsedMessage = JSON.parse(message);
|
||||
debugLog(`Parsed message: ${JSON.stringify(parsedMessage)}`);
|
||||
|
||||
if (parsedMessage.type === 'error') {
|
||||
setHasError(true);
|
||||
onError?.(parsedMessage.message);
|
||||
} else if (parsedMessage.type === 'success') {
|
||||
setHasError(false);
|
||||
}
|
||||
} catch (parseError) {
|
||||
debugLog(`Failed to parse message as JSON: ${parseError}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
debugLog(`Error handling WebView message: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Send DICOM URL to WebView when component mounts or URL changes
|
||||
useEffect(() => {
|
||||
if (webViewRef.current && dicomUrl && webViewReady) {
|
||||
debugLog(`Sending DICOM URL to WebView: ${dicomUrl}`);
|
||||
|
||||
// Wait a bit for WebView to be ready
|
||||
const timer = setTimeout(() => {
|
||||
if (webViewRef.current) {
|
||||
try {
|
||||
webViewRef.current.postMessage(dicomUrl);
|
||||
debugLog('DICOM URL sent successfully');
|
||||
} catch (error) {
|
||||
debugLog(`Failed to send DICOM URL: ${error}`);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [dicomUrl, webViewReady]);
|
||||
|
||||
// Reload WebView if there's an error
|
||||
const handleRetry = () => {
|
||||
debugLog('Retrying WebView load');
|
||||
if (webViewRef.current) {
|
||||
setHasError(false);
|
||||
setIsLoading(true);
|
||||
setWebViewReady(false);
|
||||
webViewRef.current.reload();
|
||||
}
|
||||
};
|
||||
|
||||
// Clear debug info
|
||||
const clearDebugInfo = () => {
|
||||
setDebugInfo([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<WebView
|
||||
ref={webViewRef as any}
|
||||
source={require('../../assets/dicom/dicom-viewer.html')}
|
||||
originWhitelist={['*']}
|
||||
javaScriptEnabled
|
||||
domStorageEnabled
|
||||
allowFileAccess
|
||||
allowUniversalAccessFromFileURLs
|
||||
allowFileAccessFromFileURLs
|
||||
onLoadStart={handleLoadStart}
|
||||
onLoadEnd={handleLoadEnd}
|
||||
onError={handleError}
|
||||
onMessage={handleMessage}
|
||||
style={styles.webview}
|
||||
startInLoadingState
|
||||
renderLoading={() => (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color="#2196F3" />
|
||||
<Text style={styles.loadingText}>Loading DICOM Viewer...</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
|
||||
{hasError && (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>Failed to load DICOM viewer</Text>
|
||||
<Text style={styles.errorDetails}>
|
||||
URL: {dicomUrl}
|
||||
</Text>
|
||||
<TouchableOpacity style={styles.retryButton} onPress={handleRetry}>
|
||||
<Text style={styles.retryButtonText}>Tap to retry</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{debugMode && (
|
||||
<View style={styles.debugContainer}>
|
||||
<View style={styles.debugHeader}>
|
||||
<Text style={styles.debugTitle}>Debug Info</Text>
|
||||
<TouchableOpacity onPress={clearDebugInfo}>
|
||||
<Text style={styles.clearButton}>Clear</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.debugContent}>
|
||||
{debugInfo.map((info, index) => (
|
||||
<Text key={index} style={styles.debugText}>{info}</Text>
|
||||
))}
|
||||
</View>
|
||||
<View style={styles.debugStatus}>
|
||||
<Text style={styles.debugStatusText}>
|
||||
WebView Ready: {webViewReady ? 'Yes' : 'No'}
|
||||
</Text>
|
||||
<Text style={styles.debugStatusText}>
|
||||
Loading: {isLoading ? 'Yes' : 'No'}
|
||||
</Text>
|
||||
<Text style={styles.debugStatusText}>
|
||||
Error: {hasError ? 'Yes' : 'No'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
webview: {
|
||||
flex: 1,
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
loadingContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
loadingText: {
|
||||
color: '#FFF',
|
||||
marginTop: 16,
|
||||
fontSize: 16,
|
||||
},
|
||||
errorContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#000',
|
||||
padding: 20,
|
||||
},
|
||||
errorText: {
|
||||
color: '#F44336',
|
||||
fontSize: 18,
|
||||
textAlign: 'center',
|
||||
marginBottom: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
errorDetails: {
|
||||
color: '#FF9800',
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
marginBottom: 20,
|
||||
fontFamily: 'monospace',
|
||||
},
|
||||
retryButton: {
|
||||
backgroundColor: '#2196F3',
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
},
|
||||
retryButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
debugContainer: {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
right: 10,
|
||||
backgroundColor: 'rgba(0,0,0,0.9)',
|
||||
borderRadius: 8,
|
||||
padding: 10,
|
||||
maxWidth: 300,
|
||||
maxHeight: 400,
|
||||
},
|
||||
debugHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
debugTitle: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
clearButton: {
|
||||
color: '#2196F3',
|
||||
fontSize: 12,
|
||||
textDecorationLine: 'underline',
|
||||
},
|
||||
debugContent: {
|
||||
maxHeight: 200,
|
||||
},
|
||||
debugText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 10,
|
||||
fontFamily: 'monospace',
|
||||
marginBottom: 2,
|
||||
},
|
||||
debugStatus: {
|
||||
marginTop: 8,
|
||||
paddingTop: 8,
|
||||
borderTopColor: '#333',
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
debugStatusText: {
|
||||
color: '#CCC',
|
||||
fontSize: 10,
|
||||
marginBottom: 2,
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
* End of File: DicomViewer.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
252
app/shared/components/DicomViewerTest.tsx
Normal file
252
app/shared/components/DicomViewerTest.tsx
Normal file
@ -0,0 +1,252 @@
|
||||
/*
|
||||
* File: DicomViewerTest.tsx
|
||||
* Description: Test component for DICOM viewer with sample URLs
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, StyleSheet, TextInput, TouchableOpacity, ScrollView, Alert } from 'react-native';
|
||||
import DicomViewer from './DicomViewer';
|
||||
|
||||
// Sample DICOM URLs for testing
|
||||
const SAMPLE_DICOM_URLS = [
|
||||
{
|
||||
name: 'Sample DICOM 1',
|
||||
url: 'https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm'
|
||||
},
|
||||
{
|
||||
name: 'Sample DICOM 2',
|
||||
url: 'https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-002.dcm'
|
||||
},
|
||||
{
|
||||
name: 'Sample DICOM 3',
|
||||
url: 'https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-003.dcm'
|
||||
}
|
||||
];
|
||||
|
||||
export default function DicomViewerTest(): React.ReactElement {
|
||||
const [dicomUrl, setDicomUrl] = useState(SAMPLE_DICOM_URLS[0].url);
|
||||
const [customUrl, setCustomUrl] = useState('');
|
||||
|
||||
const handleUrlSelect = (url: string) => {
|
||||
setDicomUrl(url);
|
||||
setCustomUrl(url);
|
||||
};
|
||||
|
||||
const handleCustomUrlSubmit = () => {
|
||||
if (customUrl.trim()) {
|
||||
setDicomUrl(customUrl.trim());
|
||||
} else {
|
||||
Alert.alert('Error', 'Please enter a valid URL');
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewerError = (error: string) => {
|
||||
console.error('DICOM Viewer Error:', error);
|
||||
Alert.alert('DICOM Viewer Error', error);
|
||||
};
|
||||
|
||||
const handleViewerLoad = () => {
|
||||
console.log('DICOM Viewer loaded successfully');
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>DICOM Viewer Test</Text>
|
||||
<Text style={styles.subtitle}>Test different DICOM URLs</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.urlSection}>
|
||||
<Text style={styles.sectionTitle}>Sample DICOM URLs:</Text>
|
||||
{SAMPLE_DICOM_URLS.map((sample, index) => (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={[
|
||||
styles.urlButton,
|
||||
dicomUrl === sample.url && styles.selectedUrlButton
|
||||
]}
|
||||
onPress={() => handleUrlSelect(sample.url)}
|
||||
>
|
||||
<Text style={[
|
||||
styles.urlButtonText,
|
||||
dicomUrl === sample.url && styles.selectedUrlButtonText
|
||||
]}>
|
||||
{sample.name}
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.urlText,
|
||||
dicomUrl === sample.url && styles.selectedUrlText
|
||||
]}>
|
||||
{sample.url}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
|
||||
<View style={styles.customUrlSection}>
|
||||
<Text style={styles.sectionTitle}>Custom DICOM URL:</Text>
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
value={customUrl}
|
||||
onChangeText={setCustomUrl}
|
||||
placeholder="Enter DICOM URL..."
|
||||
placeholderTextColor="#999"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.submitButton}
|
||||
onPress={handleCustomUrlSubmit}
|
||||
>
|
||||
<Text style={styles.submitButtonText}>Load</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.currentUrlSection}>
|
||||
<Text style={styles.sectionTitle}>Current URL:</Text>
|
||||
<Text style={styles.currentUrl}>{dicomUrl}</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.viewerContainer}>
|
||||
<Text style={styles.viewerTitle}>DICOM Viewer:</Text>
|
||||
<DicomViewer
|
||||
dicomUrl={dicomUrl}
|
||||
onError={handleViewerError}
|
||||
onLoad={handleViewerLoad}
|
||||
debugMode={true}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
header: {
|
||||
backgroundColor: '#2196F3',
|
||||
padding: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#FFFFFF',
|
||||
marginBottom: 8,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: '#E3F2FD',
|
||||
},
|
||||
urlSection: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#212121',
|
||||
marginBottom: 12,
|
||||
marginTop: 16,
|
||||
},
|
||||
urlButton: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
marginBottom: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E0E0E0',
|
||||
},
|
||||
selectedUrlButton: {
|
||||
backgroundColor: '#E3F2FD',
|
||||
borderColor: '#2196F3',
|
||||
},
|
||||
urlButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#212121',
|
||||
marginBottom: 4,
|
||||
},
|
||||
selectedUrlButtonText: {
|
||||
color: '#1976D2',
|
||||
},
|
||||
urlText: {
|
||||
fontSize: 12,
|
||||
color: '#757575',
|
||||
fontFamily: 'monospace',
|
||||
},
|
||||
selectedUrlText: {
|
||||
color: '#1976D2',
|
||||
},
|
||||
customUrlSection: {
|
||||
marginTop: 16,
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
textInput: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E0E0E0',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
fontSize: 14,
|
||||
color: '#212121',
|
||||
marginRight: 8,
|
||||
},
|
||||
submitButton: {
|
||||
backgroundColor: '#4CAF50',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
},
|
||||
submitButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
currentUrlSection: {
|
||||
marginTop: 16,
|
||||
backgroundColor: '#FFFFFF',
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E0E0E0',
|
||||
},
|
||||
currentUrl: {
|
||||
fontSize: 12,
|
||||
color: '#757575',
|
||||
fontFamily: 'monospace',
|
||||
backgroundColor: '#F5F5F5',
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
viewerContainer: {
|
||||
flex: 2,
|
||||
backgroundColor: '#000000',
|
||||
margin: 16,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
viewerTitle: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
padding: 12,
|
||||
backgroundColor: '#333333',
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
* End of File: DicomViewerTest.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
@ -15,6 +15,12 @@ export { CustomModal } from './CustomModal';
|
||||
// Coming Soon Screen Component
|
||||
export { ComingSoonScreen } from './ComingSoonScreen';
|
||||
|
||||
// DICOM Viewer Component
|
||||
export { default as DicomViewer } from './DicomViewer';
|
||||
|
||||
// DICOM Viewer Test Component
|
||||
export { default as DicomViewerTest } from './DicomViewerTest';
|
||||
|
||||
// ============================================================================
|
||||
// TYPE EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
15
package-lock.json
generated
15
package-lock.json
generated
@ -44,6 +44,7 @@
|
||||
"react-native-toast-message": "^2.2.1",
|
||||
"react-native-tts": "^4.1.1",
|
||||
"react-native-vector-icons": "^10.2.0",
|
||||
"react-native-webview": "^13.15.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"redux-persist": "^6.0.0"
|
||||
},
|
||||
@ -12077,6 +12078,20 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-webview": {
|
||||
"version": "13.15.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.15.0.tgz",
|
||||
"integrity": "sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"invariant": "2.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/@react-native/virtualized-lists": {
|
||||
"version": "0.79.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.0.tgz",
|
||||
|
||||
@ -46,6 +46,7 @@
|
||||
"react-native-toast-message": "^2.2.1",
|
||||
"react-native-tts": "^4.1.1",
|
||||
"react-native-vector-icons": "^10.2.0",
|
||||
"react-native-webview": "^13.15.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"redux-persist": "^6.0.0"
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user