Mode: Routes
#
OverviewModes 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.
#
Route@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: pathUpon 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 eachroute
object.
There are two types of routes
that are created by the mode.
- Routes with dataSourceName
/${mode.id}/${dataSourceName}
- Routes without dataSourceName
/${mode.id}
Therefore navigating to http://localhost:3000/viewer/?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1
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=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1
#
Route: initThe 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 initDefault init function will:
retriveSeriesMetaData
for thestudyInstanceUIDs
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 theHangingProtocolService
on the retrieves series from the study.
A simplified "pseudocode" for the defaultRouteInit
is:
async function defaultRouteInit({ servicesManager, studyInstanceUIDs, dataSource,}) { const { DisplaySetService, HangingProtocolService } = servicesManager.services
// 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) HangingProtocolService.run(studyMetadata) })
return unsubscriptions}
#
Writing a custom initYou 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 } = servicesManager.services
/** ... **/
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: layoutTemplatelayoutTemplate
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 thelayoutTemplate
being used (it should have been registered via an extension)props
: the required properties to be passed to thelayoutTemplate
.
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'], }, ], }, }}/*...*/
#
FAQWhat is the difference between
onModeEnter
androute.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' }, } }, }, ], /* ... */ }}