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 screens
|
||||||
import { AIPredictionsScreen, AIPredictionDetailScreen } from '../screens';
|
import { AIPredictionsScreen, AIPredictionDetailScreen } from '../screens';
|
||||||
import { ComingSoonScreen } from '../../../shared/components';
|
import { ComingSoonScreen, DicomViewer } from '../../../shared/components';
|
||||||
|
|
||||||
// Import types
|
// Import types
|
||||||
import type { AIPredictionStackParamList } from './navigationTypes';
|
import type { AIPredictionStackParamList } from './navigationTypes';
|
||||||
@ -152,7 +152,7 @@ const AIPredictionStackNavigator: React.FC = () => {
|
|||||||
{/* AI Prediction Details Screen */}
|
{/* AI Prediction Details Screen */}
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="AIPredictionDetails"
|
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 }) => ({
|
options={({ navigation, route }) => ({
|
||||||
title: 'Create Suggestion',
|
title: 'Create Suggestion',
|
||||||
headerLeft: () => (
|
headerLeft: () => (
|
||||||
|
|||||||
@ -543,8 +543,8 @@ const AIPredictionDetailScreen: React.FC<AIPredictionDetailsScreenProps> = ({
|
|||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
||||||
|
|
||||||
<TouchableWithoutFeedback onPress={closeAllDropdowns}>
|
<TouchableWithoutFeedback onPress={closeAllDropdowns} disabled={!showSuggestionTypeDropdown && !showPriorityDropdown}>
|
||||||
<>
|
<View style={{flex:1}}>
|
||||||
{/* Enhanced Header */}
|
{/* Enhanced Header */}
|
||||||
{renderHeader()}
|
{renderHeader()}
|
||||||
|
|
||||||
@ -860,7 +860,7 @@ const AIPredictionDetailScreen: React.FC<AIPredictionDetailsScreenProps> = ({
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</View>
|
||||||
|
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
|
|
||||||
|
|||||||
@ -470,15 +470,15 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
// Performance optimizations
|
// Performance optimizations
|
||||||
removeClippedSubviews={true}
|
// removeClippedSubviews={true}
|
||||||
maxToRenderPerBatch={10}
|
// maxToRenderPerBatch={10}
|
||||||
windowSize={10}
|
// windowSize={10}
|
||||||
initialNumToRender={8}
|
// initialNumToRender={8}
|
||||||
getItemLayout={(data, index) => ({
|
// getItemLayout={(data, index) => ({
|
||||||
length: 120, // Approximate height of PatientCard
|
// length: 120, // Approximate height of PatientCard
|
||||||
offset: 120 * index,
|
// offset: 120 * index,
|
||||||
index,
|
// index,
|
||||||
})}
|
// })}
|
||||||
/>
|
/>
|
||||||
</SafeAreaView>
|
</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
|
// Coming Soon Screen Component
|
||||||
export { ComingSoonScreen } from './ComingSoonScreen';
|
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
|
// 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-toast-message": "^2.2.1",
|
||||||
"react-native-tts": "^4.1.1",
|
"react-native-tts": "^4.1.1",
|
||||||
"react-native-vector-icons": "^10.2.0",
|
"react-native-vector-icons": "^10.2.0",
|
||||||
|
"react-native-webview": "^13.15.0",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"redux-persist": "^6.0.0"
|
"redux-persist": "^6.0.0"
|
||||||
},
|
},
|
||||||
@ -12077,6 +12078,20 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/react-native/node_modules/@react-native/virtualized-lists": {
|
||||||
"version": "0.79.0",
|
"version": "0.79.0",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.0.tgz",
|
"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-toast-message": "^2.2.1",
|
||||||
"react-native-tts": "^4.1.1",
|
"react-native-tts": "^4.1.1",
|
||||||
"react-native-vector-icons": "^10.2.0",
|
"react-native-vector-icons": "^10.2.0",
|
||||||
|
"react-native-webview": "^13.15.0",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"redux-persist": "^6.0.0"
|
"redux-persist": "^6.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user