Thanks for your detailed question. The short answer is that OHIF’s DICOM JSON data source search implementation does not work well at all for OHIF’s study list. I believe the DICOM JSON data source is meant primarily/exclusively for the viewer.
This said there is nothing to stop you from changing the code in the link I sent to be something like this for your own purposes…
const [key, value] = Object.entries(param)[0];
let studies = [];
if (key in mappings) {
// todo: should fetch from dicomMetadataStore
studies = findStudies(mappings[key], value);
} else {
// assume a study list query
studies = _store.urls.map(metaData => metaData.studies.map(aStudy => aStudy)).flat();
}
Hello @jbocce And thank you very much for your time.
But, I don’t think you fully understand the problem I’m currently experiencing.
The problem isn’t with the search function itself. It’s the Datatable that isn’t displaying at all in the Laravel view, showing “0 Studies*” / “*No studies available”, as you can see in the screenshot.
This contrasts with the OHIF VIEWER page, which correctly lists the data in the DataTable, as you can see in the screenshot:
Fetch interception - Intercepted all fetch calls to return data
Direct embedding - Stored data in window.dicomData
URL parameters - Added ?url=... parameter
Different URL schemes - Tried dicomjson://, blob:, data:, http://
All approaches result in:
Data is fetched successfully
Data is parsed successfully
Study List shows “0 Studies”
Working Alternative
When I disable Study List (showStudyList: false) and pass StudyInstanceUIDs parameter, OHIF loads and displays the study perfectly but return “Error (404) - We can’t find the page you’re looking for”.
This confirms the data format is correct.
Request for Help
Could someone from the OHIF team or community help me understand:
Why Study List doesn’t display the studies despite successful data loading?
What’s the correct approach to use Study List with pre-loaded DICOM JSON data?
If this is a limitation, what’s the recommended alternative?
My goal is to do as for OHIF VIEWER by displaying the DICOM data in the Datatable with all the functionalities and the “Viewers / Segmentation / etc…” buttons “OHIF VIEWER” does BUT DESPITE ALL MY EFFORTS THE DATATABLE IS STILL EMPTY IN MY CASE.
Yes. I understand completely. The reason that the study list displays 0 studies is because the study list does NOT work at all with a JSON data source. If you try editing the code as I suggested it might help.
It works now after after having corrected this Modules\Insurance\advanced-dicomviewer\extensions\default\src\DicomJSONDataSource\index.js and rebuild with Yarn.
But when I click on the Viewer from Study List’s Datatable, I access this Screen:
Why is it arranged like this??? Is it normal? I thought it should be arranged image by image, but it displays everything together in a single rectangular frame, even though the images weren’t uploaded on the same day and don’t belong to all users, since each user of my application has their own DICOM document or image.
The Viewer button that you clicked on launches an OHIF mode. That mode defaults to a hanging protocol which arranges the images in a 1x1 view. If you try launching a different mode (say Segmentation) you’d get a different hanging protocol.
So to answer one of your questions: yes it is expected and normal for that mode.
It sounds to me from all the questions you have presented that you might want to consider writing your own custom mode (for at least choosing a specific hanging protocol) and furthermore a custom extension. The extension could be where you customize the DICOM JSON data source to your liking to allow for the study list to do paging, filtering and sorting. Of course these are just suggestions.
Regardless please familiarize yourself with OHIF modes and extensions. There is lots of documentation to help you customize OHIF in almost whatever way you desire.
All the DICOM images are grouped under a single thumbnail on the left, and furthermore, when I click on “Layout Tool” hoping to switch to “2x2”, “3x3”, etc., as shown in the OHIF Viewer Website , for example, the other spaces in the Viewer are empty, and all the images are grouped in the first space, whereas I expected each image to have its own space.
EVERYTHING SHOULD NOT BE GROUPED BUT SEPARATELY DISPLAYED IN DIFFERENT BOXES WHEN I CHANGE THE LAYOUT, AS WITH THE HIF VIEWER LINK, INSTEAD OF ALL DISPLAYING IN THE SAME SINGLE BOX AND ALSO INSTEAD OF HIDING IN THE LEFT COLUMN IN A SINGLE THUMBNAIL.
However, here is how I edited the file Modules\Insurance\advanced-dicomviewer\modes\longitudinal\src\index.ts before recompiling with Yarn:
import i18n from 'i18next';
import { id } from './id';
import {
initToolGroups, toolbarButtons, cornerstone,
ohif,
dicomsr,
dicomvideo,
basicLayout,
basicRoute,
extensionDependencies as basicDependencies,
mode as basicMode,
modeInstance as basicModeInstance,
} from '@ohif/mode-basic';
export const tracked = {
measurements: '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements',
thumbnailList: '@ohif/extension-measurement-tracking.panelModule.seriesList',
viewport: '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked',
};
export const extensionDependencies = {
// Can derive the versions at least process.env.from npm_package_version
...basicDependencies,
'@ohif/extension-measurement-tracking': '^3.0.0',
};
export const longitudinalInstance = {
...basicLayout,
id: ohif.layout,
props: {
...basicLayout.props,
leftPanels: [tracked.thumbnailList],
rightPanels: [cornerstone.segmentation, tracked.measurements],
viewports: [
{
namespace: tracked.viewport,
// Re-use the display sets from basic
displaySetsToDisplay: basicLayout.props.viewports[0].displaySetsToDisplay,
},
...basicLayout.props.viewports,
],
}
};
export const longitudinalRoute =
{
...basicRoute,
path: 'longitudinal',
/*init: ({ servicesManager, extensionManager }) => {
//defaultViewerRouteInit
},*/
layoutInstance: longitudinalInstance,
};
export const modeInstance = {
...basicModeInstance,
// TODO: We're using this as a route segment
// We should not be.
id,
routeName: 'viewer',
displayName: i18n.t('Modes:Basic Viewer'),
hangingProtocol: ['@ohif/mnGrid', '@ohif/mnGrid8', 'default'],
routes: [
longitudinalRoute
],
extensions: extensionDependencies,
};
const mode = {
...basicMode,
id,
modeInstance,
extensionDependencies,
};
export default mode;
export { initToolGroups, toolbarButtons };
AND IN PGP CODE CONTROLER, I HAVE:
/**
* Display the DICOM viewer for a specific document.
*
* @param int $documentId
* @return \Illuminate\Http\Response
*/
public function show($documentId, $any = null)
{
$user = auth()->user();
$business_id = $user->business_id ?? request()->session()->get('user.business_id');
// SMART CONTACT RESOLUTION - but ONLY for policy holders (user_customer)
$contact_id = null;
if ($user->user_type === 'user_customer') {
$contact_id = $user->contact_id ?? $user->crm_contact_id ?? ($user->contact ? $user->contact->id : null);
// If no direct link, try matching by Email
if (!$contact_id && $user->email) {
$linkedContact = \App\Contact::where('business_id', $business_id)
->where('email', $user->email)
->where('type', 'customer')
->first();
if ($linkedContact) {
$contact_id = $linkedContact->id;
}
}
}
// DEBUG: Log the request
\Log::info('DicomViewerController@show called', [
'documentId' => $documentId,
'any' => $any,
'url' => request()->fullUrl(),
'user_id' => auth()->id(),
'user_type' => $user->user_type,
'contact_id' => $contact_id,
'business_id' => $business_id,
'is_policy_holder' => (bool)$contact_id
]);
// Determine role and validate access
$role = 'admin';
try {
if ($contact_id) {
$role = 'policy_holder';
$document = MedicalImagingDocument::with('procedure')
->whereHas('procedure', function($q) use ($contact_id) {
$q->where('patient_id', $contact_id);
})
->findOrFail($documentId);
$backUrl = route('insurance.policy-holder-portal.medical-imaging.show', $document->procedure_id);
} else {
$document = MedicalImagingDocument::where('business_id', $business_id)->findOrFail($documentId);
$backUrl = route('insurance.medical-imaging.show', $document->procedure_id);
}
} catch (\Exception $e) {
\Log::error('DicomViewerController@show - Document access denied or not found', [
'documentId' => $documentId,
'error' => $e->getMessage()
]);
return redirect()->back()->with('error', __('insurance::lang.document_not_found_or_access_denied'));
}
\Log::info('DicomViewerController@show - Role determined', [
'role' => $role,
'document_id' => $document->id,
'backUrl' => $backUrl
]);
// Check file type
$extension = strtolower(pathinfo($document->file_name, PATHINFO_EXTENSION));
if (!in_array($extension, ['dcm', 'dicom'])) {
return redirect()->back()->with('error', 'Not a DICOM file.');
}
// I18n: Load translation file based on Laravel locale with fallback
$laravelLocale = app()->getLocale();
// Define mapping between Laravel locales and Viewer locales (Nextcloud style)
$localeMapping = [
'en' => 'en_GB', // Viewer uses en_GB as default English
'fr' => 'fr',
'es' => 'es',
'de' => 'de',
'it' => 'it',
// Add more as needed
];
$viewerLocale = $localeMapping[$laravelLocale] ?? 'en_GB';
// Path to l10n files in the module
$l10nPath = module_path('Insurance', 'dicomviewer/l10n/' . $viewerLocale . '.json');
// Fallback to en_GB if specific locale file doesn't exist
if (!file_exists($l10nPath)) {
$l10nPath = module_path('Insurance', 'dicomviewer/l10n/en_GB.json');
}
$translations = [];
if (file_exists($l10nPath)) {
$jsonContent = file_get_contents($l10nPath);
$decoded = json_decode($jsonContent, true);
$translations = $decoded['translations'] ?? [];
} else {
// Absolute fallback if no files found
\Log::warning("DICOM Viewer localization file not found: $l10nPath");
}
// Use OHIF viewer (more complete and better design)
return view('insurance::medical-imaging.dicom-viewer', compact('document', 'role', 'backUrl', 'translations'));
}
/**
* Show DICOM viewer by Procedure ID (Loads first available DICOM).
*
* @param int $procedureId
* @param string|null $any
* @return \Illuminate\Http\Response
*/
public function showByProcedure($procedureId, $any = null)
{
$user = auth()->user();
$business_id = $user->business_id ?? request()->session()->get('user.business_id');
// SMART CONTACT RESOLUTION
$contact_id = null;
if ($user->user_type === 'user_customer') {
$contact_id = $user->contact_id ?? $user->crm_contact_id ?? ($user->contact ? $user->contact->id : null);
if (!$contact_id && $user->email) {
$linkedContact = \App\Contact::where('business_id', $business_id)
->where('email', $user->email)
->where('type', 'customer')
->first();
if ($linkedContact) {
$contact_id = $linkedContact->id;
}
}
}
// Find Procedure
if ($contact_id) {
$procedure = MedicalImagingProcedure::where('business_id', $business_id)
->where('patient_id', $contact_id)
->find($procedureId);
} else {
$procedure = MedicalImagingProcedure::where('business_id', $business_id)->find($procedureId);
}
if (!$procedure) {
return redirect()->back()->with('error', __('insurance::lang.procedure_not_found'));
}
// Find first DICOM document
$firstDicom = $procedure->documents()
->whereIn('file_type', ['dicom', 'dcm'])
->orderBy('id')
->first();
// If no explicit file_type, try extension
if (!$firstDicom) {
$documents = $procedure->documents()->get();
foreach ($documents as $doc) {
$ext = strtolower(pathinfo($doc->file_name, PATHINFO_EXTENSION));
if (in_array($ext, ['dcm', 'dicom'])) {
$firstDicom = $doc;
break;
}
}
}
if (!$firstDicom) {
return redirect()->back()->with('error', __('insurance::lang.no_dicom_images_found_for_procedure'));
}
// Redirect to show method logic by calling it directly
// to avoid browser redirect and allow keeping the URL structure if we wanted
// But since we want to leverage the 'show' logic which uses $documentId,
// we can just call $this->show($firstDicom->id, $any);
return $this->show($firstDicom->id, $any);
}
/**
* Get all DICOM JSON metadata for documents list (OHIF Study List compatible).
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getAllDicomJson(Request $request)
{
$user = auth()->user();
if (!$user) {
\Log::error('getAllDicomJson - Unauthorized');
return response()->json(['error' => 'Unauthorized'], 403);
}
$business_id = $user->business_id ?? request()->session()->get('user.business_id');
// SMART CONTACT RESOLUTION - but ONLY for policy holders (user_customer)
$contact_id = null;
if ($user->user_type === 'user_customer') {
$contact_id = $user->contact_id ?? $user->crm_contact_id ?? ($user->contact ? $user->contact->id : null);
// If no direct link, try matching by Email
if (!$contact_id && $user->email) {
$linkedContact = \App\Contact::where('business_id', $business_id)
->where('email', $user->email)
->where('type', 'customer')
->first();
if ($linkedContact) {
$contact_id = $linkedContact->id;
}
}
}
// Get all DICOM documents for the business or user's company
$query = MedicalImagingDocument::where('medical_imaging_documents.business_id', $business_id)
->whereIn('file_type', ['dcm', 'dicom'])
->with(['procedure.patient', 'procedure.examType']);
// If user is a healthcare agent (not super admin), filter by their company
if (!$user->can('superadmin') && $user->can('insurance.healthcare.view_prestations')) {
// Filter documents from procedures where the provider matches the user's company
$query->whereHas('procedure', function($q) use ($user) {
if ($user->crm_contact_id) {
$q->where('provider_id', $user->crm_contact_id);
}
});
}
// If policy holder, filter by patient_id
if ($contact_id) {
$query->whereHas('procedure', function($q) use ($contact_id) {
$q->where('patient_id', $contact_id);
});
}
$documents = $query->orderBy('created_at', 'desc')->get();
// Filter to only include documents that exist on disk
$documents = $documents->filter(function($doc) {
$path = storage_path('app/public/' . $doc->file_path);
return file_exists($path);
});
\Log::info('getAllDicomJson - Processing all documents', [
'user_id' => $user->id,
'business_id' => $business_id,
'total_documents' => $documents->count()
]);
// Ensure all documents have procedure_id set
foreach ($documents as $doc) {
if (!$doc->procedure_id && $doc->relationLoaded('procedure')) {
$doc->procedure_id = $doc->procedure->id;
}
}
// Generate DICOM JSON for all documents
$dicomJson = $this->generateDICOMJson($documents);
\Log::info('getAllDicomJson - Generated JSON', [
'studies_count' => count($dicomJson['studies'] ?? []),
'json_size' => strlen(json_encode($dicomJson))
]);
return response()->json($dicomJson)
->header('Access-Control-Allow-Origin', '*')
->header('Access-Control-Allow-Methods', 'GET, OPTIONS')
->header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
->header('Content-Type', 'application/json');
}
AND MY HTML - JAVASCRIPT;
<!doctype html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/>
<meta name="theme-color" content="#000000"/>
<title>{{ __('insurance::lang.documents_list') }} - {{ config('app.name') }}</title>
<link href="{{ asset('modules/insurance/advanced-dicomviewer/app.bundle.css') }}" rel="stylesheet">
</head>
<body>
<noscript>{{ __('insurance::lang.javascript_required') }}</noscript>
<div id="root"></div>
<div class="back-btn-overlay">
<button onclick="closeViewer()" class="btn-back">← {{ __('insurance::lang.back') }}</button>
</div>
<!-- Configuration & Scripts -->
<script>
// Set Public URL for webpack chunks
window.PUBLIC_URL = '{{ asset("modules/insurance/advanced-dicomviewer") }}/';
console.log('📄 Documents count:', {{ $documents->count() }});
console.log('🌐 Current URL:', window.location.href);
// Load embedded DICOM data directly
const dicomData = {!! json_encode($dicomJsonData) !!};
console.log('✅ DICOM data loaded from embedded script');
console.log('📊 Studies count:', dicomData.studies ? dicomData.studies.length : 0);
console.log('📋 Full data:', dicomData);
// Store globally for OHIF and debugging
window.dicomData = dicomData;
// Create a fake URL for OHIF to "fetch" from
const dataUrl = 'dicomjson://embedded-data.json';
// Override OHIF's data fetching to use our embedded data
// This intercepts the fetch call and returns our data directly
const originalFetch = window.fetch;
window.fetch = function(url, options) {
console.log('🔍 Fetch intercepted:', url, typeof url);
// Handle null or undefined URLs
if (!url || url === 'null' || url === 'undefined') {
console.log('⚠️ Invalid URL detected, returning embedded data');
return Promise.resolve(new Response(JSON.stringify(dicomData), {
status: 200,
headers: { 'Content-Type': 'application/json' }
}));
}
// If OHIF is trying to fetch DICOM JSON data, return our embedded data
if (typeof url === 'string' && (
url.includes('dicomjson') ||
url.includes('.json') ||
url.startsWith('blob:') ||
url.includes('embedded-data')
)) {
console.log('✅ Returning embedded DICOM data instead of fetching');
console.log('📊 Data structure:', {
hasStudies: !!dicomData.studies,
studiesCount: dicomData.studies ? dicomData.studies.length : 0,
firstStudy: dicomData.studies && dicomData.studies[0] ? {
StudyInstanceUID: dicomData.studies[0].StudyInstanceUID,
PatientName: dicomData.studies[0].PatientName,
seriesCount: dicomData.studies[0].series ? dicomData.studies[0].series.length : 0,
firstSeries: dicomData.studies[0].series && dicomData.studies[0].series[0] ? {
SeriesInstanceUID: dicomData.studies[0].series[0].SeriesInstanceUID,
instancesCount: dicomData.studies[0].series[0].instances ? dicomData.studies[0].series[0].instances.length : 0
} : null
} : null
});
const response = new Response(JSON.stringify(dicomData), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
// Log when response is consumed
const originalJson = response.json.bind(response);
response.json = async function() {
console.log('📥 OHIF is parsing the JSON response');
const data = await originalJson();
console.log('✅ JSON parsed successfully:', data);
return data;
};
return Promise.resolve(response);
}
// For all other requests, use the original fetch
return originalFetch.apply(this, arguments);
};
console.log('✅ Fetch interceptor installed');
// OHIF Configuration - Disable Study List and load study directly
const studyUIDs = dicomData.studies.map(s => s.StudyInstanceUID).join(',');
window.config = {
routerBasename: '/insurance/medical-imaging/documents-list',
showStudyList: true, // Disable study list - load directly
extensions: [],
modes: [],
dataSources: [
{
namespace: '@ohif/extension-default.dataSourcesModule.dicomjson',
sourceName: 'dicomjson',
configuration: {
friendlyName: 'Laravel DICOM JSON - Documents List',
name: 'json',
},
}
],
defaultDataSourceName: 'dicomjson',
};
// CRITICAL FIX: OHIF Study List expects data in a specific format
// We need to ensure the data is available BEFORE OHIF initializes
// Store data in a way OHIF can access it
window.__OHIF_DICOM_JSON_DATA__ = dicomData;
// Set URL parameters to load the study directly
const currentUrl = new URL(window.location.href);
if (!currentUrl.searchParams.has('url')) {
currentUrl.searchParams.set('url', dataUrl);
currentUrl.searchParams.set('StudyInstanceUIDs', studyUIDs);
window.history.replaceState({}, '', currentUrl);
console.log('✅ URL parameters set:', {
url: dataUrl,
StudyInstanceUIDs: studyUIDs
});
console.log('✅ Data stored in window.__OHIF_DICOM_JSON_DATA__');
}
// Close Function
function closeViewer() {
// Return to medical imaging list
window.location.href = "{{ route('insurance.medical-imaging.index') }}";
}
// Use MutationObserver to handle dynamically loaded content
let mutationTimeout;
const observer = new MutationObserver(function(mutations) {
clearTimeout(mutationTimeout);
mutationTimeout = setTimeout(customizeDicomViewer, 50);
});
// Start observing when document is ready
window.addEventListener('load', function() {
observer.observe(document.body, {
childList: true,
subtree: true
});
});
// Log documents info for debugging
console.log('🔵 DICOM Documents List Mode Initialized');
console.log('📄 Total Documents:', {{ $documents->count() }});
console.log('🌐 Current URL:', window.location.href);
console.log('⚙️ showStudyList:', window.config.showStudyList);
console.log('🔗 URL params:', new URL(window.location.href).searchParams.getAll('url'));
// Don't restore URL - let OHIF manage it
// The URL disappearing is normal OHIF behavior after reading the parameter
</script>
<!-- Bundle Scripts -->
<script defer="defer" src="{{ asset('modules/insurance/advanced-dicomviewer/app.bundle.fe7d3f023f734e9ebfbf.js') }}"></script>
</body>
</html>
How can I properly arrange both the thumbnails and layouts separately, as with the OHIF Viewer website, instead of always cramming them together?
EVERYTHING SHOULD NOT BE GROUPED BUT SEPARATELY DISPLAYED IN DIFFERENT BOXES WHEN I CHANGE THE LAYOUT, AS WITH THE HIF VIEWER LINK, INSTEAD OF ALL DISPLAYING IN THE SAME SINGLE BOX AND ALSO INSTEAD OF HIDING IN THE LEFT COLUMN IN A SINGLE THUMBNAIL.
The reason why only 1 display set is created is with the data. The JSON you provided lists all 5 images under that same series and thus only a single display set is created by OHIF. Each viewport (box as you call it) typically only displays a single display set in OHIF. Likewise the series tray on the left shows a single thumbnail per display set and since your data lists ALL images under one series the series tray only shows one thumbnail.
You could change your DICOM JSON as below to get two series, but if the original data is this way it would be difficult to justify this change.