ohif-viewer/platform/viewer/src/routes/Mode/Mode.tsx
Bill Wallace e401358887
fix(hp): Make the updated hanging protocol compatible with modes (#2971)
There had been a change made to make the hanging protocols
auto initialize, but that caused problems on mode init
because the couldn't specify the hanging protocols any longer.
THis change restores that, and also improves the API a little
bit by allowing the display set selectors to be specified by id
instead of positionally.

fix(hp):PR Review Comments

fix(hp):Unit test and docs
2022-10-05 08:32:54 -04:00

339 lines
9.3 KiB
TypeScript

import React, { useEffect, useState, useRef } from 'react';
import { useParams, useLocation } from 'react-router';
import PropTypes from 'prop-types';
// TODO: DicomMetadataStore should be injected?
import { DicomMetadataStore } from '@ohif/core';
import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui';
import { useQuery } from '@hooks';
import ViewportGrid from '@components/ViewportGrid';
import Compose from './Compose';
/**
* Initialize the route.
*
* @param props.servicesManager to read services from
* @param props.studyInstanceUIDs for a list of studies to read
* @param props.dataSource to read the data from
* @returns array of subscriptions to cancel
*/
function defaultRouteInit(
{ servicesManager, studyInstanceUIDs, dataSource },
hangingProtocol
) {
const {
DisplaySetService,
HangingProtocolService,
} = servicesManager.services;
const unsubscriptions = [];
const {
unsubscribe: instanceAddedUnsubscribe,
} = DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
function({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) {
const seriesMetadata = DicomMetadataStore.getSeries(
StudyInstanceUID,
SeriesInstanceUID
);
DisplaySetService.makeDisplaySets(seriesMetadata.instances, madeInClient);
}
);
unsubscriptions.push(instanceAddedUnsubscribe);
const allRetrieves = studyInstanceUIDs.map(StudyInstanceUID =>
dataSource.retrieve.series.metadata({ StudyInstanceUID })
);
// The hanging protocol matching service is fairly expensive to run multiple
// times, and doesn't allow partial matches to be made (it will simply fail
// to display anything if a required match fails), so we wait here until all metadata
// is retrieved (which will synchronously trigger the display set creation)
// until we run the hanging protocol matching service.
Promise.allSettled(allRetrieves).then(() => {
const displaySets = DisplaySetService.getActiveDisplaySets();
if (!displaySets || !displaySets.length) {
return;
}
const studyMap = {};
// Prior studies don't quite work properly yet, but the studies list
// is at least being generated and passed in.
const studies = displaySets.reduce((prev, curr) => {
const { StudyInstanceUID } = curr;
if (!studyMap[StudyInstanceUID]) {
const study = DicomMetadataStore.getStudy(StudyInstanceUID);
studyMap[StudyInstanceUID] = study;
prev.push(study);
}
return prev;
}, []);
// The assumption is that the display set at position 0 is the first
// study being displayed, and is thus the "active" study.
const activeStudy = studies[0];
// run the hanging protocol matching on the displaySets with the predefined
// hanging protocol in the mode configuration
HangingProtocolService.run(
{ studies, activeStudy, displaySets },
hangingProtocol
);
});
return unsubscriptions;
}
export default function ModeRoute({
mode,
dataSourceName,
extensionManager,
servicesManager,
commandsManager,
hotkeysManager,
}) {
// Parse route params/querystring
const location = useLocation();
const query = useQuery();
const params = useParams();
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: hangingProtocolService,
} = servicesManager.services;
const { extensions, sopClassHandlers, hotkeys, hangingProtocol } = mode;
if (dataSourceName === undefined) {
dataSourceName = extensionManager.defaultDataSourceName;
}
extensionManager.setActiveDataSource(dataSourceName);
const dataSources = extensionManager.getActiveDataSource();
// Only handling one instance of the datasource type (E.g. one DICOMWeb server)
const dataSource = dataSources[0];
// Only handling one route per mode for now
const route = mode.routes[0];
// For each extension, look up their context modules
// TODO: move to extension manager.
let contextModules = [];
Object.keys(extensions).forEach(extensionId => {
const allRegisteredModuleIds = Object.keys(extensionManager.modulesMap);
const moduleIds = allRegisteredModuleIds.filter(id =>
id.includes(`${extensionId}.contextModule.`)
);
if (!moduleIds || !moduleIds.length) {
return;
}
const modules = moduleIds.map(extensionManager.getModuleEntry);
contextModules = contextModules.concat(modules);
});
const contextModuleProviders = contextModules.map(a => a.provider);
const CombinedContextProvider = ({ children }) =>
Compose({ components: contextModuleProviders, children });
function ViewportGridWithDataSource(props) {
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.initialize({
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) {
return;
}
hotkeysManager.setDefaultHotKeys(hotkeys);
const userPreferredHotkeys = JSON.parse(
localStorage.getItem('hotkey-definitions')
);
if (userPreferredHotkeys?.length) {
hotkeysManager.setHotkeys(userPreferredHotkeys);
} else {
hotkeysManager.setHotkeys(hotkeys);
}
return () => {
hotkeysManager.destroy();
};
}, []);
useEffect(() => {
if (!layoutTemplateData.current) {
return;
}
// TODO: For some reason this is running before the Providers
// are calling setServiceImplementation
// TODO -> iterate through services.
// Extension
// Add SOPClassHandlers to a new SOPClassManager.
DisplaySetService.init(extensionManager, sopClassHandlers);
extensionManager.onModeEnter({
servicesManager,
extensionManager,
commandsManager,
});
// Sets the active hanging protocols - if hangingProtocol is undefined,
// resets to default. Done before the onModeEnter to allow the onModeEnter
// to perform custom hanging protocol actions
hangingProtocolService.setActiveProtocols(hangingProtocol);
mode?.onModeEnter({ servicesManager, extensionManager, commandsManager });
const setupRouteInit = async () => {
if (route.init) {
return await route.init(
{
servicesManager,
extensionManager,
hotkeysManager,
studyInstanceUIDs,
dataSource,
},
hangingProtocol
);
}
return defaultRouteInit(
{
servicesManager,
studyInstanceUIDs,
dataSource,
},
hangingProtocol
);
};
let unsubscriptions;
setupRouteInit().then(unsubs => {
unsubscriptions = unsubs;
});
return () => {
extensionManager.onModeExit();
mode?.onModeExit({ servicesManager, extensionManager });
unsubscriptions.forEach(unsub => {
unsub();
});
};
}, [
mode,
dataSourceName,
location,
route,
servicesManager,
extensionManager,
hotkeysManager,
studyInstanceUIDs,
refresh,
]);
const renderLayoutData = props => {
const layoutTemplateModuleEntry = extensionManager.getModuleEntry(
layoutTemplateData.current.id
);
const LayoutComponent = layoutTemplateModuleEntry.component;
return <LayoutComponent {...props} />;
};
return (
<ImageViewerProvider
// initialState={{ StudyInstanceUIDs: StudyInstanceUIDs }}
StudyInstanceUIDs={studyInstanceUIDs}
// reducer={reducer}
>
<CombinedContextProvider>
<DragAndDropProvider>
{layoutTemplateData.current &&
studyInstanceUIDs?.length &&
studyInstanceUIDs[0] !== undefined &&
renderLayoutData({
...layoutTemplateData.current.props,
ViewportGridComp: ViewportGridWithDataSource,
})}
</DragAndDropProvider>
</CombinedContextProvider>
</ImageViewerProvider>
);
}
ModeRoute.propTypes = {
mode: PropTypes.object.isRequired,
dataSourceName: PropTypes.string,
extensionManager: PropTypes.object,
servicesManager: PropTypes.object,
hotkeysManager: PropTypes.object,
};