Skip to main content
Version: Version 3.0 ๐Ÿšง

Mode: Routes


Modes are tied to a specific route in the viewer, and multiple modes/routes can be present within a single application. This makes routes config, THE most important part of the mode configuration.


@ohif/viewer compose extensions to build applications on different routes for the platform.

Below, you can see a simplified version of the longitudinal mode and the routes section which has defined one route. Each route has three different configuration:

  • route path: defines the route path to access the built application for that route
  • route init: hook that runs when application enters the defined route path, if not defined the default init function will run for the mode.
  • route layout: defines the layout of the application for the specified route (panels, viewports)
export default function mode() {  return {    id: 'viewer',    displayName: '',    routes: [      {        path: 'longitudinal',        /*init: ({ servicesManager, extensionManager }) => {          //defaultViewerRouteInit        },*/        layoutTemplate: ({ location, servicesManager }) => {          return {            id: ohif.layout,            props: {              leftPanels: [                'org.ohif.measurement-tracking.panelModule.seriesList',              ],              rightPanels: [                'org.ohif.measurement-tracking.panelModule.trackedMeasurements',              ],              viewports: [                {                  namespace:                    'org.ohif.measurement-tracking.viewportModule.cornerstone-tracked',                  displaySetsToDisplay: [                    'org.ohif.default.sopClassHandlerModule.stack',                  ],                },                {                  namespace: 'org.ohif.dicom-sr.viewportModule.dicom-sr',                  displaySetsToDisplay: [                    'org.ohif.dicom-sr.sopClassHandlerModule.dicom-sr',                  ],                },              ],            },          }        },      },    ],    /*    ...    */  }}

Route: path#

Upon initialization the viewer will consume extensions and modes and build up the route desired, these can then be accessed via the study list, or directly via url parameters.

Note: Currently, only one route is built for each mode, but we will enhance route creation to create separate routes based on the path config for each route object.

There are two types of routes that are created by the mode.

  • Routes with dataSourceName /${}/${dataSourceName}
  • Routes without dataSourceName /${}

Therefore navigating to http://localhost:3000/viewer/?StudyInstanceUIDs= will run the app with the layout and functionalities of the viewer mode using the defaultDataSourceName which is defined in the App Config

You can use the same exact mode using a different registered data source (e.g., dicomjson) by navigating to http://localhost:3000/viewer/dicomjson/?StudyInstanceUIDs=

Route: init#

The mode also has an init hook, which initializes the mode. If you don't define an init function the default init function will get run (logic is located inside Mode.jsx). However, you can define you own init function following certain steps which we will discuss next.

Default init#

Default init function will:

  • retriveSeriesMetaData for the studyInstanceUIDs that are defined in the URL.
  • Subscribe to instanceAdded event, to make display sets after a series have finished retrieving its instances metadata.
  • Subscribe to seriesAdded event, to run the HangingProtocolService on the retrieves series from the study.

A simplified "pseudocode" for the defaultRouteInit is:

async function defaultRouteInit({  servicesManager,  studyInstanceUIDs,  dataSource,}) {  const { DisplaySetService, HangingProtocolService } =
  // subscribe to run the function after the event happens  DicomMetadataStore.subscribe(    'instancesAdded',    ({ StudyInstanceUID, SeriesInstanceUID }) => {      const seriesMetadata = DicomMetadataStore.getSeries(        StudyInstanceUID,        SeriesInstanceUID      )      DisplaySetService.makeDisplaySets(seriesMetadata.instances)    }  )
  studyInstanceUIDs.forEach((StudyInstanceUID) => {    dataSource.retrieveSeriesMetadata({ StudyInstanceUID })  })
  DicomMetadataStore.subscribe('seriesAdded', ({ StudyInstanceUID }) => {    const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID)  })
  return unsubscriptions}

Writing a custom init#

You can add your custom init function to enhance the default initialization for:

  • Fetching annotations from a server for the current study
  • Changing the initial image index of the series to be displayed at first
  • Caching the next study in the work list
  • Adding a custom sort for the series to be displayed on the study browser panel

and lots of other modifications.

You just need to make sure, the mode retrieveSeriesMetadata, makeDisplaySets and run the HangingProtocols at some point. There are various events that you can subscribe to and add your custom logic. point to events

For instance for jumping to the slice where a measurement is located at the initial render, you need to follow a pattern similar to the following:

init: async ({  servicesManager,  extensionManager,  hotkeysManager,  dataSource,  studyInstanceUIDs,}) => {  const { DisplaySetService } =
  /**  ...  **/
  const onDisplaySetsAdded = ({ displaySetsAdded, options }) => {    const displaySet = displaySetsAdded[0]    const { SeriesInstanceUID } = displaySet
    const toolData = myServer.fetchMeasurements(SeriesInstanceUID)
    if (!toolData.length) {      return    }
    toolData.forEach((tool) => {      const instance = displaySet.images.find(        (image) => image.SOPInstanceUID === tool.SOPInstanceUID      )
      const { SOPInstanceUID, url } = instance      displaySet.initialImageIdIndex = displaySet.images.indexOf(instance)    })
    MeasurementService.addMeasurement(/**...**/)  }
  // subscription to the DISPLAY_SETS_ADDED  const { unsubscribe } = DisplaySetService.subscribe(    DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,    onDisplaySetsAdded  )
  /**  ...  **/
  return unsubscriptions}

Route: layoutTemplate#

layoutTemplate is the last configuration for a certain route in a mode. layoutTemplate is a function that returns an object that configures the overall layout of the application. The returned object has two properties:

  • id: the id of the layoutTemplate being used (it should have been registered via an extension)
  • props: the required properties to be passed to the layoutTemplate.

For instance default extension provides a layoutTemplate that builds the app using left/right panels and viewports. Therefore, the props include leftPanels, rightPanels and viewports sections. Note that the layoutTemplate defines the properties it is expecting. So, if you write a layoutTemplate-2 that accepts a footer section, its logic should be written in the extension, and any mode that is interested in using layoutTemplate-2 should provide the id for the footer component.

What module should the footer be registered?

/*...*/layoutTemplate: ({ location, servicesManager }) => {  return {    id: 'org.ohif.default.layoutTemplateModule.viewerLayout',    props: {      leftPanels: [        'myExtension.panelModule.leftPanel1',        'myExtension.panelModule.leftPanel2',      ],      rightPanels: ['myExtension.panelModule.rightPanel'],      viewports: [        {          namespace: 'myExtension.viewportModule.viewport1',          displaySetsToDisplay: ['myExtension.sopClassHandlerModule.sop1'],        },        {          namespace: 'myExtension.viewportModule.viewport2',          displaySetsToDisplay: ['myExtension.sopClassHandlerModule.sop2'],        },      ],    },  }}/*...*/


What is the difference between onModeEnter and route.init

onModeEnter gets run first than route.init; however, each route can have their own init, but they share the onModeEnter.

How can I change the workList appearance or add a new login page?

This is where OHIF-v3 shines! Since the default layoutTemplate is written for the viewer part, you can simply add a new layoutTemplate and use the component you have written for that route. Mode handle showing the correct component for the specified route.

export default function mode() {  return {    id: 'viewer',    displayName: '',    routes: [      {        path: 'worklist',        init,        layoutTemplate: ({ location, servicesManager }) => {          return {            id: 'worklistLayout',            props: {              component: 'myNewWorkList'            },          }        },      },    ],    /*    ...    */  }}