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

+16 -21
View File
@@ -3,7 +3,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import i18n from '@ohif/i18n';
import { I18nextProvider } from 'react-i18next';
import { BrowserRouter, HashRouter } from 'react-router-dom';
import { Router } from 'react-router-dom';
import {
DialogProvider,
Modal,
@@ -12,7 +12,6 @@ import {
ThemeWrapper,
ViewportDialogProvider,
ViewportGridProvider,
HangingProtocolProvider,
CineProvider,
} from '@ohif/ui';
// Viewer Project
@@ -20,6 +19,7 @@ import {
import { AppConfigProvider } from '@state';
import createRoutes from './routes';
import appInit from './appInit.js';
import history from './history'
// TODO: Temporarily for testing
import '@ohif/mode-longitudinal';
@@ -27,9 +27,7 @@ import '@ohif/mode-longitudinal';
/**
* ENV Variable to determine routing behavior
*/
const Router = JSON.parse(process.env.USE_HASH_ROUTER)
? HashRouter
: BrowserRouter;
const OHIFRouter = Router
let commandsManager, extensionManager, servicesManager, hotkeysManager;
@@ -59,32 +57,29 @@ function App({ config, defaultExtensions }) {
UINotificationService,
UIViewportDialogService,
ViewportGridService, // TODO: Should this be a "UI" Service?
HangingProtocolService,
CineService
} = servicesManager.services;
return (
<AppConfigProvider value={appConfigState}>
<I18nextProvider i18n={i18n}>
<Router basename={routerBasename}>
<OHIFRouter basename={routerBasename} history={history}>
<ThemeWrapper>
<ViewportGridProvider service={ViewportGridService}>
<HangingProtocolProvider service={HangingProtocolService}>
<ViewportDialogProvider service={UIViewportDialogService}>
<CineProvider service={CineService}>
<SnackbarProvider service={UINotificationService}>
<DialogProvider service={UIDialogService}>
<ModalProvider modal={Modal} service={UIModalService}>
{appRoutes}
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
</CineProvider>
</ViewportDialogProvider>
</HangingProtocolProvider>
<ViewportDialogProvider service={UIViewportDialogService}>
<CineProvider service={CineService}>
<SnackbarProvider service={UINotificationService}>
<DialogProvider service={UIDialogService}>
<ModalProvider modal={Modal} service={UIModalService}>
{appRoutes}
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
</CineProvider>
</ViewportDialogProvider>
</ViewportGridProvider>
</ThemeWrapper>
</Router>
</OHIFRouter>
</I18nextProvider>
</AppConfigProvider>
);
+1
View File
@@ -81,6 +81,7 @@ function appInit(appConfigOrFunc, defaultExtensions) {
// appConfig.modes.push(window.segmentationMode);
}
return {
appConfig,
commandsManager,
+51 -29
View File
@@ -26,38 +26,38 @@ function ViewerViewportGrid(props) {
HangingProtocolService,
} = servicesManager.services;
// This is a placeholder for applying hanging protocols
// It probably shouldn't be done here
// For now it just hangs the first display set in the study in 1x1
// as sorted by SeriesNumber
// Using Hanging protocol engine to match the displaysets
useEffect(() => {
const { unsubscribe } = DisplaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
eventData => {
const { displaySetsAdded } = eventData;
const data = HangingProtocolService.getState();
// TODO: Sometimes this is undefined?
const { hpAlreadyApplied } = data;
const [
matchDetails,
hpAlreadyApplied,
] = HangingProtocolService.getState();
if (!matchDetails.length) return;
// Match each viewport individually
const numViewports = numRows * numCols;
const numViewports = viewportGrid.numRows * viewportGrid.numCols;
for (let i = 0; i < numViewports; i++) {
if (hpAlreadyApplied[i] === true) {
return;
continue;
}
// Temporary until matching is ported back over from the Meteor version.
const reqSeriesInstanceUID =
data.hangingProtocol.stages[0].viewports[0].seriesMatchingRules[0]
.constraint.equals.value;
// if current viewport doesn't have a match
if (matchDetails[i] === undefined) return
const { SeriesInstanceUID } = matchDetails[i];
const matchingDisplaySet = displaySetsAdded.find(ds => {
return ds.SeriesInstanceUID === reqSeriesInstanceUID;
return ds.SeriesInstanceUID === SeriesInstanceUID;
});
if (!matchingDisplaySet) {
return;
continue;
}
viewportGridService.setDisplaysetForViewport({
@@ -73,7 +73,23 @@ function ViewerViewportGrid(props) {
return () => {
unsubscribe();
};
}, []);
}, [numRows, numCols]);
// Layout change based on hanging protocols
useEffect(() => {
const { unsubscribe } = HangingProtocolService.subscribe(
HangingProtocolService.EVENTS.NEW_LAYOUT,
({ numRows, numCols }) => {
viewportGridService.setLayout({ numRows, numCols });
}
);
return () => {
unsubscribe();
};
}, [viewports]);
useEffect(() => {
const { unsubscribe } = MeasurementService.subscribe(
@@ -82,16 +98,24 @@ function ViewerViewportGrid(props) {
const referencedDisplaySetInstanceUID =
measurement.displaySetInstanceUID;
// If the viewport does not contain the displaySet, then hang that displaySet.
const viewportsDisplaySetInstanceUIDs = viewports.map(
vp => vp.displaySetInstanceUID
);
// if we already have the displayset in one of the viewports
if (
viewports[viewportIndex].displaySetInstanceUID !==
referencedDisplaySetInstanceUID
viewportsDisplaySetInstanceUIDs.indexOf(
referencedDisplaySetInstanceUID
) > -1
) {
viewportGridService.setDisplaysetForViewport({
viewportIndex,
displaySetInstanceUID: referencedDisplaySetInstanceUID,
});
return;
}
// If not in any of the viewports, hang it inside the active viewport
viewportGridService.setDisplaysetForViewport({
viewportIndex,
displaySetInstanceUID: referencedDisplaySetInstanceUID,
});
}
);
@@ -208,11 +232,9 @@ function ViewerViewportGrid(props) {
return viewportPanes;
};
// const ViewportPanes = React.useMemo(getViewportPanes, [
// viewportComponents,
// activeViewportIndex,
// viewportGrid,
// ]);
if (!numCols || !numCols) {
return null;
}
return (
<ViewportGrid numRows={numRows} numCols={numCols}>
+5
View File
@@ -0,0 +1,5 @@
import { createBrowserHistory, createHashHistory } from 'history';
const useHashRouting = JSON.parse(process.env.USE_HASH_ROUTER);
const router = useHashRouting ? createHashHistory() : createBrowserHistory();
export default router;
+172 -152
View File
@@ -1,14 +1,57 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState, useRef } from 'react';
import { useParams } from 'react-router';
import PropTypes from 'prop-types';
// TODO: DicomMetadataStore should be injected?
import { DicomMetadataStore, utils } from '@ohif/core';
import { DicomMetadataStore } from '@ohif/core';
import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui';
import { useQuery } from '@hooks';
import ViewportGrid from '@components/ViewportGrid';
import Compose from './Compose';
const { isLowPriorityModality } = utils;
async function defaultRouteInit({
servicesManager,
studyInstanceUIDs,
dataSource,
}) {
const {
DisplaySetService,
HangingProtocolService,
} = servicesManager.services;
const unsubscriptions = [];
// TODO: This should be baked into core, not manuall?
// DisplaySetService would wire this up?
const {
unsubscribe: instanceAddedUnsubscribe,
} = DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) => {
const seriesMetadata = DicomMetadataStore.getSeries(
StudyInstanceUID,
SeriesInstanceUID
);
DisplaySetService.makeDisplaySets(seriesMetadata.instances, madeInClient);
}
);
unsubscriptions.push(instanceAddedUnsubscribe);
studyInstanceUIDs.forEach(StudyInstanceUID => {
dataSource.retrieveSeriesMetadata({ StudyInstanceUID });
});
const { unsubscribe: seriesAddedUnsubscribe } = DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.SERIES_ADDED,
({ StudyInstanceUID }) => {
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
HangingProtocolService.run(studyMetadata);
}
);
unsubscriptions.push(seriesAddedUnsubscribe);
return unsubscriptions;
}
export default function ModeRoute({
location,
@@ -20,15 +63,25 @@ export default function ModeRoute({
}) {
// Parse route params/querystring
const query = useQuery();
const queryStudyInstanceUIDs = query.get('StudyInstanceUIDs');
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = useParams();
const StudyInstanceUIDs = queryStudyInstanceUIDs || paramsStudyInstanceUIDs;
const StudyInstanceUIDsAsArray =
StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs)
? StudyInstanceUIDs
: [StudyInstanceUIDs];
const params = useParams();
const { extensions, sopClassHandlers, hotkeys } = mode;
const [studyInstanceUIDs, setStudyInstanceUIDs] = useState();
const [refresh, setRefresh] = useState(false);
const layoutTemplateData = useRef(false);
const locationRef = useRef(null);
const isMounted = useRef(false);
if (location !== locationRef.current) {
layoutTemplateData.current = null;
locationRef.current = location;
}
const {
DisplaySetService,
HangingProtocolService,
} = servicesManager.services;
const { extensions, sopClassHandlers, hotkeys, hangingProtocols } = mode;
if (dataSourceName === undefined) {
dataSourceName = extensionManager.defaultDataSourceName;
@@ -43,19 +96,6 @@ export default function ModeRoute({
// Only handling one route per mode for now
const route = mode.routes[0];
const {
DisplaySetService,
MeasurementService,
ViewportGridService,
HangingProtocolService,
} = servicesManager.services;
const layoutTemplateData = route.layoutTemplate({ location });
const layoutTemplateModuleEntry = extensionManager.getModuleEntry(
layoutTemplateData.id
);
const LayoutComponent = layoutTemplateModuleEntry.component;
// For each extension, look up their context modules
// TODO: move to extension manager.
let contextModules = [];
@@ -81,9 +121,52 @@ export default function ModeRoute({
return ViewportGrid({ ...props, dataSource });
}
useEffect(() => {
// Preventing state update for unmounted component
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
// Todo: this should not be here, data source should not care about params
const initializeDataSource = async (params, query) => {
const studyInstanceUIDs = await dataSource.parseRouteParams({
params,
query,
});
setStudyInstanceUIDs(studyInstanceUIDs);
};
initializeDataSource(params, query);
return () => {
layoutTemplateData.current = null;
};
}, [location]);
useEffect(() => {
const retrieveLayoutData = async () => {
const layoutData = await route.layoutTemplate({
location,
servicesManager,
studyInstanceUIDs,
});
if (isMounted.current) {
layoutTemplateData.current = layoutData;
setRefresh(!refresh);
}
};
if (studyInstanceUIDs?.length && studyInstanceUIDs[0] !== undefined) {
retrieveLayoutData();
}
return () => {
layoutTemplateData.current = null;
};
}, [studyInstanceUIDs]);
useEffect(() => {
if (!hotkeys) {
console.warn('[hotkeys] No bindings defined for hotkeys hook!');
return;
}
@@ -96,19 +179,61 @@ export default function ModeRoute({
}, []);
useEffect(() => {
if (!layoutTemplateData.current) {
return;
}
// TODO: For some reason this is running before the Providers
// are calling setServiceImplementation
// are calling setServiceImplementationf
// TOOD -> iterate through services.
// Extension
// Add SOPClassHandlers to a new SOPClassManager.
DisplaySetService.init(extensionManager, sopClassHandlers);
extensionManager.onModeEnter();
mode?.onModeEnter({ servicesManager, extensionManager });
// Mode
route.init({ servicesManager, extensionManager });
// Adding hanging protocols of extensions after onModeEnter since
// it will reset the protocols
hangingProtocols.forEach(extentionProtocols => {
const hangingProtocolModule = extensionManager.getModuleEntry(extentionProtocols);
if (hangingProtocolModule?.protocols) {
HangingProtocolService.addProtocols(hangingProtocolModule.protocols);
}
});
const setupRouteInit = async () => {
if (route.init) {
return await route.init({
servicesManager,
extensionManager,
hotkeysManager,
studyInstanceUIDs,
dataSource,
});
}
return await defaultRouteInit({
servicesManager,
extensionManager,
hotkeysManager,
studyInstanceUIDs,
dataSource,
});
};
let unsubscriptions;
setupRouteInit().then(unsubs => {
unsubscriptions = unsubs;
});
return () => {
extensionManager.onModeExit();
mode?.onModeExit({ servicesManager, extensionManager });
unsubscriptions.forEach(unsub => {
unsub();
});
};
}, [
mode,
@@ -118,140 +243,35 @@ export default function ModeRoute({
servicesManager,
extensionManager,
hotkeysManager,
studyInstanceUIDs,
refresh,
hangingProtocols,
]);
// This queries for series, but... What does it do with them?
useEffect(() => {
// Add SOPClassHandlers to a new SOPClassManager.
DisplaySetService.init(extensionManager, sopClassHandlers);
// TODO: This should be baked into core, not manuel?
// DisplaySetService would wire this up?
const { unsubscribe } = DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) => {
const seriesMetadata = DicomMetadataStore.getSeries(
StudyInstanceUID,
SeriesInstanceUID
);
DisplaySetService.makeDisplaySets(
seriesMetadata.instances,
madeInClient
);
}
const renderLayoutData = props => {
const layoutTemplateModuleEntry = extensionManager.getModuleEntry(
layoutTemplateData.current.id
);
const LayoutComponent = layoutTemplateModuleEntry.component;
StudyInstanceUIDsAsArray.forEach(StudyInstanceUID => {
dataSource.retrieveSeriesMetadata({ StudyInstanceUID });
});
return unsubscribe;
}, [
mode,
dataSourceName,
location,
DisplaySetService,
extensionManager,
sopClassHandlers,
StudyInstanceUIDsAsArray,
dataSource,
]);
useEffect(() => {
const { unsubscribe } = DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.SERIES_ADDED,
({ StudyInstanceUID }) => {
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
const sortedSeries = studyMetadata.series.sort((a, b) => {
const aLowPriority = isLowPriorityModality(a.Modality);
const bLowPriority = isLowPriorityModality(b.Modality);
if (!aLowPriority && bLowPriority) {
return -1;
}
if (aLowPriority && !bLowPriority) {
return 1;
}
return a.SeriesNumber - b.SeriesNumber;
});
const { SeriesInstanceUID } = sortedSeries[0];
HangingProtocolService.setHangingProtocol({
/*protocolMatchingRules: [
{
id: '7tmuq7KzDMCWFeapc',
weight: 2,
required: false,
attribute: 'x00081030',
constraint: {
contains: {
value: 'DFCI CT CHEST',
},
},
},
],*/
stages: [
{
/*id: 'v5PfGt9F6mffZPif5',
viewportStructure: {
type: 'grid',
properties: {
Rows: 1,
Columns: 1,
},
layoutTemplateName: 'gridLayout',
},*/
viewports: [
{
viewportSettings: {},
imageMatchingRules: [],
seriesMatchingRules: [
{
id: 'mXnsCcNzZL56z7mTZ',
weight: 1,
required: true,
attribute: 'SeriesInstanceUID',
constraint: {
equals: {
value: SeriesInstanceUID,
},
},
},
],
studyMatchingRules: [],
},
],
},
],
});
}
);
return unsubscribe;
}, [
mode,
dataSourceName,
location,
DisplaySetService,
extensionManager,
sopClassHandlers,
StudyInstanceUIDsAsArray,
dataSource,
]);
return <LayoutComponent {...props} />;
};
return (
<ImageViewerProvider
initialState={{ StudyInstanceUIDs: StudyInstanceUIDsAsArray }}
// initialState={{ StudyInstanceUIDs: StudyInstanceUIDs }}
StudyInstanceUIDs={studyInstanceUIDs}
// reducer={reducer}
>
<CombinedContextProvider>
<DragAndDropProvider>
<LayoutComponent
{...layoutTemplateData.props}
StudyInstanceUIDs={StudyInstanceUIDs}
ViewportGridComp={ViewportGridWithDataSource}
/>
{layoutTemplateData.current &&
studyInstanceUIDs?.length &&
studyInstanceUIDs[0] !== undefined &&
renderLayoutData({
...layoutTemplateData.current.props,
ViewportGridComp: ViewportGridWithDataSource,
})}
</DragAndDropProvider>
</CombinedContextProvider>
</ImageViewerProvider>
@@ -408,7 +408,7 @@ function WorkList({
'h-screen': !hasStudies,
})}
>
<Header isSticky menuOptions={menuOptions} isReturnEnabled={false} />
<Header isSticky menuOptions={menuOptions} isReturnEnabled={false} WhiteLabeling={appConfig.whiteLabeling} />
<StudyListFilter
numOfStudies={pageNumber * resultsPerPage > 100 ? 101 : numOfStudies}
filtersMeta={filtersMeta}