Describe Your Question
- A clear and concise description of what problem you are having.
I am trying to use the ECG viewer onto the release build, but when trying to open an ECG study, I get “The hanging protocol viewport is requesting to display defaultDisplaySetId displaySet that is not matched based on the provided criteria (e.g. matching rules).” on the console, with a black screen on the viewport.
I am using the mode ECG mode downloaded from the cli, and manually linked the extension from the @radicalimaging repo (https://github.com/RadicalImaging/OHIFExtensionsAndModes/tree/main/extensions/ecg-dicom).
- Which OHIF version you are using?
3.7 release
What steps can we follow to reproduce the bug?
- Download and link ecg-dicom extension
- Download ECG mode from yarn run cli search
- Connect the app to a DB with ECGs
4.Try to open one and open the console
Please use code blocks to show formatted errors or code snippets
I believe I have the mode and extension properly set up, but might be missing addind a hanging protocol? Quite new to the app and still very lost, appreciate the help a lot.
I have changed some names from sopClassHandlers but made sure they are named properly inbetween files.
Mode set up:
import { hotkeys } from '@ohif/core';
import { id } from './id';
import { initToolGroups, toolbarButtons } from '@ohif/mode-longitudinal';
const ohif = {
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
hangingProtocol: '@ohif/extension-default.hangingProtocolModule.default',
leftPanel: '@ohif/extension-default.panelModule.seriesList',
rightPanel: '@ohif/extension-default.panelModule.measure',
};
const cornerstone = {
viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone',
};
const modeECG = {
sopClassHandler: 'ecg-extension.sopClassHandlerModule.ecg-dicom',
viewport: 'ecg-extension.viewportModule.ecg-dicom',
};
/**
* Just two dependencies to be able to render a viewport with panels in order
* to make sure that the mode is working.
*/
const extensionDependencies = {
'@ohif/extension-default': '^3.0.0',
'@ohif/extension-cornerstone': '^3.0.0',
'ecg-extension': "^0.0.1"
};
function modeFactory({ modeConfiguration }) {
return {
/**
* Mode ID, which should be unique among modes used by the viewer. This ID
* is used to identify the mode in the viewer's state.
*/
id,
routeName: 'ecg',
/**
* Mode name, which is displayed in the viewer's UI in the workList, for the
* user to select the mode.
*/
displayName: 'ECG',
/**
* Runs when the Mode Route is mounted to the DOM. Usually used to initialize
* Services and other resources.
*/
onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => {
const { measurementService, toolbarService, toolGroupService } = servicesManager.services;
measurementService.clearMeasurements();
// Init Default and SR ToolGroups
initToolGroups(extensionManager, toolGroupService, commandsManager);
let unsubscribe;
const activateTool = () => {
toolbarService.recordInteraction({
groupId: 'WindowLevel',
interactionType: 'tool',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'WindowLevel',
},
context: 'CORNERSTONE',
},
],
});
// We don't need to reset the active tool whenever a viewport is getting
// added to the toolGroup.
unsubscribe();
};
// Since we only have one viewport for the basic cs3d mode and it has
// only one hanging protocol, we can just use the first viewport
({ unsubscribe } = toolGroupService.subscribe(
toolGroupService.EVENTS.VIEWPORT_ADDED,
activateTool
));
toolbarService.init(extensionManager);
toolbarService.addButtons(toolbarButtons);
toolbarService.createButtonSection('primary', [
'MeasurementTools',
'Zoom',
'WindowLevel',
'Pan',
'Capture',
'Layout',
'MPR',
'Crosshairs',
'MoreTools',
]);
},
onModeExit: ({ servicesManager }) => {
const {
toolGroupService,
syncGroupService,
toolbarService,
segmentationService,
cornerstoneViewportService,
} = servicesManager.services;
toolGroupService.destroy();
syncGroupService.destroy();
segmentationService.destroy();
cornerstoneViewportService.destroy();
},
/** */
validationTags: {
study: [],
series: [],
},
/**
* A boolean return value that indicates whether the mode is valid for the
* modalities of the selected studies. For instance a PET/CT mode should be
*/
isValidMode: ({ modalities }) => {
const modalities_list = modalities.split('\\');
return modalities_list.includes('ECG')
},
/**
* Mode Routes are used to define the mode's behavior. A list of Mode Route
* that includes the mode's path and the layout to be used. The layout will
* include the components that are used in the layout. For instance, if the
* default layoutTemplate is used (id: '@ohif/extension-default.layoutTemplateModule.viewerLayout')
* it will include the leftPanels, rightPanels, and viewports. However, if
* you define another layoutTemplate that includes a Footer for instance,
* you should provide the Footer component here too. Note: We use Strings
* to reference the component's ID as they are registered in the internal
* ExtensionManager. The template for the string is:
* `${extensionId}.{moduleType}.${componentId}`.
*/
routes: [
{
path: 'ECG',
layoutTemplate: ({ location, servicesManager }) => {
return {
id: ohif.layout,
props: {
leftPanels: [ohif.leftPanel],
viewports: [
{
namespace: modeECG.viewport,
displaySetsToDisplay: [modeECG.sopClassHandler],
},
],
},
};
},
},
],
/** List of extensions that are used by the mode */
extensions: extensionDependencies,
/** HangingProtocol used by the mode */
hangingProtocol: 'default',
/** SopClassHandlers used by the mode */
sopClassHandlers: [modeECG.sopClassHandler, ohif.sopClassHandler],
/** hotkeys for mode */
hotkeys: [...hotkeys.defaults.hotkeyBindings],
};
}
const mode = {
id,
modeFactory,
extensionDependencies,
};
export default mode;
Extension src/index:
import { id } from './id';
import getSopClassHandlerModule from "./getSopClassHandlerModule";
import React, { Suspense } from "react";
const Component = React.lazy(() => {
return import('./viewports/EcgViewport');
});
const EcgViewport = props => {
return (
<Suspense fallback={(<div>Loading...</div>)}>
<Component {...props} />
</Suspense>
);
};
/**
* You can remove any of the following modules if you don't need them.
*/
export default {
/**
* Only required property. Should be a unique value across all extensions.
* You ID can be anything you want, but it should be unique.
*/
id,
/**
* Perform any pre-registration tasks here. This is called before the extension
* is registered. Usually we run tasks such as: configuring the libraries
* (e.g. cornerstone, cornerstoneTools, ...) or registering any services that
* this extension is providing.
*/
preRegistration: ({
servicesManager,
commandsManager,
configuration = {},
}) => { },
/**
* ViewportModule should provide a list of viewports that will be available in OHIF
* for Modes to consume and use in the viewports. Each viewport is defined by
* {name, component} object. Example of a viewport module is the CornerstoneViewport
* that is provided by the Cornerstone extension in OHIF.
*/
getViewportModule({ servicesManager, extensionManager }) {
const ExtendedEcgViewport = props => {
return (
<EcgViewport
servicesManager={servicesManager}
extensionManager={extensionManager}
{...props}
/>
);
};
return [{ name: 'ecg-dicom', component: ExtendedEcgViewport }];
},
/**
* SopClassHandlerModule should provide a list of sop class handlers that will be
* available in OHIF for Modes to consume and use to create displaySets from Series.
* Each sop class handler is defined by a { name, sopClassUids, getDisplaySetsFromSeries}.
* Examples include the default sop class handler provided by the default extension
*/
getSopClassHandlerModule,
};
ecg-extension/src/getSopClassHandlerModule (If not hanging protocol issue might be here?):
import { utils, classes } from '@ohif/core';
import { SOPClassHandlerId } from './id';
const { ImageSet } = classes;
const SOP_CLASS_UIDS = {
TWELVE_LEAD_WAVEFORM_STORAGE: '1.2.840.10008.5.1.4.1.1.9.1.1',
};
const sopClassUids = Object.values(SOP_CLASS_UIDS);
const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) => {
return instances
.map(instance => {
const { Modality, SOPInstanceUID, SeriesDescription = "ECG" } = instance;
const { SeriesDate, SeriesNumber, SeriesInstanceUID, StudyInstanceUID } = instance;
const displaySet = {
//plugin: id,
Modality,
displaySetInstanceUID: utils.guid(),
SeriesDescription,
SeriesNumber: SeriesNumber || 1,
SeriesDate,
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
SOPClassHandlerId,
referencedImages: null,
measurements: null,
others: [instance],
isDerivedDisplaySet: true,
isLoaded: false,
sopClassUids,
numImageFrames: 0,
instance,
};
return displaySet;
});
};
export default function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(
instances,
servicesManager,
extensionManager
);
};
return [
{
name: 'ecg-dicom',
sopClassUids,
getDisplaySetsFromSeries,
},
];
}
Thank you for your time!