From 14797a897368b1e637e80feb5e810ad8327c3e80 Mon Sep 17 00:00:00 2001 From: James Petts Date: Fri, 3 Jul 2020 18:18:59 +0100 Subject: [PATCH] [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 --- extensions/cornerstone/src/init.js | 9 ++++++ .../dicom-sr/src/OHIFCornerstoneSRViewport.js | 2 +- .../dicom-sr/src/getSopClassHandlerModule.js | 21 +++++++------- extensions/dicom-sr/src/id.js | 8 ++++- extensions/dicom-sr/src/index.js | 2 ++ extensions/dicom-sr/src/init.js | 20 ++++++++++++- extensions/dicom-sr/src/onModeEnter.js | 15 ++++++++++ .../core/src/extensions/ExtensionManager.js | 29 ++++++++++++++++++- .../DisplaySetService/DisplaySetService.js | 5 +++- .../MeasurementService/MeasurementService.js | 8 ++++- .../ViewportGridService.js | 9 ++++++ .../contextProviders/ViewportGridProvider.jsx | 29 +++++++++++++++++-- platform/viewer/src/routes/Mode/Mode.jsx | 24 +++++++++++---- 13 files changed, 156 insertions(+), 25 deletions(-) create mode 100644 extensions/dicom-sr/src/onModeEnter.js diff --git a/extensions/cornerstone/src/init.js b/extensions/cornerstone/src/init.js index edfdf4b8b..057d1fe6b 100644 --- a/extensions/cornerstone/src/init.js +++ b/extensions/cornerstone/src/init.js @@ -5,6 +5,7 @@ import cornerstone from 'cornerstone-core'; import csTools from 'cornerstone-tools'; import merge from 'lodash.merge'; import initCornerstoneTools from './initCornerstoneTools.js'; +import cornerstoneTools from 'cornerstone-tools'; import initWADOImageLoader from './initWADOImageLoader.js'; 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 completedEvt = csTools.EVENTS.MEASUREMENT_COMPLETED; const updatedEvt = csTools.EVENTS.MEASUREMENT_MODIFIED; diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js index 5415e2078..bb06f36eb 100644 --- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -109,8 +109,8 @@ function OHIFCornerstoneSRViewport({ useEffect(() => { if (!displaySet.isLoaded) { displaySet.load(); - setIsHydrated(displaySet.isHydrated); } + setIsHydrated(displaySet.isHydrated); }, [displaySet]); const setTrackingUniqueIdentifiersForElement = useCallback(targetElement => { diff --git a/extensions/dicom-sr/src/getSopClassHandlerModule.js b/extensions/dicom-sr/src/getSopClassHandlerModule.js index 66d35127e..2569e34d5 100644 --- a/extensions/dicom-sr/src/getSopClassHandlerModule.js +++ b/extensions/dicom-sr/src/getSopClassHandlerModule.js @@ -1,12 +1,10 @@ -import id from './id'; +import { SOPClassHandlerName, SOPClassHandlerId } from './id'; import { utils, classes } from '@ohif/core'; import addMeasurement from './utils/addMeasurement'; import isRehydratable from './utils/isRehydratable'; const { ImageSet } = classes; -const sopClassHandlerName = 'dicom-sr'; - // TODO -> // Add SR thumbnail // Make viewport @@ -70,7 +68,7 @@ function _getDisplaySetsFromSeries( if ( !ConceptNameCodeSequence || ConceptNameCodeSequence.CodeValue !== - CodeNameCodeSequenceValues.ImagingMeasurementReport + CodeNameCodeSequenceValues.ImagingMeasurementReport ) { console.warn( 'Only support Imaging Measurement Report SRs (TID1500) for now' @@ -79,7 +77,7 @@ function _getDisplaySetsFromSeries( } const displaySet = { - plugin: id, + //plugin: id, Modality: 'SR', displaySetInstanceUID: utils.guid(), SeriesDescription, @@ -88,7 +86,7 @@ function _getDisplaySetsFromSeries( SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID, - SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`, + SOPClassHandlerId, referencedImages: null, measurements: null, isDerivedDisplaySet: true, @@ -119,6 +117,7 @@ function _load(displaySet, servicesManager, extensionManager) { displaySet.isHydrated = false; displaySet.isLocked = isRehydratable(displaySet, mappings) ? false : true; + displaySet.isLoaded = true; // Check currently added displaySets and add measurements if the sources exist. DisplaySetService.activeDisplaySets.forEach(activeDisplaySet => { @@ -250,7 +249,7 @@ function getSopClassHandlerModule({ servicesManager, extensionManager }) { return [ { - name: sopClassHandlerName, + name: SOPClassHandlerName, sopClassUids, getDisplaySetsFromSeries, }, @@ -282,7 +281,7 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) { trackingUniqueIdentifier => { const mergedContentSequence = mergedContentSequencesByTrackingUniqueIdentifiers[ - trackingUniqueIdentifier + trackingUniqueIdentifier ]; const measurement = _processMeasurement(mergedContentSequence); @@ -322,7 +321,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers( if ( mergedContentSequencesByTrackingUniqueIdentifiers[ - trackingUniqueIdentifier + trackingUniqueIdentifier ] === undefined ) { // Add the full ContentSequence @@ -448,9 +447,9 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { const cornerstoneFreeTextFinding = Findings.find( Finding => Finding.ConceptCodeSequence.CodingSchemeDesignator === - CORNERSTONE_CODING_SCHEME_DESIGNATOR && + CORNERSTONE_CODING_SCHEME_DESIGNATOR && Finding.ConceptCodeSequence.CodeValue === - CORNERSTONE_FREETEXT_CODE_VALUE + CORNERSTONE_FREETEXT_CODE_VALUE ); if (cornerstoneFreeTextFinding) { measurement.labels.push({ diff --git a/extensions/dicom-sr/src/id.js b/extensions/dicom-sr/src/id.js index f9ab81f91..623f69a90 100644 --- a/extensions/dicom-sr/src/id.js +++ b/extensions/dicom-sr/src/id.js @@ -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 }; diff --git a/extensions/dicom-sr/src/index.js b/extensions/dicom-sr/src/index.js index 498f51517..ec5121b13 100644 --- a/extensions/dicom-sr/src/index.js +++ b/extensions/dicom-sr/src/index.js @@ -1,5 +1,6 @@ import React from 'react'; import getSopClassHandlerModule from './getSopClassHandlerModule'; +import onModeEnter from './onModeEnter'; import id from './id.js'; import init from './init'; @@ -60,4 +61,5 @@ export default { return [{ name: 'dicom-sr', component: ExtendedOHIFCornerstoneSRViewport }]; }, getSopClassHandlerModule, + onModeEnter, }; diff --git a/extensions/dicom-sr/src/init.js b/extensions/dicom-sr/src/init.js index 03d203167..ca8a9610b 100644 --- a/extensions/dicom-sr/src/init.js +++ b/extensions/dicom-sr/src/init.js @@ -14,7 +14,7 @@ const defaultConfig = { /** * @param {object} configuration */ -export default function init({ configuration = {} }) { +export default function init({ configuration = {}, servicesManager }) { const conifg = Object.assign({}, defaultConfig, configuration); 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.addTool(DICOMSRDisplayTool); 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; + } + }); + } + ); } diff --git a/extensions/dicom-sr/src/onModeEnter.js b/extensions/dicom-sr/src/onModeEnter.js new file mode 100644 index 000000000..fcd6d5407 --- /dev/null +++ b/extensions/dicom-sr/src/onModeEnter.js @@ -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; + }); +} diff --git a/platform/core/src/extensions/ExtensionManager.js b/platform/core/src/extensions/ExtensionManager.js index 0ce25d6b6..0993e31ef 100644 --- a/platform/core/src/extensions/ExtensionManager.js +++ b/platform/core/src/extensions/ExtensionManager.js @@ -16,6 +16,7 @@ export default class ExtensionManager { this.moduleTypeNames.forEach(moduleType => { this.modules[moduleType] = []; }); + this._extensionLifeCycleHooks = { onModeEnter: {} }; this.dataSourceMap = {}; this.defaultDataSourceName = appConfig.defaultDataSourceName; this.activeDataSource = undefined; @@ -25,6 +26,27 @@ export default class ExtensionManager { 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 * configuration pairs. @@ -83,6 +105,11 @@ export default class ExtensionManager { }); } + if (extension.onModeEnter) { + this._extensionLifeCycleHooks.onModeEnter[extensionId] = + extension.onModeEnter; + } + // Register Modules this.moduleTypeNames.forEach(moduleType => { const extensionModule = this._getExtensionModule( @@ -146,7 +173,7 @@ export default class ExtensionManager { }; getActiveDataSource = () => { - return this.activeDataSource; + return this.dataSourceMap[this.activeDataSource]; }; /** diff --git a/platform/core/src/services/DisplaySetService/DisplaySetService.js b/platform/core/src/services/DisplaySetService/DisplaySetService.js index 92f8a0043..bd81d0325 100644 --- a/platform/core/src/services/DisplaySetService/DisplaySetService.js +++ b/platform/core/src/services/DisplaySetService/DisplaySetService.js @@ -5,7 +5,6 @@ const displaySetCache = []; export default class DisplaySetService { constructor() { - this.displaySets = {}; this.activeDisplaySets = []; this.listeners = {}; this.EVENTS = EVENTS; @@ -33,6 +32,10 @@ export default class DisplaySetService { }); } + getDisplaySetCache() { + return displaySetCache; + } + getActiveDisplaySets() { return this.activeDisplaySets; } diff --git a/platform/core/src/services/MeasurementService/MeasurementService.js b/platform/core/src/services/MeasurementService/MeasurementService.js index 34f1a398c..fde47eb0d 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.js +++ b/platform/core/src/services/MeasurementService/MeasurementService.js @@ -287,7 +287,7 @@ class MeasurementService { if (this.measurements[id]) { const updatedMeasurement = { ...measurement, - modifiedTimestamp: Math.floor(Date.now() / 1000) + modifiedTimestamp: Math.floor(Date.now() / 1000), }; log.info(`Updating measurement...`, updatedMeasurement); @@ -492,6 +492,12 @@ class MeasurementService { this._broadcastChange(this.EVENTS.MEASUREMENT_REMOVED, source, id); } + clearMeasurements() { + this.measurements = {}; + + this._broadcastChange(this.EVENTS.MEASUREMENTS_CLEARED); + } + _getMappingByMeasurementSource(measurementId, definition) { const measurement = this.getMeasurement(measurementId); if (this._isValidSource(measurement.source)) { diff --git a/platform/core/src/services/ViewportGridService/ViewportGridService.js b/platform/core/src/services/ViewportGridService/ViewportGridService.js index 23c984668..cfc2b48a7 100644 --- a/platform/core/src/services/ViewportGridService/ViewportGridService.js +++ b/platform/core/src/services/ViewportGridService/ViewportGridService.js @@ -8,6 +8,7 @@ const publicAPI = { setLayout: _setLayout, setCachedLayout: _setCachedLayout, setServiceImplementation, + reset: _reset, }; const serviceImplementation = { @@ -17,6 +18,7 @@ const serviceImplementation = { _setDisplaysetForViewport: () => console.warn('setDisplaysetForViewport() NOT IMPLEMENTED'), _setLayout: () => console.warn('setLayout() NOT IMPLEMENTED'), + _reset: () => console.warn('reset() NOT IMPLEMENTED'), _setCachedLayout: () => console.warn('setCachedLayout() NOT IMPLEMENTED'), }; @@ -39,6 +41,9 @@ function _setLayout({ numCols, numRows }) { return serviceImplementation._setLayout({ numCols, numRows }); } +function _reset() { + return serviceImplementation._reset({}); +} function _setCachedLayout({ numCols, numRows, viewports }) { return serviceImplementation._setLayout({ numCols, numRows, viewports }); } @@ -49,6 +54,7 @@ function setServiceImplementation({ setDisplaysetForViewport: setDisplaysetForViewportImplementation, setCachedLayout: setCachedLayoutImplementation, setLayout: setLayoutImplementation, + reset: resetImplementation, }) { if (getStateImplementation) { serviceImplementation._getState = getStateImplementation; @@ -62,6 +68,9 @@ function setServiceImplementation({ if (setLayoutImplementation) { serviceImplementation._setLayout = setLayoutImplementation; } + if (resetImplementation) { + serviceImplementation._reset = resetImplementation; + } if (setCachedLayoutImplementation) { serviceImplementation._setCachedLayout = setCachedLayoutImplementation; } diff --git a/platform/ui/src/contextProviders/ViewportGridProvider.jsx b/platform/ui/src/contextProviders/ViewportGridProvider.jsx index bfc9b3d51..2eb184537 100644 --- a/platform/ui/src/contextProviders/ViewportGridProvider.jsx +++ b/platform/ui/src/contextProviders/ViewportGridProvider.jsx @@ -27,7 +27,11 @@ export function ViewportGridProvider({ children, service }) { return { ...state, ...{ activeViewportIndex: action.payload } }; } case 'SET_DISPLAYSET_FOR_VIEWPORT': { - const { viewportIndex, displaySetInstanceUID, imageIndex } = action.payload; + const { + viewportIndex, + displaySetInstanceUID, + imageIndex, + } = action.payload; const viewports = state.viewports.slice(); viewports[viewportIndex] = { displaySetInstanceUID, imageIndex }; @@ -54,6 +58,17 @@ export function ViewportGridProvider({ children, service }) { cachedLayout: undefined, }; } + case 'RESET': { + return { + numCols: 1, + numRows: 1, + activeViewportIndex: 0, + viewports: [ + { displaySetInstanceUID: undefined, imageIndex: undefined }, + ], + }; + } + case 'SET_CACHED_LAYOUT': { return { ...state, cachedLayout: action.payload }; } @@ -79,7 +94,7 @@ export function ViewportGridProvider({ children, service }) { payload: { viewportIndex, displaySetInstanceUID, - imageIndex + imageIndex, }, }), [dispatch] @@ -97,6 +112,14 @@ export function ViewportGridProvider({ children, service }) { [dispatch] ); + const reset = useCallback( + () => + dispatch({ + type: 'RESET', + payload: {}, + }), + [dispatch] + ); const setCachedLayout = useCallback( payload => dispatch({ @@ -118,6 +141,7 @@ export function ViewportGridProvider({ children, service }) { setActiveViewportIndex, setDisplaysetForViewport, setLayout, + reset, setCachedLayout, }); } @@ -127,6 +151,7 @@ export function ViewportGridProvider({ children, service }) { setActiveViewportIndex, setDisplaysetForViewport, setLayout, + reset, setCachedLayout, ]); diff --git a/platform/viewer/src/routes/Mode/Mode.jsx b/platform/viewer/src/routes/Mode/Mode.jsx index 5bf8e37f2..65daaeb6f 100644 --- a/platform/viewer/src/routes/Mode/Mode.jsx +++ b/platform/viewer/src/routes/Mode/Mode.jsx @@ -15,7 +15,7 @@ export default function ModeRoute({ dataSourceName, extensionManager, servicesManager, - hotkeysManager + hotkeysManager, }) { // Parse route params/querystring const query = useQuery(); @@ -35,15 +35,19 @@ export default function ModeRoute({ 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]; + // Only handling one route per mode for now 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 layoutTemplateModuleEntry = extensionManager.getModuleEntry( layoutTemplateData.id @@ -92,6 +96,14 @@ export default function ModeRoute({ }, []); useEffect(() => { + // Core + + // TOOD -> iterate through services. + MeasurementService.clearMeasurements(); + ViewportGridService.reset(); + // Extension + extensionManager.onModeEnter(); + // Mode route.init({ servicesManager, extensionManager }); }, [ mode, @@ -100,7 +112,7 @@ export default function ModeRoute({ route, servicesManager, extensionManager, - hotkeysManager + hotkeysManager, ]); // This queries for series, but... What does it do with them?