How to implement ECG viewer / hanging protocol not matched

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?

  1. Download and link ecg-dicom extension
  2. Download ECG mode from yarn run cli search
  3. 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!

Got it working with the DB that comes from the “Try it out”, seems like the issue comes from the DB I was working with from the hospital not sending the same response when asking for the metadata as the try out.

Apologies for the post, cannot see a way to remove it, if any staff can or mark as resolved/edit name to resolved.

Thank you!