[OHIF-339] - workflow changes (#1931)

* workflow changes

* Cherry pick OHIF-310 into this PR.

* fix label bug and dead code

* remove comment

* Wire up CreateReportButton

* fix additionalFindingTypes + save interaction

* Remove unused createReportAsync call

* stub for report hydration prompt

* Update PR

* Remove debuggers

* push updated code

* Push updates

* push

* fix sidebar save

* enable tracking of new study or series

* fix for duplicate created report series in sidebard

* fix for unexpected viewport dialog after visiting study list then a new study

* Fix for hydration not marking multiple series as active

* Reviewer changes.

* Respond to final review comment, add series description setting for export.

Co-authored-by: James A. Petts <jamesapetts@gmail.com>
This commit is contained in:
Danny BrownandJames A. Petts authored and GitHub committed 2020-08-04 11:11:23 +02:00
1 parent 5f487840a1
commit 2a51d75f77
26 files changed
+948 -391

No files matched your search

+33 -9
View File
@@ -6,6 +6,7 @@ import {
stowSRFromMeasurements,
} from './handleStructuredReport';
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import dcmjs from 'dcmjs';
@@ -47,11 +48,20 @@ const retrieveMeasurements = server => {
*
* @param {object[]} measurementData An array of measurements from the measurements service
* @param {string[]} additionalFindingTypes toolTypes that should be stored with labels as Findings
* @param {object} options Naturalized DICOM JSON headers to merge into the displaySet.
* as opposed to Finding Sites.
* that you wish to serialize.
*/
const downloadReport = (measurementData, additionalFindingTypes = []) => {
const srDataset = generateReport(measurementData, additionalFindingTypes);
const downloadReport = (
measurementData,
additionalFindingTypes,
options = {}
) => {
const srDataset = generateReport(
measurementData,
additionalFindingTypes,
options
);
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
//Create a URL for the binary.
@@ -64,19 +74,29 @@ const downloadReport = (measurementData, additionalFindingTypes = []) => {
* @param {object[]} measurementData An array of measurements from the measurements service
* that you wish to serialize.
* @param {string[]} additionalFindingTypes toolTypes that should be stored with labels as Findings
* @param {object} options Naturalized DICOM JSON headers to merge into the displaySet.
*
*/
const generateReport = (measurementData, additionalFindingTypes) => {
const generateReport = (
measurementData,
additionalFindingTypes,
options = {}
) => {
const filteredToolState = _getFilteredCornerstoneToolState(
measurementData,
additionalFindingTypes
);
const report = MeasurementReport.generateReport(
filteredToolState,
cornerstone.metaData
);
return report.dataset;
const { dataset } = report;
// Add in top level series options
Object.assign(dataset, options);
return dataset;
};
/**
@@ -85,13 +105,14 @@ const generateReport = (measurementData, additionalFindingTypes) => {
* that you wish to serialize.
* @param {object} dataSource The dataSource that you wish to use to persist the data.
* @param {string[]} additionalFindingTypes toolTypes that should be stored with labels as Findings
* as opposed to Finding Sites.
* @param {object} options Naturalized DICOM JSON headers to merge into the displaySet.
* @return {object} The naturalized report
*/
const storeMeasurements = async (
measurementData,
dataSource,
additionalFindingTypes = []
additionalFindingTypes,
options = {}
) => {
// TODO -> Eventually use the measurements directly and not the dcmjs adapter,
// But it is good enough for now whilst we only have cornerstone as a datasource.
@@ -105,7 +126,8 @@ const storeMeasurements = async (
try {
const naturalizedReport = generateReport(
measurementData,
additionalFindingTypes
additionalFindingTypes,
options
);
const { StudyInstanceUID } = naturalizedReport;
@@ -117,6 +139,7 @@ const storeMeasurements = async (
return naturalizedReport;
} catch (error) {
console.warn(error);
log.error(
`[DICOMSR] Error while saving the measurements: ${error.message}`
);
@@ -124,6 +147,8 @@ const storeMeasurements = async (
}
};
// _getFilteredCornerstoneToolState
// DIFFERENT IMPLEMENTATION HERE! What's up?
function _getFilteredCornerstoneToolState(
measurementData,
additionalFindingTypes
@@ -147,7 +172,6 @@ function _getFilteredCornerstoneToolState(
}
const measurmentDataI = measurementData.find(md => md.id === toolDataI.id);
const toolData = imageIdSpecificToolState[toolType].data;
let finding;
@@ -16,7 +16,7 @@ export default class ExtensionManager {
this.moduleTypeNames.forEach(moduleType => {
this.modules[moduleType] = [];
});
this._extensionLifeCycleHooks = { onModeEnter: {} };
this._extensionLifeCycleHooks = { onModeEnter: {}, onModeExit: {} };
this.dataSourceMap = {};
this.defaultDataSourceName = appConfig.defaultDataSourceName;
this.activeDataSource = undefined;
@@ -29,12 +29,21 @@ export default class ExtensionManager {
onModeEnter() {
const {
registeredExtensionIds,
getModuleEntry,
_servicesManager,
_commandsManager,
_extensionLifeCycleHooks,
} = this;
const {
MeasurementService,
ViewportGridService,
HangingProtocolService,
} = _servicesManager.services;
MeasurementService.clearMeasurements();
ViewportGridService.reset();
HangingProtocolService.reset();
registeredExtensionIds.forEach(extensionId => {
const onModeEnter = _extensionLifeCycleHooks.onModeEnter[extensionId];
@@ -47,6 +56,36 @@ export default class ExtensionManager {
});
}
onModeExit() {
const {
registeredExtensionIds,
_servicesManager,
_commandsManager,
_extensionLifeCycleHooks,
} = this;
const {
MeasurementService,
ViewportGridService,
HangingProtocolService,
} = _servicesManager.services;
MeasurementService.clearMeasurements();
ViewportGridService.reset();
HangingProtocolService.reset();
registeredExtensionIds.forEach(extensionId => {
const onModeExit = _extensionLifeCycleHooks.onModeExit[extensionId];
if (typeof onModeExit === 'function') {
onModeExit({
servicesManager: _servicesManager,
commandsManager: _commandsManager,
});
}
});
}
/**
* An array of extensions, or an array of arrays that contains extension
* configuration pairs.
@@ -110,6 +149,11 @@ export default class ExtensionManager {
extension.onModeEnter;
}
if (extension.onModeExit) {
this._extensionLifeCycleHooks.onModeExit[extensionId] =
extension.onModeExit;
}
// Register Modules
this.moduleTypeNames.forEach(moduleType => {
const extensionModule = this._getExtensionModule(
@@ -77,10 +77,10 @@ class ConformanceCriteria {
this.nonconformities = nonconformities;
this.groupedNonConformities = groupedNonConformities;
console.warn('nonconformities');
console.warn(nonconformities);
console.warn('groupedNonConformities');
console.warn(groupedNonConformities);
// console.warn('nonconformities');
// console.warn(nonconformities);
// console.warn('groupedNonConformities');
// console.warn(groupedNonConformities);
return nonconformities;
}
@@ -57,9 +57,7 @@ function _getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) {
const BaseImplementation = {
EVENTS,
listeners: {},
// TODO: The assumption is that this is called per Study per Series
// We should do more to verify/clarify that
addInstances(instances) {
addInstances(instances, madeInClient = false) {
const { StudyInstanceUID, SeriesInstanceUID } = instances[0];
let study = _model.studies.find(
@@ -81,6 +79,7 @@ const BaseImplementation = {
this._broadcastEvent(EVENTS.INSTANCES_ADDED, {
StudyInstanceUID,
SeriesInstanceUID,
madeInClient,
});
},
addSeriesMetadata(seriesSummaryMetadata) {
@@ -36,6 +36,10 @@ export default class DisplaySetService {
return displaySetCache;
}
getMostRecentDisplaySet() {
return this.activeDisplaySets[this.activeDisplaySets.length - 1];
}
getActiveDisplaySets() {
return this.activeDisplaySets;
}
@@ -69,6 +73,12 @@ export default class DisplaySetService {
displaySet => displaySet.displaySetInstanceUID === displaySetInstanceUid
);
/**
*
* @param {*} input
* @param {*} param1
* @returns {string[]} - added displaySetInstanceUIDs
*/
makeDisplaySets = (input, { batch = false, madeInClient = false } = {}) => {
if (!input || !input.length) {
throw new Error('No instances were provided.');
@@ -84,11 +94,12 @@ export default class DisplaySetService {
let displaySetsAdded = [];
if (batch) {
input.forEach(instances => {
for (let i = 0; i < input.length; i++) {
const instances = input[i];
const displaySets = this.makeDisplaySetForInstances(instances);
displaySetsAdded = [...displaySetsAdded, displaySets];
});
}
} else {
const displaySets = this.makeDisplaySetForInstances(input);
@@ -109,6 +120,8 @@ export default class DisplaySetService {
displaySetsAdded,
options,
});
return displaySetsAdded;
}
};
@@ -16,7 +16,6 @@ const ViewportActionBar = ({
showNavArrows,
showPatientInfo: patientInfoVisibility,
onSeriesChange,
onHydrationClick,
onDoubleClick,
}) => {
const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility);
@@ -32,7 +31,6 @@ const ViewportActionBar = ({
label,
isTracked,
isLocked,
isHydrated,
modality,
studyDate,
currentSeries,
@@ -78,13 +76,7 @@ const ViewportActionBar = ({
Measurements cannot be duplicated.
</div>
)
: () => (
<div>
This SR is unlocked. <br />
You can duplicate measurements on your current report <br /> by
clicking &apos;Edit&apos;.
</div>
);
: () => <div>This SR is unlocked.</div>;
return (
<>
<Tooltip content={<TooltipMessage />} position="bottom-left">
@@ -101,16 +93,6 @@ const ViewportActionBar = ({
)}
</div>
</Tooltip>
{!isLocked && !isHydrated && (
<div className="relative flex p-1 ml-1 border rounded cursor-pointer border-primary-light">
<span
className="text-sm font-bold leading-none text-primary-light"
onClick={onHydrationClick}
>
Edit
</span>
</div>
)}
</>
);
}
+38 -38
View File
@@ -104,14 +104,15 @@ export default function ModeRoute({
// TODO: For some reason this is running before the Providers
// are calling setServiceImplementation
// TOOD -> iterate through services.
MeasurementService.clearMeasurements();
ViewportGridService.reset();
HangingProtocolService.reset();
// Extension
extensionManager.onModeEnter();
// Mode
route.init({ servicesManager, extensionManager });
return () => {
extensionManager.onModeExit();
};
}, [
mode,
dataSourceName,
@@ -131,13 +132,16 @@ export default function ModeRoute({
// DisplaySetService would wire this up?
const { unsubscribe } = DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
({ StudyInstanceUID, SeriesInstanceUID }) => {
({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) => {
const seriesMetadata = DicomMetadataStore.getSeries(
StudyInstanceUID,
SeriesInstanceUID
);
DisplaySetService.makeDisplaySets(seriesMetadata.instances);
DisplaySetService.makeDisplaySets(
seriesMetadata.instances,
madeInClient
);
}
);
@@ -161,9 +165,7 @@ export default function ModeRoute({
const { unsubscribe } = DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.SERIES_ADDED,
({ StudyInstanceUID }) => {
const studyMetadata = DicomMetadataStore.getStudy(
StudyInstanceUID,
);
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
const sortedSeries = studyMetadata.series.sort((a, b) => {
const aLowPriority = isLowPriorityModality(a.Modality);
@@ -176,13 +178,12 @@ export default function ModeRoute({
}
return a.SeriesNumber - b.SeriesNumber;
})
});
const { SeriesInstanceUID } = sortedSeries[0]
const { SeriesInstanceUID } = sortedSeries[0];
HangingProtocolService.setHangingProtocol(
{
/*protocolMatchingRules: [
HangingProtocolService.setHangingProtocol({
/*protocolMatchingRules: [
{
id: '7tmuq7KzDMCWFeapc',
weight: 2,
@@ -195,9 +196,9 @@ export default function ModeRoute({
},
},
],*/
stages: [
{
/*id: 'v5PfGt9F6mffZPif5',
stages: [
{
/*id: 'v5PfGt9F6mffZPif5',
viewportStructure: {
type: 'grid',
properties: {
@@ -206,30 +207,29 @@ export default function ModeRoute({
},
layoutTemplateName: 'gridLayout',
},*/
viewports: [
{
viewportSettings: {},
imageMatchingRules: [],
seriesMatchingRules: [
{
id: 'mXnsCcNzZL56z7mTZ',
weight: 1,
required: true,
attribute: 'SeriesInstanceUID',
constraint: {
equals: {
value: SeriesInstanceUID,
},
viewports: [
{
viewportSettings: {},
imageMatchingRules: [],
seriesMatchingRules: [
{
id: 'mXnsCcNzZL56z7mTZ',
weight: 1,
required: true,
attribute: 'SeriesInstanceUID',
constraint: {
equals: {
value: SeriesInstanceUID,
},
},
],
studyMatchingRules: [],
},
],
}
]
}
)
},
],
studyMatchingRules: [],
},
],
},
],
});
}
);
return unsubscribe;