[OHIF-261 & OHIF-262] (#1862)

* WIP

* Reset viewport grid on mode switch.

* Make lifecyclehooks distinct from modules. Add needed SR hydration state on creation.

* Update extensions/cornerstone/src/init.js

* Remove unused REMOVE_MEASUREMENT listener.

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
This commit is contained in:
James Petts 2020-07-03 18:18:59 +01:00 committed by GitHub
parent 4dfe144bf8
commit 14797a8973
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 156 additions and 25 deletions

View File

@ -5,6 +5,7 @@ import cornerstone from 'cornerstone-core';
import csTools from 'cornerstone-tools'; import csTools from 'cornerstone-tools';
import merge from 'lodash.merge'; import merge from 'lodash.merge';
import initCornerstoneTools from './initCornerstoneTools.js'; import initCornerstoneTools from './initCornerstoneTools.js';
import cornerstoneTools from 'cornerstone-tools';
import initWADOImageLoader from './initWADOImageLoader.js'; import initWADOImageLoader from './initWADOImageLoader.js';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory'; import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
// //
@ -329,6 +330,14 @@ const _connectToolsToMeasurementService = measurementService => {
} }
} }
const { MEASUREMENTS_CLEARED } = measurementService.EVENTS;
measurementService.subscribe(MEASUREMENTS_CLEARED, () => {
cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState(
{}
);
});
const enabledElement = evt.detail.element; const enabledElement = evt.detail.element;
const completedEvt = csTools.EVENTS.MEASUREMENT_COMPLETED; const completedEvt = csTools.EVENTS.MEASUREMENT_COMPLETED;
const updatedEvt = csTools.EVENTS.MEASUREMENT_MODIFIED; const updatedEvt = csTools.EVENTS.MEASUREMENT_MODIFIED;

View File

@ -109,8 +109,8 @@ function OHIFCornerstoneSRViewport({
useEffect(() => { useEffect(() => {
if (!displaySet.isLoaded) { if (!displaySet.isLoaded) {
displaySet.load(); displaySet.load();
setIsHydrated(displaySet.isHydrated);
} }
setIsHydrated(displaySet.isHydrated);
}, [displaySet]); }, [displaySet]);
const setTrackingUniqueIdentifiersForElement = useCallback(targetElement => { const setTrackingUniqueIdentifiersForElement = useCallback(targetElement => {

View File

@ -1,12 +1,10 @@
import id from './id'; import { SOPClassHandlerName, SOPClassHandlerId } from './id';
import { utils, classes } from '@ohif/core'; import { utils, classes } from '@ohif/core';
import addMeasurement from './utils/addMeasurement'; import addMeasurement from './utils/addMeasurement';
import isRehydratable from './utils/isRehydratable'; import isRehydratable from './utils/isRehydratable';
const { ImageSet } = classes; const { ImageSet } = classes;
const sopClassHandlerName = 'dicom-sr';
// TODO -> // TODO ->
// Add SR thumbnail // Add SR thumbnail
// Make viewport // Make viewport
@ -70,7 +68,7 @@ function _getDisplaySetsFromSeries(
if ( if (
!ConceptNameCodeSequence || !ConceptNameCodeSequence ||
ConceptNameCodeSequence.CodeValue !== ConceptNameCodeSequence.CodeValue !==
CodeNameCodeSequenceValues.ImagingMeasurementReport CodeNameCodeSequenceValues.ImagingMeasurementReport
) { ) {
console.warn( console.warn(
'Only support Imaging Measurement Report SRs (TID1500) for now' 'Only support Imaging Measurement Report SRs (TID1500) for now'
@ -79,7 +77,7 @@ function _getDisplaySetsFromSeries(
} }
const displaySet = { const displaySet = {
plugin: id, //plugin: id,
Modality: 'SR', Modality: 'SR',
displaySetInstanceUID: utils.guid(), displaySetInstanceUID: utils.guid(),
SeriesDescription, SeriesDescription,
@ -88,7 +86,7 @@ function _getDisplaySetsFromSeries(
SOPInstanceUID, SOPInstanceUID,
SeriesInstanceUID, SeriesInstanceUID,
StudyInstanceUID, StudyInstanceUID,
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`, SOPClassHandlerId,
referencedImages: null, referencedImages: null,
measurements: null, measurements: null,
isDerivedDisplaySet: true, isDerivedDisplaySet: true,
@ -119,6 +117,7 @@ function _load(displaySet, servicesManager, extensionManager) {
displaySet.isHydrated = false; displaySet.isHydrated = false;
displaySet.isLocked = isRehydratable(displaySet, mappings) ? false : true; displaySet.isLocked = isRehydratable(displaySet, mappings) ? false : true;
displaySet.isLoaded = true;
// Check currently added displaySets and add measurements if the sources exist. // Check currently added displaySets and add measurements if the sources exist.
DisplaySetService.activeDisplaySets.forEach(activeDisplaySet => { DisplaySetService.activeDisplaySets.forEach(activeDisplaySet => {
@ -250,7 +249,7 @@ function getSopClassHandlerModule({ servicesManager, extensionManager }) {
return [ return [
{ {
name: sopClassHandlerName, name: SOPClassHandlerName,
sopClassUids, sopClassUids,
getDisplaySetsFromSeries, getDisplaySetsFromSeries,
}, },
@ -282,7 +281,7 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
trackingUniqueIdentifier => { trackingUniqueIdentifier => {
const mergedContentSequence = const mergedContentSequence =
mergedContentSequencesByTrackingUniqueIdentifiers[ mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier trackingUniqueIdentifier
]; ];
const measurement = _processMeasurement(mergedContentSequence); const measurement = _processMeasurement(mergedContentSequence);
@ -322,7 +321,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(
if ( if (
mergedContentSequencesByTrackingUniqueIdentifiers[ mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier trackingUniqueIdentifier
] === undefined ] === undefined
) { ) {
// Add the full ContentSequence // Add the full ContentSequence
@ -448,9 +447,9 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
const cornerstoneFreeTextFinding = Findings.find( const cornerstoneFreeTextFinding = Findings.find(
Finding => Finding =>
Finding.ConceptCodeSequence.CodingSchemeDesignator === Finding.ConceptCodeSequence.CodingSchemeDesignator ===
CORNERSTONE_CODING_SCHEME_DESIGNATOR && CORNERSTONE_CODING_SCHEME_DESIGNATOR &&
Finding.ConceptCodeSequence.CodeValue === Finding.ConceptCodeSequence.CodeValue ===
CORNERSTONE_FREETEXT_CODE_VALUE CORNERSTONE_FREETEXT_CODE_VALUE
); );
if (cornerstoneFreeTextFinding) { if (cornerstoneFreeTextFinding) {
measurement.labels.push({ measurement.labels.push({

View File

@ -1 +1,7 @@
export default 'org.ohif.dicom-sr'; const id = 'org.ohif.dicom-sr';
export default id;
const SOPClassHandlerName = 'dicom-sr';
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
export { SOPClassHandlerName, SOPClassHandlerId };

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import getSopClassHandlerModule from './getSopClassHandlerModule'; import getSopClassHandlerModule from './getSopClassHandlerModule';
import onModeEnter from './onModeEnter';
import id from './id.js'; import id from './id.js';
import init from './init'; import init from './init';
@ -60,4 +61,5 @@ export default {
return [{ name: 'dicom-sr', component: ExtendedOHIFCornerstoneSRViewport }]; return [{ name: 'dicom-sr', component: ExtendedOHIFCornerstoneSRViewport }];
}, },
getSopClassHandlerModule, getSopClassHandlerModule,
onModeEnter,
}; };

View File

@ -14,7 +14,7 @@ const defaultConfig = {
/** /**
* @param {object} configuration * @param {object} configuration
*/ */
export default function init({ configuration = {} }) { export default function init({ configuration = {}, servicesManager }) {
const conifg = Object.assign({}, defaultConfig, configuration); const conifg = Object.assign({}, defaultConfig, configuration);
TOOL_NAMES.DICOM_SR_DISPLAY_TOOL = conifg.TOOL_NAMES.DICOM_SR_DISPLAY_TOOL; TOOL_NAMES.DICOM_SR_DISPLAY_TOOL = conifg.TOOL_NAMES.DICOM_SR_DISPLAY_TOOL;
@ -22,4 +22,22 @@ export default function init({ configuration = {} }) {
cornerstoneTools.register('module', id, dicomSRModule); cornerstoneTools.register('module', id, dicomSRModule);
cornerstoneTools.addTool(DICOMSRDisplayTool); cornerstoneTools.addTool(DICOMSRDisplayTool);
cornerstoneTools.setToolEnabled(TOOL_NAMES.DICOM_SR_DISPLAY_TOOL); cornerstoneTools.setToolEnabled(TOOL_NAMES.DICOM_SR_DISPLAY_TOOL);
const { DisplaySetService } = servicesManager.services;
DisplaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
data => {
const { displaySetsAdded, options } = data;
displaySetsAdded.forEach(dSet => {
if (options.madeInClient) {
// Set just made displaySets as hydrated.
if (!dSet.isLoaded) {
dSet.load();
}
dSet.isHydrated = true;
}
});
}
);
} }

View File

@ -0,0 +1,15 @@
import { SOPClassHandlerId } from './id';
export default function onModeEnter({ servicesManager }) {
const { DisplaySetService } = servicesManager.services;
const displaySetCache = DisplaySetService.getDisplaySetCache();
const srDisplaySets = displaySetCache.filter(
ds => ds.SOPClassHandlerId === SOPClassHandlerId
);
srDisplaySets.forEach(ds => {
// New mode route, allow SRs to be hydrated again
ds.isHydrated = false;
});
}

View File

@ -16,6 +16,7 @@ export default class ExtensionManager {
this.moduleTypeNames.forEach(moduleType => { this.moduleTypeNames.forEach(moduleType => {
this.modules[moduleType] = []; this.modules[moduleType] = [];
}); });
this._extensionLifeCycleHooks = { onModeEnter: {} };
this.dataSourceMap = {}; this.dataSourceMap = {};
this.defaultDataSourceName = appConfig.defaultDataSourceName; this.defaultDataSourceName = appConfig.defaultDataSourceName;
this.activeDataSource = undefined; this.activeDataSource = undefined;
@ -25,6 +26,27 @@ export default class ExtensionManager {
this.activeDataSource = dataSourceName; this.activeDataSource = dataSourceName;
} }
onModeEnter() {
const {
registeredExtensionIds,
getModuleEntry,
_servicesManager,
_commandsManager,
_extensionLifeCycleHooks,
} = this;
registeredExtensionIds.forEach(extensionId => {
const onModeEnter = _extensionLifeCycleHooks.onModeEnter[extensionId];
if (typeof onModeEnter === 'function') {
onModeEnter({
servicesManager: _servicesManager,
commandsManager: _commandsManager,
});
}
});
}
/** /**
* An array of extensions, or an array of arrays that contains extension * An array of extensions, or an array of arrays that contains extension
* configuration pairs. * configuration pairs.
@ -83,6 +105,11 @@ export default class ExtensionManager {
}); });
} }
if (extension.onModeEnter) {
this._extensionLifeCycleHooks.onModeEnter[extensionId] =
extension.onModeEnter;
}
// Register Modules // Register Modules
this.moduleTypeNames.forEach(moduleType => { this.moduleTypeNames.forEach(moduleType => {
const extensionModule = this._getExtensionModule( const extensionModule = this._getExtensionModule(
@ -146,7 +173,7 @@ export default class ExtensionManager {
}; };
getActiveDataSource = () => { getActiveDataSource = () => {
return this.activeDataSource; return this.dataSourceMap[this.activeDataSource];
}; };
/** /**

View File

@ -5,7 +5,6 @@ const displaySetCache = [];
export default class DisplaySetService { export default class DisplaySetService {
constructor() { constructor() {
this.displaySets = {};
this.activeDisplaySets = []; this.activeDisplaySets = [];
this.listeners = {}; this.listeners = {};
this.EVENTS = EVENTS; this.EVENTS = EVENTS;
@ -33,6 +32,10 @@ export default class DisplaySetService {
}); });
} }
getDisplaySetCache() {
return displaySetCache;
}
getActiveDisplaySets() { getActiveDisplaySets() {
return this.activeDisplaySets; return this.activeDisplaySets;
} }

View File

@ -287,7 +287,7 @@ class MeasurementService {
if (this.measurements[id]) { if (this.measurements[id]) {
const updatedMeasurement = { const updatedMeasurement = {
...measurement, ...measurement,
modifiedTimestamp: Math.floor(Date.now() / 1000) modifiedTimestamp: Math.floor(Date.now() / 1000),
}; };
log.info(`Updating measurement...`, updatedMeasurement); log.info(`Updating measurement...`, updatedMeasurement);
@ -492,6 +492,12 @@ class MeasurementService {
this._broadcastChange(this.EVENTS.MEASUREMENT_REMOVED, source, id); this._broadcastChange(this.EVENTS.MEASUREMENT_REMOVED, source, id);
} }
clearMeasurements() {
this.measurements = {};
this._broadcastChange(this.EVENTS.MEASUREMENTS_CLEARED);
}
_getMappingByMeasurementSource(measurementId, definition) { _getMappingByMeasurementSource(measurementId, definition) {
const measurement = this.getMeasurement(measurementId); const measurement = this.getMeasurement(measurementId);
if (this._isValidSource(measurement.source)) { if (this._isValidSource(measurement.source)) {

View File

@ -8,6 +8,7 @@ const publicAPI = {
setLayout: _setLayout, setLayout: _setLayout,
setCachedLayout: _setCachedLayout, setCachedLayout: _setCachedLayout,
setServiceImplementation, setServiceImplementation,
reset: _reset,
}; };
const serviceImplementation = { const serviceImplementation = {
@ -17,6 +18,7 @@ const serviceImplementation = {
_setDisplaysetForViewport: () => _setDisplaysetForViewport: () =>
console.warn('setDisplaysetForViewport() NOT IMPLEMENTED'), console.warn('setDisplaysetForViewport() NOT IMPLEMENTED'),
_setLayout: () => console.warn('setLayout() NOT IMPLEMENTED'), _setLayout: () => console.warn('setLayout() NOT IMPLEMENTED'),
_reset: () => console.warn('reset() NOT IMPLEMENTED'),
_setCachedLayout: () => console.warn('setCachedLayout() NOT IMPLEMENTED'), _setCachedLayout: () => console.warn('setCachedLayout() NOT IMPLEMENTED'),
}; };
@ -39,6 +41,9 @@ function _setLayout({ numCols, numRows }) {
return serviceImplementation._setLayout({ numCols, numRows }); return serviceImplementation._setLayout({ numCols, numRows });
} }
function _reset() {
return serviceImplementation._reset({});
}
function _setCachedLayout({ numCols, numRows, viewports }) { function _setCachedLayout({ numCols, numRows, viewports }) {
return serviceImplementation._setLayout({ numCols, numRows, viewports }); return serviceImplementation._setLayout({ numCols, numRows, viewports });
} }
@ -49,6 +54,7 @@ function setServiceImplementation({
setDisplaysetForViewport: setDisplaysetForViewportImplementation, setDisplaysetForViewport: setDisplaysetForViewportImplementation,
setCachedLayout: setCachedLayoutImplementation, setCachedLayout: setCachedLayoutImplementation,
setLayout: setLayoutImplementation, setLayout: setLayoutImplementation,
reset: resetImplementation,
}) { }) {
if (getStateImplementation) { if (getStateImplementation) {
serviceImplementation._getState = getStateImplementation; serviceImplementation._getState = getStateImplementation;
@ -62,6 +68,9 @@ function setServiceImplementation({
if (setLayoutImplementation) { if (setLayoutImplementation) {
serviceImplementation._setLayout = setLayoutImplementation; serviceImplementation._setLayout = setLayoutImplementation;
} }
if (resetImplementation) {
serviceImplementation._reset = resetImplementation;
}
if (setCachedLayoutImplementation) { if (setCachedLayoutImplementation) {
serviceImplementation._setCachedLayout = setCachedLayoutImplementation; serviceImplementation._setCachedLayout = setCachedLayoutImplementation;
} }

View File

@ -27,7 +27,11 @@ export function ViewportGridProvider({ children, service }) {
return { ...state, ...{ activeViewportIndex: action.payload } }; return { ...state, ...{ activeViewportIndex: action.payload } };
} }
case 'SET_DISPLAYSET_FOR_VIEWPORT': { case 'SET_DISPLAYSET_FOR_VIEWPORT': {
const { viewportIndex, displaySetInstanceUID, imageIndex } = action.payload; const {
viewportIndex,
displaySetInstanceUID,
imageIndex,
} = action.payload;
const viewports = state.viewports.slice(); const viewports = state.viewports.slice();
viewports[viewportIndex] = { displaySetInstanceUID, imageIndex }; viewports[viewportIndex] = { displaySetInstanceUID, imageIndex };
@ -54,6 +58,17 @@ export function ViewportGridProvider({ children, service }) {
cachedLayout: undefined, cachedLayout: undefined,
}; };
} }
case 'RESET': {
return {
numCols: 1,
numRows: 1,
activeViewportIndex: 0,
viewports: [
{ displaySetInstanceUID: undefined, imageIndex: undefined },
],
};
}
case 'SET_CACHED_LAYOUT': { case 'SET_CACHED_LAYOUT': {
return { ...state, cachedLayout: action.payload }; return { ...state, cachedLayout: action.payload };
} }
@ -79,7 +94,7 @@ export function ViewportGridProvider({ children, service }) {
payload: { payload: {
viewportIndex, viewportIndex,
displaySetInstanceUID, displaySetInstanceUID,
imageIndex imageIndex,
}, },
}), }),
[dispatch] [dispatch]
@ -97,6 +112,14 @@ export function ViewportGridProvider({ children, service }) {
[dispatch] [dispatch]
); );
const reset = useCallback(
() =>
dispatch({
type: 'RESET',
payload: {},
}),
[dispatch]
);
const setCachedLayout = useCallback( const setCachedLayout = useCallback(
payload => payload =>
dispatch({ dispatch({
@ -118,6 +141,7 @@ export function ViewportGridProvider({ children, service }) {
setActiveViewportIndex, setActiveViewportIndex,
setDisplaysetForViewport, setDisplaysetForViewport,
setLayout, setLayout,
reset,
setCachedLayout, setCachedLayout,
}); });
} }
@ -127,6 +151,7 @@ export function ViewportGridProvider({ children, service }) {
setActiveViewportIndex, setActiveViewportIndex,
setDisplaysetForViewport, setDisplaysetForViewport,
setLayout, setLayout,
reset,
setCachedLayout, setCachedLayout,
]); ]);

View File

@ -15,7 +15,7 @@ export default function ModeRoute({
dataSourceName, dataSourceName,
extensionManager, extensionManager,
servicesManager, servicesManager,
hotkeysManager hotkeysManager,
}) { }) {
// Parse route params/querystring // Parse route params/querystring
const query = useQuery(); const query = useQuery();
@ -35,15 +35,19 @@ export default function ModeRoute({
extensionManager.setActiveDataSource(dataSourceName); extensionManager.setActiveDataSource(dataSourceName);
const dataSources = extensionManager.getDataSources(); const dataSources = extensionManager.getActiveDataSource();
// Only handling one instance of the datasource type (E.g. one DICOMWeb server)
const dataSource = dataSources[0]; const dataSource = dataSources[0];
// Only handling one route per mode for now
const route = mode.routes[0]; const route = mode.routes[0];
const { DisplaySetService } = servicesManager.services; const {
DisplaySetService,
MeasurementService,
ViewportGridService,
} = servicesManager.services;
// Only handling one route per mode for now
// You can test via http://localhost:3000/example-mode/dicomweb
const layoutTemplateData = route.layoutTemplate({ location }); const layoutTemplateData = route.layoutTemplate({ location });
const layoutTemplateModuleEntry = extensionManager.getModuleEntry( const layoutTemplateModuleEntry = extensionManager.getModuleEntry(
layoutTemplateData.id layoutTemplateData.id
@ -92,6 +96,14 @@ export default function ModeRoute({
}, []); }, []);
useEffect(() => { useEffect(() => {
// Core
// TOOD -> iterate through services.
MeasurementService.clearMeasurements();
ViewportGridService.reset();
// Extension
extensionManager.onModeEnter();
// Mode
route.init({ servicesManager, extensionManager }); route.init({ servicesManager, extensionManager });
}, [ }, [
mode, mode,
@ -100,7 +112,7 @@ export default function ModeRoute({
route, route,
servicesManager, servicesManager,
extensionManager, extensionManager,
hotkeysManager hotkeysManager,
]); ]);
// This queries for series, but... What does it do with them? // This queries for series, but... What does it do with them?