[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 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;

View File

@ -109,8 +109,8 @@ function OHIFCornerstoneSRViewport({
useEffect(() => {
if (!displaySet.isLoaded) {
displaySet.load();
setIsHydrated(displaySet.isHydrated);
}
setIsHydrated(displaySet.isHydrated);
}, [displaySet]);
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 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({

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 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,
};

View File

@ -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;
}
});
}
);
}

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.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];
};
/**

View File

@ -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;
}

View File

@ -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)) {

View File

@ -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;
}

View File

@ -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,
]);

View File

@ -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?