feat: Add White Labelling, Hanging Protocols, and tons of Extensions / Mode fixes

* modified

* new files in existing extensions/libraries/modes

* fixed bugs and tests

* Clean up

* fix: viewport display fixed

* Fixed configs for deployment

* Removed unnecessary files and functions

* Fixed default HP module

* Added white labelling

* Fixed hash routing

* Removed unnecessary routers

Co-authored-by: Alireza Sedghi <ar.sedghi@gmail.com>
This commit is contained in:
Erik ZieglerandAlireza Sedghi authored and GitHub committed 2021-05-28 14:32:48 -04:00
1 parent 0db81b30f3
commit 99b8dc4759
93 files changed
+3825 -1293

No files matched your search

@@ -0,0 +1,245 @@
import { DicomMetadataStore, IWebApiDataSource } from '@ohif/core';
import OHIF from '@ohif/core';
import getImageId from '../DicomWebDataSource/utils/getImageId';
const metadataProvider = OHIF.classes.metadataProvider;
const mappings = {
studyInstanceUid: 'StudyInstanceUID',
patientId: 'PatientID',
};
let _store = {
urls: [],
// {
// url: url1
// studies: [Study1, Study2], // if multiple studies
// }
// {
// url: url2
// studies: [Study1],
// }
// }
};
const getMetaDataByURL = url => {
return _store.urls.find(metaData => metaData.url === url);
};
const findStudies = (key, value) => {
let studies = [];
_store.urls.map(metaData => {
metaData.studies.map(aStudy => {
if (aStudy[key] === value) {
studies.push(aStudy);
}
});
});
return studies;
};
function createDicomJSONApi(dicomJsonConfig) {
const { name } = dicomJsonConfig;
const implementation = {
parseRouteParams: async ({ params, query, url }) => {
if (!url) url = query.get('url');
let metaData = getMetaDataByURL(url);
// if we have already cached the data from this specific url
// We are only handling one StudyInstanceUID to run; however,
// all studies for patientID will be put in the correct tab
if (metaData) {
return metaData.studies.map(aStudy => {
return aStudy.StudyInstanceUID;
});
}
const response = await fetch(url);
let data = await response.json();
const studyInstanceUIDs = data.studies.map(
study => study.StudyInstanceUID
);
let StudyInstanceUID;
let SeriesInstanceUID;
data.studies.forEach(study => {
StudyInstanceUID = study.StudyInstanceUID;
study.series.forEach(series => {
SeriesInstanceUID = series.SeriesInstanceUID;
series.instances.forEach(instance => {
const { url: imageId, metadata: naturalizedDicom } = instance;
// Add imageId specific mapping to this data as the URL isn't necessarliy WADO-URI.
metadataProvider.addImageIdToUIDs(imageId, {
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID: naturalizedDicom.SOPInstanceUID,
});
});
});
});
_store.urls.push({
url,
studies: [...data.studies],
});
return studyInstanceUIDs;
},
query: {
studies: {
mapParams: () => {},
search: async param => {
const [key, value] = Object.entries(param)[0];
const mappedParam = mappings[key];
const studies = findStudies(mappedParam, value);
return studies.map(aStudy => {
return {
accession: aStudy.AccessionNumber,
date: aStudy.StudyDate,
description: aStudy.StudyDescription,
instances: aStudy.NumInstances,
modalities: aStudy.Modalities,
mrn: aStudy.PatientID,
patientName: aStudy.PatientName,
studyInstanceUid: aStudy.StudyInstanceUID,
NumInstances: aStudy.NumInstances,
time: aStudy.StudyTime,
};
});
},
processResults: () => {
console.debug(' DICOMJson QUERY processResults');
},
},
series: {
// mapParams: mapParams.bind(),
search: () => {
console.debug(' DICOMJson QUERY SERIES SEARCH');
},
},
instances: {
search: () => {
console.debug(' DICOMJson QUERY instances SEARCH');
},
},
},
retrieve: {
series: {
metaData: () => {
console.debug(' DICOMJson retrieve series metadata');
},
},
},
store: {
dicom: () => {
console.debug(' DICOMJson store dicom');
},
},
retrieveSeriesMetadata: ({
StudyInstanceUID,
madeInClient = false,
customSort,
} = {}) => {
if (!StudyInstanceUID) {
throw new Error(
'Unable to query for SeriesMetadata without StudyInstanceUID'
);
}
const study = findStudies('StudyInstanceUID', StudyInstanceUID)[0];
let series;
if (customSort) {
series = customSort(study.series);
} else {
series = study.series;
}
const seriesSummaryMetadata = series.map(series => {
const seriesSummary = {
StudyInstanceUID: study.StudyInstanceUID,
...series,
};
delete seriesSummary.instances;
return seriesSummary;
});
// Async load series, store as retrieved
function storeInstances(naturalizedInstances) {
DicomMetadataStore.addInstances(naturalizedInstances, madeInClient);
}
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient);
function setSuccessFlag() {
const study = DicomMetadataStore.getStudy(
StudyInstanceUID,
madeInClient
);
study.isLoaded = true;
}
const numberOfSeries = series.length;
series.forEach((series, index) => {
const instances = series.instances.map(instance => {
const obj = {
...instance.metadata,
url: instance.url,
imageId: instance.url,
...series,
};
delete obj.instances;
return obj;
});
storeInstances(instances);
if (index === numberOfSeries - 1) setSuccessFlag();
});
},
getImageIdsForDisplaySet(displaySet) {
const images = displaySet.images;
const imageIds = [];
if (!images) {
return imageIds;
}
displaySet.images.forEach(instance => {
const NumberOfFrames = instance.NumberOfFrames;
if (NumberOfFrames > 1) {
for (let i = 0; i < NumberOfFrames; i++) {
const imageId = getImageId({
instance,
frame: i,
config: dicomJsonConfig,
});
imageIds.push(imageId);
}
} else {
const imageId = getImageId({ instance, config: dicomJsonConfig });
imageIds.push(imageId);
}
});
return imageIds;
},
getImageIdsForInstance({ instance, frame }) {
const imageIds = getImageId({
instance,
frame,
});
return imageIds;
},
};
return IWebApiDataSource.create(implementation);
}
export { createDicomJSONApi };
@@ -63,10 +63,22 @@ function createDicomWebApi(dicomWebConfig) {
const wadoDicomWebClient = new api.DICOMwebClient(wadoConfig);
const implementation = {
parseRouteParams: ({ params, query }) => {
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
const queryStudyInstanceUIDs = query.get('StudyInstanceUIDs');
const StudyInstanceUIDs =
queryStudyInstanceUIDs || paramsStudyInstanceUIDs;
const StudyInstanceUIDsAsArray =
StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs)
? StudyInstanceUIDs
: [StudyInstanceUIDs];
return StudyInstanceUIDsAsArray;
},
query: {
studies: {
mapParams: mapParams.bind(),
search: async function (origParams) {
search: async function(origParams) {
const { studyInstanceUid, seriesInstanceUid, ...mappedParams } =
mapParams(origParams, {
supportsFuzzyMatching,
@@ -86,7 +98,7 @@ function createDicomWebApi(dicomWebConfig) {
},
series: {
// mapParams: mapParams.bind(),
search: async function (studyInstanceUid) {
search: async function(studyInstanceUid) {
const results = await seriesInStudy(
qidoDicomWebClient,
studyInstanceUid
@@ -129,7 +141,6 @@ function createDicomWebApi(dicomWebConfig) {
DicomMetadataStore.addInstances(naturalizedInstances);
callback(naturalizedInstances);
};
const studyPromises = StudyInstanceUIDs.map(StudyInstanceUID =>
retrieveStudyMetadata(
wadoDicomWebClient,
@@ -177,7 +188,14 @@ function createDicomWebApi(dicomWebConfig) {
await wadoDicomWebClient.storeInstances(options);
},
},
retrieveSeriesMetadata: async ({ StudyInstanceUID } = {}) => {
// TODO: Rename this it makes no sense at all
retrieveSeriesMetadata: async ({
StudyInstanceUID,
filters,
sortCriteria,
sortFunction,
madeInClient = false,
} = {}) => {
if (!StudyInstanceUID) {
throw new Error(
'Unable to query for SeriesMetadata without StudyInstanceUID'
@@ -191,21 +209,34 @@ function createDicomWebApi(dicomWebConfig) {
} = await retrieveStudyMetadata(
wadoDicomWebClient,
StudyInstanceUID,
enableStudyLazyLoad
enableStudyLazyLoad,
filters,
sortCriteria,
sortFunction
);
// Async load series, store as retrieved
function storeInstances(instances) {
const naturalizedInstances = instances.map(naturalizeDataset);
DicomMetadataStore.addInstances(naturalizedInstances);
DicomMetadataStore.addInstances(naturalizedInstances, madeInClient);
}
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata);
function setSuccessFlag() {
const study = DicomMetadataStore.getStudy(
StudyInstanceUID,
madeInClient
);
study.isLoaded = true;
}
seriesPromises.forEach(async seriesPromise => {
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient);
const numberOfSeries = seriesPromises.length;
seriesPromises.forEach(async (seriesPromise, index) => {
const instances = await seriesPromise;
storeInstances(instances);
if (index === numberOfSeries - 1) setSuccessFlag();
});
},
deleteStudyMetadataPromise,
@@ -222,21 +253,28 @@ function createDicomWebApi(dicomWebConfig) {
if (NumberOfFrames > 1) {
for (let i = 0; i < NumberOfFrames; i++) {
const imageId = getImageId({
const imageId = this.getImageIdsForInstance({
instance,
frame: i,
config: dicomWebConfig,
});
imageIds.push(imageId);
}
} else {
const imageId = getImageId({ instance, config: dicomWebConfig });
const imageId = this.getImageIdsForInstance({ instance });
imageIds.push(imageId);
}
});
return imageIds;
},
getImageIdsForInstance({ instance, frame }) {
const imageIds = getImageId({
instance,
frame,
config: dicomWebConfig,
});
return imageIds;
},
};
if (supportsReject) {
@@ -19,7 +19,9 @@ export function retrieveStudyMetadata(
dicomWebClient,
StudyInstanceUID,
enableStudyLazyLoad,
filters
filters,
sortCriteria,
sortFunction
) {
// @TODO: Whenever a study metadata request has failed, its related promise will be rejected once and for all
// and further requests for that metadata will always fail. On failure, we probably need to remove the
@@ -47,7 +49,9 @@ export function retrieveStudyMetadata(
dicomWebClient,
StudyInstanceUID,
enableStudyLazyLoad,
filters
filters,
sortCriteria,
sortFunction
).then(function(data) {
resolve(data);
}, reject);
@@ -34,6 +34,10 @@ export default function getImageId({
return;
}
if (instance.url) {
return instance.url;
}
const renderingAttr = thumbnail ? 'thumbnailRendering' : 'imageRendering';
if (!config[renderingAttr] || config[renderingAttr] === 'wadouri') {
@@ -43,9 +43,11 @@ const sortingCriteria = {
*/
const sortStudySeries = (
series,
seriesSortingCriteria = seriesSortCriteria.default
seriesSortingCriteria = seriesSortCriteria.default,
sortFunction
) => {
return series.sort(seriesSortingCriteria);
if (typeof sortFunction === 'function') return sortFunction(series);
else return series.sort(seriesSortingCriteria);
};
/**
@@ -15,7 +15,9 @@ async function RetrieveMetadata(
dicomWebClient,
studyInstanceUid,
enableStudyLazyLoad,
filters = {}
filters = {},
sortCriteria,
sortFunction
) {
// const RetrieveMetadataLoader =
// enableStudyLazyLoad !== false
@@ -27,7 +29,9 @@ async function RetrieveMetadata(
const retrieveMetadataLoader = new RetrieveMetadataLoader(
dicomWebClient,
studyInstanceUid,
filters
filters,
sortCriteria,
sortFunction
);
const { preLoadData, promises } = await retrieveMetadataLoader.execLoad();
@@ -12,11 +12,20 @@ export default class RetrieveMetadataLoader {
* @param {Array} studyInstanceUID Study instance ui to be retrieved
* @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
* @param {Function} [sortSeries] - Custom sort function for series
*/
constructor(client, studyInstanceUID, filters = {}) {
constructor(
client,
studyInstanceUID,
filters = {},
sortCriteria,
sortFunction
) {
this.client = client;
this.studyInstanceUID = studyInstanceUID;
this.filters = filters;
this.sortCriteria = sortCriteria;
this.sortFunction = sortFunction;
}
async execLoad() {
@@ -2,6 +2,7 @@ import dcmjs from 'dcmjs';
import { sortStudySeries, sortingCriteria } from '../utils/sortStudy';
import RetrieveMetadataLoader from './retrieveMetadataLoader';
/**
* Creates an immutable series loader object which loads each series sequentially using the iterator interface
* @param {DICOMWebClient} dicomWebClient The DICOMWebClient instance to be used for series load
@@ -62,13 +63,17 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
async preLoad() {
const preLoaders = this.getPreLoaders();
const result = await this.runLoaders(preLoaders);
const sortCriteria = this.sortCriteria;
const sortFunction = this.sortFunction;
const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary;
const naturalized = result.map(naturalizeDataset);
return sortStudySeries(
naturalized,
sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria
sortCriteria ||
sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria,
sortFunction
);
}
@@ -21,12 +21,13 @@ export default function PanelMeasurementTable({
// ~~ Subscription
const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
const addedRaw = MeasurementService.EVENTS.RAW_MEASUREMENT_ADDED;
const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED;
const removed = MeasurementService.EVENTS.MEASUREMENT_REMOVED;
const cleared = MeasurementService.EVENTS.MEASUREMENTS_CLEARED;
const subscriptions = [];
[added, updated, removed, cleared].forEach(evt => {
[added, addedRaw, updated, removed, cleared].forEach(evt => {
subscriptions.push(
MeasurementService.subscribe(evt, () => {
debouncedSetDisplayMeasurements(
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { StudyBrowser, useImageViewer } from '@ohif/ui';
import { utils } from '@ohif/core';
@@ -19,7 +19,7 @@ function PanelStudyBrowser({
// Normally you nest the components so the tree isn't so deep, and the data
// doesn't have to have such an intense shape. This works well enough for now.
// Tabs --> Studies --> DisplaySets --> Thumbnails
const [{ StudyInstanceUIDs }, dispatch] = useImageViewer();
const { StudyInstanceUIDs } = useImageViewer();
const [activeTabName, setActiveTabName] = useState('primary');
const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([
...StudyInstanceUIDs,
@@ -27,6 +27,7 @@ function PanelStudyBrowser({
const [studyDisplayList, setStudyDisplayList] = useState([]);
const [displaySets, setDisplaySets] = useState([]);
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
const isMounted = useRef(true);
// ~~ studyDisplayList
useEffect(() => {
@@ -47,8 +48,9 @@ function PanelStudyBrowser({
// displaySets: []
};
});
setStudyDisplayList(actuallyMappedStudies);
if (isMounted.current) {
setStudyDisplayList(actuallyMappedStudies);
}
}
StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid));
@@ -72,11 +74,16 @@ function PanelStudyBrowser({
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
imageId
);
setThumbnailImageSrcMap(prevState => {
return { ...prevState, ...newImageSrcEntry };
});
if (isMounted.current) {
setThumbnailImageSrcMap(prevState => {
return { ...prevState, ...newImageSrcEntry };
});
}
}
});
return () => {
isMounted.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -112,11 +119,14 @@ function PanelStudyBrowser({
if (imageId) {
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
imageId
imageId,
dSet.initialViewport
);
setThumbnailImageSrcMap(prevState => {
return { ...prevState, ...newImageSrcEntry };
});
if (isMounted.current) {
setThumbnailImageSrcMap(prevState => {
return { ...prevState, ...newImageSrcEntry };
});
}
}
});
}
@@ -2,6 +2,7 @@ async function getStudiesForPatientByStudyInstanceUID(
dataSource,
StudyInstanceUID
) {
if (StudyInstanceUID === undefined) return;
// TODO: The `DicomMetadataStore` should short-circuit both of these requests
// Data _could_ be here from route query, or if using JSON data source
// We could also force this to "await" these values being available in the DICOMStore?
+10 -2
View File
@@ -10,6 +10,8 @@ import {
useModal,
} from '@ohif/ui';
import { useAppConfig } from '@state';
function Toolbar({ servicesManager }) {
const { ToolBarService } = servicesManager.services;
const [toolbarButtons, setToolbarButtons] = useState([]);
@@ -45,7 +47,10 @@ function Toolbar({ servicesManager }) {
// isActive if:
// - id is primary?
// - id is in list of "toggled on"?
let isActive;
if (componentProps.type === 'toggle') {
isActive = buttonState.toggles[id];
}
// Also need... to filter list for splitButton, and set primary based on most recently clicked
// Also need to kill the radioGroup button's magic logic
// Everything should be reactive off these props, so commands can inform ToolbarService
@@ -58,6 +63,7 @@ function Toolbar({ servicesManager }) {
id={id}
{...componentProps}
bState={buttonState}
isActive={isActive}
onInteraction={args => ToolBarService.recordInteraction(args)}
/>
);
@@ -78,6 +84,8 @@ function ViewerLayout({
viewports,
ViewportGridComp,
}) {
const [appConfig] = useAppConfig();
const { t } = useTranslation();
const { show, hide } = useModal();
@@ -154,7 +162,7 @@ function ViewerLayout({
return (
<div>
<Header menuOptions={menuOptions}>
<Header menuOptions={menuOptions} WhiteLabeling={appConfig.whiteLabeling} >
<ErrorBoundary context="Primary Toolbar">
<div className="relative flex justify-center">
<Toolbar servicesManager={servicesManager} />
+27 -2
View File
@@ -1,5 +1,11 @@
const commandsModule = ({ servicesManager, commandsManager }) => {
const { MeasurementService, ViewportGridService, ToolBarService, CineService } = servicesManager.services;
const {
MeasurementService,
ViewportGridService,
ToolBarService,
HangingProtocolService,
CineService,
} = servicesManager.services;
const actions = {
clearMeasurements: () => {
@@ -10,7 +16,16 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
const { isCineEnabled } = CineService.getState();
CineService.setIsCineEnabled(!isCineEnabled);
ToolBarService.setButton('Cine', { props: { isActive: !isCineEnabled } });
viewports.forEach((_, index) => CineService.setCine({ id: index, isPlaying: false }));
viewports.forEach((_, index) =>
CineService.setCine({ id: index, isPlaying: false })
);
},
nextStage: () => {
// next stage in hanging protocols
HangingProtocolService.nextProtocolStage();
},
previousStage: () => {
HangingProtocolService.previousProtocolStage();
},
};
@@ -25,6 +40,16 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
storeContexts: [],
options: {},
},
nextStage: {
commandFn: actions.nextStage,
storeContexts: [],
options: {},
},
previousStage: {
commandFn: actions.previousStage,
storeContexts: [],
options: {},
},
};
return {
@@ -3,6 +3,7 @@
// TODO: Use existing DICOMWeb configuration (previously, appConfig, to configure instance)
import { createDicomWebApi } from './DicomWebDataSource/index.js';
import { createDicomJSONApi } from './DicomJSONDataSource/index.js';
/**
*
@@ -14,6 +15,11 @@ function getDataSourcesModule() {
type: 'webApi',
createDataSource: createDicomWebApi,
},
{
name: 'dicomjson',
type: 'jsonApi',
createDataSource: createDicomJSONApi,
},
];
}
@@ -0,0 +1,188 @@
const hangingProtocolName = 'petCT';
const petCTProtocol = {
id: 'PET/CT',
locked: true,
hasUpdatedPriorsInformation: false,
name: 'PET/CT',
createdDate: '2021-02-23T18:32:42.849Z',
modifiedDate: '2021-02-23T18:32:42.849Z',
availableTo: {},
editableBy: {},
protocolMatchingRules: [
{
id: 'wauZK2QNEfDPwcAQo',
weight: 1,
attribute: 'StudyInstanceUID',
constraint: {
equals: {
value: '1.3.6.1.4.1.25403.345050719074.3824.20170125112931.11',
},
},
required: true,
},
],
stages: [
{
id: 'hYbmMy3b7pz7GLiaT',
name: 'oneByTwo',
viewportStructure: {
type: 'grid',
properties: {
rows: 2,
columns: 2,
},
},
viewports: [
{
viewportSettings: [
{
options: {
voi: {
windowWidth: 500,
windowCenter: 500,
},
},
commandName: '',
// Type can be viewport or prop
// viewport: It is most suited for settings that
// should be applied before the first render
// prop: It is the type of command that can be applied
// after the image render, such as tool activations.
type: 'viewport',
},
],
imageMatchingRules: [],
seriesMatchingRules: [
{
id: 'vSjk7NCYjtdS3XZAw',
weight: 1,
attribute: 'SeriesNumber',
constraint: {
equals: {
value: "4",
},
},
required: false,
},
],
studyMatchingRules: [],
},
{
viewportSettings: [
{
options: {
invert: true,
},
commandName: '',
type: 'viewport',
},
],
imageMatchingRules: [
],
seriesMatchingRules: [
{
id: 'GPEYqFLv2dwzCM322',
weight: 1,
attribute: 'SeriesNumber',
constraint: {
equals: {
value: "1",
},
},
required: false,
},
],
studyMatchingRules: [
],
},
{
viewportSettings: [
{
options: {
invert: true,
},
commandName: '',
type: 'viewport',
},
],
imageMatchingRules: [
],
seriesMatchingRules: [
{
id: 'GPEYqFLv2dwzCM322',
weight: 1,
attribute: 'SeriesNumber',
constraint: {
equals: {
value: "6",
},
},
required: false,
},
],
studyMatchingRules: [
],
},
{
viewportSettings: [
{
options: {
invert: true,
},
commandName: '',
type: 'viewport',
},
],
imageMatchingRules: [
],
seriesMatchingRules: [
{
id: 'GPEYqFLv2dwzCM322',
weight: 1,
attribute: 'SeriesDescription',
constraint: {
contains: {
value: "Corrected",
},
},
required: false,
},
{
id: 'GPEYqFLv2dwzCM322',
weight: 2,
attribute: 'SeriesDescription',
constraint: {
doesNotContain: {
value: "Uncorrected",
},
},
required: false,
},
],
studyMatchingRules: [
],
},
],
createdDate: '2021-02-23T18:32:42.850Z',
},
],
numberOfPriorsReferenced: 0,
};
function getHangingProtocolModule() {
return [
{
name: hangingProtocolName,
protocols: [petCTProtocol],
},
];
}
export default getHangingProtocolModule;
+2
View File
@@ -3,6 +3,7 @@ import getDataSourcesModule from './getDataSourcesModule.js';
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
import getPanelModule from './getPanelModule.js';
import getSopClassHandlerModule from './getSopClassHandlerModule.js';
import getHangingProtocolModule from './getHangingProtocolModule.js';
import getToolbarModule from './getToolbarModule.js';
import commandsModule from './commandsModule';
import id from './id';
@@ -14,6 +15,7 @@ export default {
id,
getContextModule,
getDataSourcesModule,
getHangingProtocolModule,
getLayoutTemplateModule,
getPanelModule,
getSopClassHandlerModule,