fix(SR): When loading DICOM SR, only one measurement is shown with no way to show others (#3228)
* fix: Make the cornerstone sR viewport show all measurements * PR fixes * PR fixes * Add a DICOM SR hanging protocol * Duplicate the hanging protocol for seg as well * PR requested change * PR requested changes * PR fixes plus merge update fixes * PR fixes and integration test fix * PR - documentation
This commit is contained in:
parent
dc61d87238
commit
69d8e6a191
@ -53,6 +53,7 @@ function _getDisplaySetsFromSeries(
|
|||||||
segments: {},
|
segments: {},
|
||||||
sopClassUids,
|
sopClassUids,
|
||||||
instance,
|
instance,
|
||||||
|
instances: [instance],
|
||||||
wadoRoot,
|
wadoRoot,
|
||||||
wadoUriRoot,
|
wadoUriRoot,
|
||||||
wadoUri,
|
wadoUri,
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { Types } from '@ohif/core';
|
import { Types } from '@ohif/core';
|
||||||
|
|
||||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
import getSopClassHandlerModule, { protocols } from './getSopClassHandlerModule';
|
||||||
import PanelSegmentation from './panels/PanelSegmentation';
|
import PanelSegmentation from './panels/PanelSegmentation';
|
||||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ const extension = {
|
|||||||
* iconName, iconLabel, label, component} object. Example of a panel module
|
* iconName, iconLabel, label, component} object. Example of a panel module
|
||||||
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
|
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
|
||||||
*/
|
*/
|
||||||
getPanelModule: ({ servicesManager, commandsManager, extensionManager }): Types.Panel[] => {
|
getPanelModule: ({ servicesManager, commandsManager, extensionManager }: Types.Extensions.ExtensionParams): Types.Panel[] => {
|
||||||
const wrappedPanelSegmentation = () => {
|
const wrappedPanelSegmentation = () => {
|
||||||
return (
|
return (
|
||||||
<PanelSegmentation
|
<PanelSegmentation
|
||||||
@ -58,6 +58,7 @@ const extension = {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
getViewportModule({ servicesManager, extensionManager }) {
|
getViewportModule({ servicesManager, extensionManager }) {
|
||||||
const ExtendedOHIFCornerstoneSEGViewport = props => {
|
const ExtendedOHIFCornerstoneSEGViewport = props => {
|
||||||
return (
|
return (
|
||||||
@ -85,3 +86,7 @@ const extension = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default extension;
|
export default extension;
|
||||||
|
|
||||||
|
// Export the protocols separately to allow for extending it at compile time
|
||||||
|
// in other modules
|
||||||
|
export { protocols };
|
||||||
|
|||||||
@ -46,7 +46,7 @@
|
|||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"@cornerstonejs/adapters": "^0.6.0",
|
"@cornerstonejs/adapters": "^0.6.0",
|
||||||
"@cornerstonejs/core": "^0.40.0",
|
"@cornerstonejs/core": "^0.42.2",
|
||||||
"@cornerstonejs/tools": "^0.60.1"
|
"@cornerstonejs/tools": "^0.61.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { metaData, utilities } from '@cornerstonejs/core';
|
import { metaData, utilities } from '@cornerstonejs/core';
|
||||||
|
|
||||||
import OHIF from '@ohif/core';
|
import OHIF, { DicomMetadataStore } from '@ohif/core';
|
||||||
import dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||||
|
|
||||||
@ -41,7 +41,6 @@ const _generateReport = (
|
|||||||
if (typeof dataset.SpecificCharacterSet === 'undefined') {
|
if (typeof dataset.SpecificCharacterSet === 'undefined') {
|
||||||
dataset.SpecificCharacterSet = 'ISO_IR 192';
|
dataset.SpecificCharacterSet = 'ISO_IR 192';
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataset;
|
return dataset;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -104,7 +103,18 @@ const commandsModule = ({}) => {
|
|||||||
additionalFindingTypes,
|
additionalFindingTypes,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
const { StudyInstanceUID } = naturalizedReport;
|
|
||||||
|
const { StudyInstanceUID, ContentSequence } = naturalizedReport;
|
||||||
|
// The content sequence has 5 or more elements, of which
|
||||||
|
// the `[4]` element contains the annotation data, so this is
|
||||||
|
// checking that there is some annotation data present.
|
||||||
|
if (!ContentSequence?.[4].ContentSequence?.length) {
|
||||||
|
console.log(
|
||||||
|
'naturalizedReport missing imaging content',
|
||||||
|
naturalizedReport
|
||||||
|
);
|
||||||
|
throw new Error('Invalid report, no content');
|
||||||
|
}
|
||||||
|
|
||||||
await dataSource.store.dicom(naturalizedReport);
|
await dataSource.store.dicom(naturalizedReport);
|
||||||
|
|
||||||
@ -112,6 +122,11 @@ const commandsModule = ({}) => {
|
|||||||
dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
|
dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The "Mode" route listens for DicomMetadataStore changes
|
||||||
|
// When a new instance is added, it listens and
|
||||||
|
// automatically calls makeDisplaySets
|
||||||
|
DicomMetadataStore.addInstances([naturalizedReport], true);
|
||||||
|
|
||||||
return naturalizedReport;
|
return naturalizedReport;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(error);
|
console.warn(error);
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
|
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
|
||||||
import { utils, classes } from '@ohif/core';
|
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
|
||||||
import addMeasurement from './utils/addMeasurement';
|
import addMeasurement from './utils/addMeasurement';
|
||||||
import isRehydratable from './utils/isRehydratable';
|
import isRehydratable from './utils/isRehydratable';
|
||||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||||
|
|
||||||
|
type InstanceMetadata = Types.InstanceMetadata;
|
||||||
|
|
||||||
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
|
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
|
||||||
|
|
||||||
const { ImageSet, MetadataProvider: metadataProvider } = classes;
|
const { ImageSet, MetadataProvider: metadataProvider } = classes;
|
||||||
|
|
||||||
// TODO ->
|
// TODO ->
|
||||||
// Add SR thumbnail
|
// Add SR thumbnail
|
||||||
// Make viewport
|
// Make viewport
|
||||||
@ -22,6 +25,17 @@ const sopClassUids = [
|
|||||||
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
|
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
|
||||||
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
||||||
|
|
||||||
|
const validateSameStudyUID = (uid: string, instances): void => {
|
||||||
|
instances.forEach(it => {
|
||||||
|
if (it.StudyInstanceUID !== uid) {
|
||||||
|
console.warn('Not all instances have the same UID', uid, it);
|
||||||
|
throw new Error(
|
||||||
|
`Instances ${it.SOPInstanceUID} does not belong to ${uid}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const CodeNameCodeSequenceValues = {
|
const CodeNameCodeSequenceValues = {
|
||||||
ImagingMeasurementReport: '126000',
|
ImagingMeasurementReport: '126000',
|
||||||
ImageLibrary: '111028',
|
ImageLibrary: '111028',
|
||||||
@ -50,15 +64,38 @@ const RELATIONSHIP_TYPE = {
|
|||||||
|
|
||||||
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
|
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds instances to the DICOM SR series, rather than creating a new
|
||||||
|
* series, so that as SR's are saved, they append to the series, and the
|
||||||
|
* key image display set gets updated as well, containing just the new series.
|
||||||
|
* @param instances is a list of instances from THIS series that are not
|
||||||
|
* in this DICOM SR Display Set already.
|
||||||
|
*/
|
||||||
|
function addInstances(
|
||||||
|
instances: InstanceMetadata[],
|
||||||
|
displaySetService: DisplaySetService
|
||||||
|
) {
|
||||||
|
this.instances.push(...instances);
|
||||||
|
utils.sortStudyInstances(this.instances);
|
||||||
|
// The last instance is the newest one, so is the one most interesting.
|
||||||
|
// Eventually, the SR viewer should have the ability to choose which SR
|
||||||
|
// gets loaded, and to navigate among them.
|
||||||
|
this.instance = this.instances[this.instances.length - 1];
|
||||||
|
this.isLoaded = false;
|
||||||
|
if (this.keyImageDisplaySet) {
|
||||||
|
this.load();
|
||||||
|
this.keyImageDisplaySet.updateInstances();
|
||||||
|
displaySetService.setDisplaySetMetadataInvalidated(
|
||||||
|
this.keyImageDisplaySet.displaySetInstanceUID
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DICOM SR SOP Class Handler
|
* DICOM SR SOP Class Handler
|
||||||
* For all referenced images in the TID 1500/300 sections, add an image to the
|
* For all referenced images in the TID 1500/300 sections, add an image to the
|
||||||
* display (this is TODO - it is not the actual behaviour below unfortunately)
|
* display.
|
||||||
*
|
|
||||||
* This will only display and rehydrate the latest DICOM SR in the given series
|
|
||||||
* It would be possible to add the ability to view older series rehydrations
|
|
||||||
* in the future.
|
|
||||||
*
|
|
||||||
* @param instances is a set of instances all from the same series
|
* @param instances is a set of instances all from the same series
|
||||||
* @param servicesManager is the services that can be used for creating
|
* @param servicesManager is the services that can be used for creating
|
||||||
* @returns The list of display sets created for the given instances object
|
* @returns The list of display sets created for the given instances object
|
||||||
@ -74,6 +111,9 @@ function _getDisplaySetsFromSeries(
|
|||||||
}
|
}
|
||||||
|
|
||||||
utils.sortStudyInstances(instances);
|
utils.sortStudyInstances(instances);
|
||||||
|
// The last instance is the newest one, so is the one most interesting.
|
||||||
|
// Eventually, the SR viewer should have the ability to choose which SR
|
||||||
|
// gets loaded, and to navigate among them.
|
||||||
const instance = instances[instances.length - 1];
|
const instance = instances[instances.length - 1];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -86,6 +126,7 @@ function _getDisplaySetsFromSeries(
|
|||||||
ConceptNameCodeSequence,
|
ConceptNameCodeSequence,
|
||||||
SOPClassUID,
|
SOPClassUID,
|
||||||
} = instance;
|
} = instance;
|
||||||
|
validateSameStudyUID(instance.StudyInstanceUID, instances);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!ConceptNameCodeSequence ||
|
!ConceptNameCodeSequence ||
|
||||||
@ -111,14 +152,13 @@ function _getDisplaySetsFromSeries(
|
|||||||
SOPClassHandlerId,
|
SOPClassHandlerId,
|
||||||
SOPClassUID,
|
SOPClassUID,
|
||||||
instances,
|
instances,
|
||||||
// Others is a historical value used for instances which is deprecated and will be removed
|
|
||||||
others: instances,
|
|
||||||
referencedImages: null,
|
referencedImages: null,
|
||||||
measurements: null,
|
measurements: null,
|
||||||
isDerivedDisplaySet: true,
|
isDerivedDisplaySet: true,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
sopClassUids,
|
sopClassUids,
|
||||||
instance,
|
instance,
|
||||||
|
addInstances,
|
||||||
};
|
};
|
||||||
|
|
||||||
displaySet.load = () => _load(displaySet, servicesManager, extensionManager);
|
displaySet.load = () => _load(displaySet, servicesManager, extensionManager);
|
||||||
@ -473,13 +513,13 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
|||||||
CodeNameCodeSequenceValues.TrackingIdentifier
|
CodeNameCodeSequenceValues.TrackingIdentifier
|
||||||
);
|
);
|
||||||
|
|
||||||
const Finding = mergedContentSequence.find(
|
const finding = mergedContentSequence.find(
|
||||||
item =>
|
item =>
|
||||||
item.ConceptNameCodeSequence.CodeValue ===
|
item.ConceptNameCodeSequence.CodeValue ===
|
||||||
CodeNameCodeSequenceValues.Finding
|
CodeNameCodeSequenceValues.Finding
|
||||||
);
|
);
|
||||||
|
|
||||||
const FindingSites = mergedContentSequence.filter(
|
const findingSites = mergedContentSequence.filter(
|
||||||
item =>
|
item =>
|
||||||
item.ConceptNameCodeSequence.CodingSchemeDesignator ===
|
item.ConceptNameCodeSequence.CodingSchemeDesignator ===
|
||||||
CodingSchemeDesignators.SRT &&
|
CodingSchemeDesignators.SRT &&
|
||||||
@ -496,22 +536,22 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Finding &&
|
finding &&
|
||||||
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
||||||
Finding.ConceptCodeSequence.CodingSchemeDesignator
|
finding.ConceptCodeSequence.CodingSchemeDesignator
|
||||||
) &&
|
) &&
|
||||||
Finding.ConceptCodeSequence.CodeValue ===
|
finding.ConceptCodeSequence.CodeValue ===
|
||||||
CodeNameCodeSequenceValues.CornerstoneFreeText
|
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||||
) {
|
) {
|
||||||
measurement.labels.push({
|
measurement.labels.push({
|
||||||
label: CORNERSTONE_FREETEXT_CODE_VALUE,
|
label: CORNERSTONE_FREETEXT_CODE_VALUE,
|
||||||
value: Finding.ConceptCodeSequence.CodeMeaning,
|
value: finding.ConceptCodeSequence.CodeMeaning,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO -> Eventually hopefully support SNOMED or some proper code library, just free text for now.
|
// TODO -> Eventually hopefully support SNOMED or some proper code library, just free text for now.
|
||||||
if (FindingSites.length) {
|
if (findingSites.length) {
|
||||||
const cornerstoneFreeTextFindingSite = FindingSites.find(
|
const cornerstoneFreeTextFindingSite = findingSites.find(
|
||||||
FindingSite =>
|
FindingSite =>
|
||||||
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
||||||
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
|
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
|
||||||
@ -633,24 +673,24 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
|
|||||||
|
|
||||||
_getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
|
_getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
|
||||||
const { ReferencedSOPSequence } = item;
|
const { ReferencedSOPSequence } = item;
|
||||||
|
if (!ReferencedSOPSequence) return;
|
||||||
if (item.hasOwnProperty('ReferencedSOPClassUID')) {
|
for (const ref of _getSequenceAsArray(ReferencedSOPSequence)) {
|
||||||
const {
|
if (ref.ReferencedSOPClassUID) {
|
||||||
ReferencedSOPClassUID,
|
const { ReferencedSOPClassUID, ReferencedSOPInstanceUID } = ref;
|
||||||
ReferencedSOPInstanceUID,
|
|
||||||
} = ReferencedSOPSequence;
|
|
||||||
|
|
||||||
referencedImages.push({
|
referencedImages.push({
|
||||||
ReferencedSOPClassUID,
|
ReferencedSOPClassUID,
|
||||||
ReferencedSOPInstanceUID,
|
ReferencedSOPInstanceUID,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return referencedImages;
|
return referencedImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getSequenceAsArray(sequence) {
|
function _getSequenceAsArray(sequence) {
|
||||||
|
if (!sequence) return [];
|
||||||
return Array.isArray(sequence) ? sequence : [sequence];
|
return Array.isArray(sequence) ? sequence : [sequence];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import preRegistration from './init';
|
|||||||
import { id } from './id.js';
|
import { id } from './id.js';
|
||||||
import toolNames from './tools/toolNames';
|
import toolNames from './tools/toolNames';
|
||||||
import hydrateStructuredReport from './utils/hydrateStructuredReport';
|
import hydrateStructuredReport from './utils/hydrateStructuredReport';
|
||||||
|
import createReferencedImageDisplaySet from './utils/createReferencedImageDisplaySet';
|
||||||
|
|
||||||
const Component = React.lazy(() => {
|
const Component = React.lazy(() => {
|
||||||
return import(
|
return import(
|
||||||
@ -57,8 +58,6 @@ const dicomSRExtension = {
|
|||||||
},
|
},
|
||||||
getCommandsModule,
|
getCommandsModule,
|
||||||
getSopClassHandlerModule,
|
getSopClassHandlerModule,
|
||||||
getHangingProtocolModule,
|
|
||||||
|
|
||||||
// Include dynmically computed values such as toolNames not known till instantiation
|
// Include dynmically computed values such as toolNames not known till instantiation
|
||||||
getUtilityModule({ servicesManager }) {
|
getUtilityModule({ servicesManager }) {
|
||||||
return [
|
return [
|
||||||
@ -75,4 +74,4 @@ const dicomSRExtension = {
|
|||||||
export default dicomSRExtension;
|
export default dicomSRExtension;
|
||||||
|
|
||||||
// Put static exports here so they can be type checked
|
// Put static exports here so they can be type checked
|
||||||
export { hydrateStructuredReport, srProtocol };
|
export { hydrateStructuredReport, createReferencedImageDisplaySet, srProtocol };
|
||||||
|
|||||||
@ -4,7 +4,7 @@ export default function onModeEnter({ servicesManager }) {
|
|||||||
const { displaySetService } = servicesManager.services;
|
const { displaySetService } = servicesManager.services;
|
||||||
const displaySetCache = displaySetService.getDisplaySetCache();
|
const displaySetCache = displaySetService.getDisplaySetCache();
|
||||||
|
|
||||||
const srDisplaySets = displaySetCache.filter(
|
const srDisplaySets = [...displaySetCache.values()].filter(
|
||||||
ds => ds.SOPClassHandlerId === SOPClassHandlerId
|
ds => ds.SOPClassHandlerId === SOPClassHandlerId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,89 @@
|
|||||||
|
import { DisplaySetService, classes } from '@ohif/core';
|
||||||
|
|
||||||
|
const ImageSet = classes.ImageSet;
|
||||||
|
|
||||||
|
const findInstance = (measurement, displaySetService: DisplaySetService) => {
|
||||||
|
const {
|
||||||
|
displaySetInstanceUID,
|
||||||
|
ReferencedSOPInstanceUID: sopUid,
|
||||||
|
} = measurement;
|
||||||
|
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||||
|
displaySetInstanceUID
|
||||||
|
);
|
||||||
|
if (!referencedDisplaySet.images) return;
|
||||||
|
return referencedDisplaySet.images.find(it => it.SOPInstanceUID === sopUid);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Finds references to display sets inside the measurements
|
||||||
|
* contained within the provided display set.
|
||||||
|
* @return an array of instances referenced.
|
||||||
|
*/
|
||||||
|
const findReferencedInstances = (
|
||||||
|
displaySetService: DisplaySetService,
|
||||||
|
displaySet
|
||||||
|
) => {
|
||||||
|
const instances = [];
|
||||||
|
const instanceById = {};
|
||||||
|
for (const measurement of displaySet.measurements) {
|
||||||
|
const { imageId } = measurement;
|
||||||
|
if (!imageId) continue;
|
||||||
|
if (instanceById[imageId]) continue;
|
||||||
|
|
||||||
|
const instance = findInstance(measurement, displaySetService);
|
||||||
|
if (!instance) {
|
||||||
|
console.log('Measurement', measurement, 'had no instances found');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceById[imageId] = instance;
|
||||||
|
instances.push(instance);
|
||||||
|
}
|
||||||
|
return instances;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new display set containing a single image instance for each
|
||||||
|
* referenced image.
|
||||||
|
*
|
||||||
|
* @param displaySetService
|
||||||
|
* @param displaySet - containing measurements referencing images.
|
||||||
|
* @returns A new (registered/active) display set containing the referenced images
|
||||||
|
*/
|
||||||
|
const createReferencedImageDisplaySet = (displaySetService, displaySet) => {
|
||||||
|
const instances = findReferencedInstances(displaySetService, displaySet);
|
||||||
|
// This will be a member function of the created image set
|
||||||
|
const updateInstances = function() {
|
||||||
|
this.images.splice(
|
||||||
|
0,
|
||||||
|
this.images.length,
|
||||||
|
...findReferencedInstances(displaySetService, displaySet)
|
||||||
|
);
|
||||||
|
this.numImageFrames = this.images.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const imageSet = new ImageSet(instances);
|
||||||
|
const instance = instances[0];
|
||||||
|
imageSet.setAttributes({
|
||||||
|
displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
|
||||||
|
SeriesDate: instance.SeriesDate,
|
||||||
|
SeriesTime: instance.SeriesTime,
|
||||||
|
SeriesInstanceUID: imageSet.uid,
|
||||||
|
StudyInstanceUID: instance.StudyInstanceUID,
|
||||||
|
SeriesNumber: instance.SeriesNumber || 0,
|
||||||
|
SOPClassUID: instance.SOPClassUID,
|
||||||
|
SeriesDescription: `${displaySet.SeriesDescription} KO ${displaySet.instance.SeriesNumber}`,
|
||||||
|
Modality: 'KO',
|
||||||
|
isMultiFrame: false,
|
||||||
|
numImageFrames: instances.length,
|
||||||
|
SOPClassHandlerId: `@ohif/extension-default.sopClassHandlerModule.stack`,
|
||||||
|
isReconstructable: false,
|
||||||
|
madeInClient: true,
|
||||||
|
updateInstances,
|
||||||
|
});
|
||||||
|
|
||||||
|
displaySetService.addDisplaySets(imageSet);
|
||||||
|
|
||||||
|
return imageSet;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createReferencedImageDisplaySet;
|
||||||
@ -15,6 +15,7 @@ import {
|
|||||||
ViewportActionBar,
|
ViewportActionBar,
|
||||||
} from '@ohif/ui';
|
} from '@ohif/ui';
|
||||||
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
|
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
|
||||||
|
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
|
||||||
|
|
||||||
const { formatDate } = utils;
|
const { formatDate } = utils;
|
||||||
|
|
||||||
@ -59,7 +60,6 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
referencedDisplaySetMetadata,
|
referencedDisplaySetMetadata,
|
||||||
setReferencedDisplaySetMetadata,
|
setReferencedDisplaySetMetadata,
|
||||||
] = useState(null);
|
] = useState(null);
|
||||||
const [isHydrated, setIsHydrated] = useState(srDisplaySet.isHydrated);
|
|
||||||
const [element, setElement] = useState(null);
|
const [element, setElement] = useState(null);
|
||||||
const { viewports, activeViewportIndex } = viewportGrid;
|
const { viewports, activeViewportIndex } = viewportGrid;
|
||||||
|
|
||||||
@ -89,9 +89,9 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
{ servicesManager, extensionManager },
|
{ servicesManager, extensionManager },
|
||||||
displaySetInstanceUID
|
displaySetInstanceUID
|
||||||
);
|
);
|
||||||
const displaySets = displaySetService.getDisplaySetsForSeries(
|
const displaySets = srDisplaySet.keyImageDisplaySet
|
||||||
SeriesInstanceUIDs[0]
|
? [srDisplaySet.keyImageDisplaySet]
|
||||||
);
|
: displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
|
||||||
if (displaySets.length) {
|
if (displaySets.length) {
|
||||||
viewportGridService.setDisplaySetsForViewports([
|
viewportGridService.setDisplaySetsForViewports([
|
||||||
{
|
{
|
||||||
@ -293,8 +293,6 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
if (!srDisplaySet.isLoaded) {
|
if (!srDisplaySet.isLoaded) {
|
||||||
srDisplaySet.load();
|
srDisplaySet.load();
|
||||||
}
|
}
|
||||||
setIsHydrated(srDisplaySet.isHydrated);
|
|
||||||
|
|
||||||
const numMeasurements = srDisplaySet.measurements.length;
|
const numMeasurements = srDisplaySet.measurements.length;
|
||||||
setMeasurementCount(numMeasurements);
|
setMeasurementCount(numMeasurements);
|
||||||
}, [srDisplaySet]);
|
}, [srDisplaySet]);
|
||||||
@ -382,6 +380,7 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
label: viewportLabel,
|
label: viewportLabel,
|
||||||
useAltStyling: true,
|
useAltStyling: true,
|
||||||
studyDate: formatDate(StudyDate),
|
studyDate: formatDate(StudyDate),
|
||||||
|
currentSeries: SeriesNumber,
|
||||||
seriesDescription: SeriesDescription || '',
|
seriesDescription: SeriesDescription || '',
|
||||||
patientInformation: {
|
patientInformation: {
|
||||||
patientName: PatientName
|
patientName: PatientName
|
||||||
@ -424,6 +423,7 @@ OHIFCornerstoneSRViewport.propTypes = {
|
|||||||
viewportIndex: PropTypes.number.isRequired,
|
viewportIndex: PropTypes.number.isRequired,
|
||||||
dataSource: PropTypes.object,
|
dataSource: PropTypes.object,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
|
viewportLabel: PropTypes.string,
|
||||||
customProps: PropTypes.object,
|
customProps: PropTypes.object,
|
||||||
viewportOptions: PropTypes.object,
|
viewportOptions: PropTypes.object,
|
||||||
viewportLabel: PropTypes.string,
|
viewportLabel: PropTypes.string,
|
||||||
@ -440,14 +440,18 @@ async function _getViewportReferencedDisplaySetData(
|
|||||||
measurementSelected,
|
measurementSelected,
|
||||||
displaySetService
|
displaySetService
|
||||||
) {
|
) {
|
||||||
const { measurements } = displaySet;
|
if (!displaySet.keyImageDisplaySet) {
|
||||||
const measurement = measurements[measurementSelected];
|
// Create a new display set, and preserve a reference to it here,
|
||||||
|
// so that it can be re-displayed and shown inside the SR viewport.
|
||||||
const { displaySetInstanceUID } = measurement;
|
// This is only for ease of redisplay - the display set is stored in the
|
||||||
|
// usual manner in the display set service.
|
||||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
displaySet.keyImageDisplaySet = createReferencedImageDisplaySet(
|
||||||
displaySetInstanceUID
|
displaySetService,
|
||||||
|
displaySet
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const referencedDisplaySet = displaySet.keyImageDisplaySet;
|
||||||
|
|
||||||
const image0 = referencedDisplaySet.images[0];
|
const image0 = referencedDisplaySet.images[0];
|
||||||
const referencedDisplaySetMetadata = {
|
const referencedDisplaySetMetadata = {
|
||||||
|
|||||||
@ -49,9 +49,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^0.6.0",
|
"@cornerstonejs/adapters": "^0.6.0",
|
||||||
"@cornerstonejs/core": "^0.40.0",
|
"@cornerstonejs/core": "^0.42.2",
|
||||||
"@cornerstonejs/streaming-image-volume-loader": "^0.16.0",
|
"@cornerstonejs/streaming-image-volume-loader": "^0.16.0",
|
||||||
"@cornerstonejs/tools": "^0.60.1",
|
"@cornerstonejs/tools": "^0.61.11",
|
||||||
"@kitware/vtk.js": "26.5.6",
|
"@kitware/vtk.js": "26.5.6",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
|
|||||||
@ -550,6 +550,7 @@ function _checkForCachedJumpToMeasurementEvents(
|
|||||||
const displaysUIDs = displaySets.map(
|
const displaysUIDs = displaySets.map(
|
||||||
displaySet => displaySet.displaySetInstanceUID
|
displaySet => displaySet.displaySetInstanceUID
|
||||||
);
|
);
|
||||||
|
if (!displaysUIDs?.length) return;
|
||||||
|
|
||||||
const measurementIdToJumpTo = measurementService.getJumpToMeasurement(
|
const measurementIdToJumpTo = measurementService.getJumpToMeasurement(
|
||||||
viewportIndex
|
viewportIndex
|
||||||
@ -561,7 +562,7 @@ function _checkForCachedJumpToMeasurementEvents(
|
|||||||
measurementIdToJumpTo
|
measurementIdToJumpTo
|
||||||
);
|
);
|
||||||
|
|
||||||
if (displaysUIDs.includes(measurement.displaySetInstanceUID)) {
|
if (displaysUIDs.includes(measurement?.displaySetInstanceUID)) {
|
||||||
_jumpToMeasurement(
|
_jumpToMeasurement(
|
||||||
measurement,
|
measurement,
|
||||||
elementRef,
|
elementRef,
|
||||||
|
|||||||
@ -93,7 +93,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
return (
|
return (
|
||||||
<OHIFCornerstoneViewport
|
<OHIFCornerstoneViewport
|
||||||
{...props}
|
{...props}
|
||||||
ToolbarService={toolbarService}
|
toolbarService={toolbarService}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { ServicesManager, Types } from '@ohif/core';
|
import { ServicesManager, Types } from '@ohif/core';
|
||||||
import { cache as cs3DCache, Enums, volumeLoader } from '@cornerstonejs/core';
|
import {
|
||||||
|
cache as cs3DCache,
|
||||||
|
Enums,
|
||||||
|
volumeLoader,
|
||||||
|
utilities as utils,
|
||||||
|
} from '@cornerstonejs/core';
|
||||||
|
|
||||||
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
||||||
import {
|
import {
|
||||||
@ -91,7 +96,10 @@ class CornerstoneCacheService {
|
|||||||
displaySetService
|
displaySetService
|
||||||
) {
|
) {
|
||||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||||
throw new Error('Invalidation of StackViewport is not supported yet');
|
return this._getCornerstoneStackImageIds(
|
||||||
|
displaySetService.getDisplaySetByUID(invalidatedDisplaySetInstanceUID),
|
||||||
|
dataSource
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo: grab the volume and get the id from the viewport itself
|
// Todo: grab the volume and get the id from the viewport itself
|
||||||
|
|||||||
@ -203,6 +203,10 @@ class ViewportInfo {
|
|||||||
orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation);
|
orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!toolGroupId) {
|
||||||
|
toolGroupId = DEFAULT_TOOLGROUP_ID;
|
||||||
|
}
|
||||||
|
|
||||||
this.setViewportOptions({
|
this.setViewportOptions({
|
||||||
...viewportOptionsEntry,
|
...viewportOptionsEntry,
|
||||||
viewportId: this.viewportId,
|
viewportId: this.viewportId,
|
||||||
@ -228,7 +232,8 @@ class ViewportInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getSyncGroups(): SyncGroup[] {
|
public getSyncGroups(): SyncGroup[] {
|
||||||
return this.viewportOptions.syncGroups || [];
|
this.viewportOptions.syncGroups ||= [];
|
||||||
|
return this.viewportOptions.syncGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDisplaySetOptions(): Array<DisplaySetOptions> {
|
public getDisplaySetOptions(): Array<DisplaySetOptions> {
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export default function findSRWithSameSeriesDescription(
|
|||||||
SeriesTime,
|
SeriesTime,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
Modality,
|
Modality,
|
||||||
InstanceNumber: sameSeries.others.length + 1,
|
InstanceNumber: sameSeries.instances.length + 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -50,7 +50,7 @@ const _getDisplaySetsFromSeries = (
|
|||||||
referencedImages: null,
|
referencedImages: null,
|
||||||
measurements: null,
|
measurements: null,
|
||||||
pdfUrl,
|
pdfUrl,
|
||||||
others: [instance],
|
instances: [instance],
|
||||||
thumbnailSrc: dataSource.retrieve.directURL({
|
thumbnailSrc: dataSource.retrieve.directURL({
|
||||||
instance,
|
instance,
|
||||||
defaultPath: '/thumbnail',
|
defaultPath: '/thumbnail',
|
||||||
|
|||||||
@ -70,7 +70,7 @@ const _getDisplaySetsFromSeries = (
|
|||||||
singlepart: 'video',
|
singlepart: 'video',
|
||||||
tag: 'PixelData',
|
tag: 'PixelData',
|
||||||
}),
|
}),
|
||||||
others: [instance],
|
instances: [instance],
|
||||||
thumbnailSrc: dataSource.retrieve.directURL({
|
thumbnailSrc: dataSource.retrieve.directURL({
|
||||||
instance,
|
instance,
|
||||||
defaultPath: '/thumbnail',
|
defaultPath: '/thumbnail',
|
||||||
|
|||||||
@ -32,8 +32,8 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^3.0.0",
|
"@ohif/core": "^3.0.0",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"@cornerstonejs/core": "^0.40.0",
|
"@cornerstonejs/core": "^0.42.2",
|
||||||
"@cornerstonejs/tools": "^0.60.1",
|
"@cornerstonejs/tools": "^0.61.11",
|
||||||
"@ohif/extension-cornerstone-dicom-sr": "^3.0.0",
|
"@ohif/extension-cornerstone-dicom-sr": "^3.0.0",
|
||||||
"dcmjs": "^0.29.5",
|
"dcmjs": "^0.29.5",
|
||||||
"lodash.debounce": "^4.17.21",
|
"lodash.debounce": "^4.17.21",
|
||||||
|
|||||||
@ -441,6 +441,7 @@ class MetadataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getUIDsFromImageID(imageId) {
|
getUIDsFromImageID(imageId) {
|
||||||
|
if (!imageId) throw new Error('MetadataProvider::Empty imageId');
|
||||||
// TODO: adding csiv here is not really correct. Probably need to use
|
// TODO: adding csiv here is not really correct. Probably need to use
|
||||||
// metadataProvider.addImageIdToUIDs(imageId, {
|
// metadataProvider.addImageIdToUIDs(imageId, {
|
||||||
// StudyInstanceUID,
|
// StudyInstanceUID,
|
||||||
|
|||||||
@ -1,31 +1,40 @@
|
|||||||
|
import { InstanceMetadata } from '../../types';
|
||||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||||
import EVENTS from './EVENTS';
|
import EVENTS from './EVENTS';
|
||||||
|
|
||||||
const displaySetCache = [];
|
export type DisplaySet = {
|
||||||
|
displaySetInstanceUID: string;
|
||||||
/**
|
instances: InstanceMetadata[];
|
||||||
* Find an instance in a list of instances, comparing by SOP instance UID
|
StudyInstanceUID: string;
|
||||||
*/
|
SeriesInstanceUID?: string;
|
||||||
const findInSet = (instance, list) => {
|
numImages?: number;
|
||||||
if (!list) return false;
|
|
||||||
for (const elem of list) {
|
|
||||||
if (!elem) continue;
|
|
||||||
if (elem === instance) return true;
|
|
||||||
if (elem.SOPInstanceUID === instance.SOPInstanceUID) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const displaySetCache = new Map<string, DisplaySet>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find an instance in a display set
|
* Filters the instances set by instances not in
|
||||||
* @returns true if found
|
* display sets. Done in O(n) time.
|
||||||
*/
|
*/
|
||||||
const findInstance = (instance, displaySets) => {
|
const filterInstances = (
|
||||||
for (const displayset of displaySets) {
|
instances: InstanceMetadata[],
|
||||||
if (findInSet(instance, displayset.images)) return true;
|
displaySets: DisplaySet[]
|
||||||
if (findInSet(instance, displayset.others)) return true;
|
): InstanceMetadata[] => {
|
||||||
|
const dsInstancesSOP = new Set();
|
||||||
|
displaySets.forEach(ds => {
|
||||||
|
const dsInstances = ds.instances;
|
||||||
|
if (!dsInstances) {
|
||||||
|
console.warn('No instances in', ds);
|
||||||
|
} else {
|
||||||
|
dsInstances.forEach(instance =>
|
||||||
|
dsInstancesSOP.add(instance.SOPInstanceUID)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return false;
|
});
|
||||||
|
|
||||||
|
return instances.filter(
|
||||||
|
instance => !dsInstancesSOP.has(instance.SOPInstanceUID)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class DisplaySetService extends PubSubService {
|
export default class DisplaySetService extends PubSubService {
|
||||||
@ -38,6 +47,13 @@ export default class DisplaySetService extends PubSubService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public activeDisplaySets = [];
|
public activeDisplaySets = [];
|
||||||
|
|
||||||
|
protected activeDisplaySetsMap = new Map<string, DisplaySet>();
|
||||||
|
|
||||||
|
// Record if the active display sets changed - used to group change events so
|
||||||
|
// that fewer events need to be fired when creating multiple display sets
|
||||||
|
protected activeDisplaySetsChanged = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(EVENTS);
|
super(EVENTS);
|
||||||
}
|
}
|
||||||
@ -46,64 +62,85 @@ export default class DisplaySetService extends PubSubService {
|
|||||||
this.extensionManager = extensionManager;
|
this.extensionManager = extensionManager;
|
||||||
this.SOPClassHandlerIds = SOPClassHandlerIds;
|
this.SOPClassHandlerIds = SOPClassHandlerIds;
|
||||||
this.activeDisplaySets = [];
|
this.activeDisplaySets = [];
|
||||||
|
this.activeDisplaySetsMap.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
_addDisplaySetsToCache(displaySets) {
|
_addDisplaySetsToCache(displaySets: DisplaySet[]) {
|
||||||
displaySets.forEach(displaySet => {
|
displaySets.forEach(displaySet => {
|
||||||
displaySetCache.push(displaySet);
|
displaySetCache.set(displaySet.displaySetInstanceUID, displaySet);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_addActiveDisplaySets(displaySets) {
|
_addActiveDisplaySets(displaySets: DisplaySet[]) {
|
||||||
const activeDisplaySets = this.activeDisplaySets;
|
const { activeDisplaySets, activeDisplaySetsMap } = this;
|
||||||
|
|
||||||
displaySets.forEach(displaySet => {
|
displaySets.forEach(displaySet => {
|
||||||
// This test makes adding display sets an N^2 operation, so it might
|
if (!activeDisplaySetsMap.has(displaySet.displaySetInstanceUID)) {
|
||||||
// become important to do this in an efficient manner for large
|
this.activeDisplaySetsChanged = true;
|
||||||
// numbers of display sets.
|
|
||||||
if (!activeDisplaySets.includes(displaySet)) {
|
|
||||||
activeDisplaySets.push(displaySet);
|
activeDisplaySets.push(displaySet);
|
||||||
|
activeDisplaySetsMap.set(displaySet.displayInstanceUID, displaySet);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getDisplaySetCache() {
|
/**
|
||||||
|
* Adds new display sets directly, as specified.
|
||||||
|
* Use this function when the display sets are created externally directly
|
||||||
|
* rather than using the default sop class handlers to create display sets.
|
||||||
|
*/
|
||||||
|
public addDisplaySets(...displaySets: DisplaySet[]): string[] {
|
||||||
|
this._addDisplaySetsToCache(displaySets);
|
||||||
|
this._addActiveDisplaySets(displaySets);
|
||||||
|
|
||||||
|
// The activeDisplaySetsChanged flag is only seen if we add display sets
|
||||||
|
// so, don't broadcast the change if all the display sets were pre-existing.
|
||||||
|
this.activeDisplaySetsChanged = false;
|
||||||
|
this._broadcastEvent(EVENTS.DISPLAY_SETS_ADDED, {
|
||||||
|
displaySetsAdded: displaySets,
|
||||||
|
options: { madeInClient: displaySets[0].madeInClient },
|
||||||
|
});
|
||||||
|
return displaySets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDisplaySetCache(): Map<string, DisplaySet> {
|
||||||
return displaySetCache;
|
return displaySetCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMostRecentDisplaySet() {
|
public getMostRecentDisplaySet(): DisplaySet {
|
||||||
return this.activeDisplaySets[this.activeDisplaySets.length - 1];
|
return this.activeDisplaySets[this.activeDisplaySets.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveDisplaySets() {
|
public getActiveDisplaySets(): DisplaySet[] {
|
||||||
return this.activeDisplaySets;
|
return this.activeDisplaySets;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDisplaySetsForSeries = SeriesInstanceUID => {
|
public getDisplaySetsForSeries = (
|
||||||
return displaySetCache.filter(
|
seriesInstanceUID: string
|
||||||
displaySet => displaySet.SeriesInstanceUID === SeriesInstanceUID
|
): DisplaySet[] => {
|
||||||
|
return [...displaySetCache.values()].filter(
|
||||||
|
displaySet => displaySet.SeriesInstanceUID === seriesInstanceUID
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
getDisplaySetForSOPInstanceUID(
|
public getDisplaySetForSOPInstanceUID(
|
||||||
SOPInstanceUID,
|
sopInstanceUID: string,
|
||||||
SeriesInstanceUID,
|
seriesInstanceUID: string,
|
||||||
frameNumber
|
frameNumber?: number
|
||||||
) {
|
): DisplaySet {
|
||||||
const displaySets = SeriesInstanceUID
|
const displaySets = seriesInstanceUID
|
||||||
? this.getDisplaySetsForSeries(SeriesInstanceUID)
|
? this.getDisplaySetsForSeries(seriesInstanceUID)
|
||||||
: this.getDisplaySetCache();
|
: [...this.getDisplaySetCache().values()];
|
||||||
|
|
||||||
const displaySet = displaySets.find(ds => {
|
const displaySet = displaySets.find(ds => {
|
||||||
return (
|
return (
|
||||||
ds.images && ds.images.some(i => i.SOPInstanceUID === SOPInstanceUID)
|
ds.images && ds.images.some(i => i.SOPInstanceUID === sopInstanceUID)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return displaySet;
|
return displaySet;
|
||||||
}
|
}
|
||||||
|
|
||||||
setDisplaySetMetadataInvalidated(displaySetInstanceUID) {
|
public setDisplaySetMetadataInvalidated(displaySetInstanceUID: string): void {
|
||||||
const displaySet = this.getDisplaySetByUID(displaySetInstanceUID);
|
const displaySet = this.getDisplaySetByUID(displaySetInstanceUID);
|
||||||
|
|
||||||
if (!displaySet) {
|
if (!displaySet) {
|
||||||
@ -117,19 +154,17 @@ export default class DisplaySetService extends PubSubService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteDisplaySet(displaySetInstanceUID) {
|
public deleteDisplaySet(displaySetInstanceUID) {
|
||||||
const { activeDisplaySets } = this;
|
if (!displaySetInstanceUID) return;
|
||||||
|
const { activeDisplaySets, activeDisplaySetsMap } = this;
|
||||||
const displaySetCacheIndex = displaySetCache.findIndex(
|
|
||||||
ds => ds.displaySetInstanceUID === displaySetInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
const activeDisplaySetsIndex = activeDisplaySets.findIndex(
|
const activeDisplaySetsIndex = activeDisplaySets.findIndex(
|
||||||
ds => ds.displaySetInstanceUID === displaySetInstanceUID
|
ds => ds.displaySetInstanceUID === displaySetInstanceUID
|
||||||
);
|
);
|
||||||
|
|
||||||
displaySetCache.splice(displaySetCacheIndex, 1);
|
displaySetCache.delete(displaySetInstanceUID);
|
||||||
activeDisplaySets.splice(activeDisplaySetsIndex, 1);
|
activeDisplaySets.splice(activeDisplaySetsIndex, 1);
|
||||||
|
activeDisplaySetsMap.delete(displaySetInstanceUID);
|
||||||
|
|
||||||
this._broadcastEvent(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets);
|
this._broadcastEvent(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets);
|
||||||
this._broadcastEvent(EVENTS.DISPLAY_SETS_REMOVED, {
|
this._broadcastEvent(EVENTS.DISPLAY_SETS_REMOVED, {
|
||||||
@ -141,10 +176,8 @@ export default class DisplaySetService extends PubSubService {
|
|||||||
* @param {string} displaySetInstanceUID
|
* @param {string} displaySetInstanceUID
|
||||||
* @returns {object} displaySet
|
* @returns {object} displaySet
|
||||||
*/
|
*/
|
||||||
getDisplaySetByUID = displaySetInstanceUid =>
|
public getDisplaySetByUID = (displaySetInstanceUid: string): DisplaySet =>
|
||||||
displaySetCache.find(
|
displaySetCache.get(displaySetInstanceUid);
|
||||||
displaySet => displaySet.displaySetInstanceUID === displaySetInstanceUid
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -167,7 +200,7 @@ export default class DisplaySetService extends PubSubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If array of instances => One instance.
|
// If array of instances => One instance.
|
||||||
let displaySetsAdded = [];
|
const displaySetsAdded = [];
|
||||||
|
|
||||||
if (batch) {
|
if (batch) {
|
||||||
for (let i = 0; i < input.length; i++) {
|
for (let i = 0; i < input.length; i++) {
|
||||||
@ -177,12 +210,12 @@ export default class DisplaySetService extends PubSubService {
|
|||||||
settings
|
settings
|
||||||
);
|
);
|
||||||
|
|
||||||
displaySetsAdded = [...displaySetsAdded, displaySets];
|
displaySetsAdded.push(...displaySets);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const displaySets = this.makeDisplaySetForInstances(input, settings);
|
const displaySets = this.makeDisplaySetForInstances(input, settings);
|
||||||
|
|
||||||
displaySetsAdded = displaySets;
|
displaySetsAdded.push(...displaySets);
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = {};
|
const options = {};
|
||||||
@ -191,10 +224,13 @@ export default class DisplaySetService extends PubSubService {
|
|||||||
options.madeInClient = true;
|
options.madeInClient = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This is tricky. How do we know we're not resetting to the same/existing DSs?
|
if (this.activeDisplaySetsChanged) {
|
||||||
// TODO: This is likely run anytime we touch DicomMetadataStore. How do we prevent unnecessary broadcasts?
|
this.activeDisplaySetsChanged = false;
|
||||||
if (displaySetsAdded && displaySetsAdded.length) {
|
|
||||||
this._broadcastEvent(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets);
|
this._broadcastEvent(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets);
|
||||||
|
}
|
||||||
|
if (displaySetsAdded?.length) {
|
||||||
|
// The response from displaySetsAdded will only contain newly added
|
||||||
|
// display sets.
|
||||||
this._broadcastEvent(EVENTS.DISPLAY_SETS_ADDED, {
|
this._broadcastEvent(EVENTS.DISPLAY_SETS_ADDED, {
|
||||||
displaySetsAdded,
|
displaySetsAdded,
|
||||||
options,
|
options,
|
||||||
@ -210,22 +246,45 @@ export default class DisplaySetService extends PubSubService {
|
|||||||
* the mode specific onModeExit is called before this method and should
|
* the mode specific onModeExit is called before this method and should
|
||||||
* store the active display sets and the cached data.
|
* store the active display sets and the cached data.
|
||||||
*/
|
*/
|
||||||
onModeExit() {
|
public onModeExit(): void {
|
||||||
this.getDisplaySetCache().length = 0;
|
this.getDisplaySetCache().clear();
|
||||||
this.activeDisplaySets.length = 0;
|
this.activeDisplaySets.length = 0;
|
||||||
|
this.activeDisplaySetsMap.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
makeDisplaySetForInstances(instancesSrc, settings) {
|
/**
|
||||||
let instances = instancesSrc;
|
* Creates new display sets for the instances contained in instancesSrc
|
||||||
|
* according to the sop class handlers registered.
|
||||||
|
* This is idempotent in that calling it a second time with the
|
||||||
|
* same set of instances will not result in new display sets added.
|
||||||
|
* However, the response for the subsequent call will be empty as the data
|
||||||
|
* is already present.
|
||||||
|
* Calling it with some new instances and some existing instances will
|
||||||
|
* result in the new instances being added to existing display sets if
|
||||||
|
* they support the addInstances call, OR to new instances otherwise.
|
||||||
|
* Only the new instances are returned - the others are updated.
|
||||||
|
*
|
||||||
|
* @param instancesSrc are instances to add
|
||||||
|
* @param settings are settings to add
|
||||||
|
* @returns Array of the display sets added.
|
||||||
|
*/
|
||||||
|
public makeDisplaySetForInstances(
|
||||||
|
instancesSrc: InstanceMetadata[],
|
||||||
|
settings
|
||||||
|
): DisplaySet[] {
|
||||||
|
// Some of the sop class handlers take a direct reference to instances
|
||||||
|
// so make sure it gets copied here so that they have their own ref
|
||||||
|
let instances = [...instancesSrc];
|
||||||
const instance = instances[0];
|
const instance = instances[0];
|
||||||
|
|
||||||
const existingDisplaySets =
|
const existingDisplaySets =
|
||||||
this.getDisplaySetsForSeries(instance.SeriesInstanceUID) || [];
|
this.getDisplaySetsForSeries(instance.SeriesInstanceUID) || [];
|
||||||
|
|
||||||
const SOPClassHandlerIds = this.SOPClassHandlerIds;
|
const SOPClassHandlerIds = this.SOPClassHandlerIds;
|
||||||
let allDisplaySets;
|
const allDisplaySets = [];
|
||||||
|
|
||||||
for (let i = 0; i < SOPClassHandlerIds.length; i++) {
|
// Iterate over the sop class handlers while there are still instances to add
|
||||||
|
for (let i = 0; i < SOPClassHandlerIds.length && instances.length; i++) {
|
||||||
const SOPClassHandlerId = SOPClassHandlerIds[i];
|
const SOPClassHandlerId = SOPClassHandlerIds[i];
|
||||||
const handler = this.extensionManager.getModuleEntry(SOPClassHandlerId);
|
const handler = this.extensionManager.getModuleEntry(SOPClassHandlerId);
|
||||||
|
|
||||||
@ -236,8 +295,36 @@ export default class DisplaySetService extends PubSubService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (displaySets.length) {
|
if (displaySets.length) {
|
||||||
|
// This case occurs when there are already display sets, so remove
|
||||||
|
// any instances in existing display sets.
|
||||||
|
instances = filterInstances(instances, displaySets);
|
||||||
|
// See if an existing display set can add this instance to it,
|
||||||
|
// for example, if it is a new image to be added to the existing set
|
||||||
|
for (const ds of displaySets) {
|
||||||
|
const addedDs = ds.addInstances?.(instances, this);
|
||||||
|
if (addedDs) {
|
||||||
|
this.activeDisplaySetsChanged = true;
|
||||||
|
instances = filterInstances(instances, [addedDs]);
|
||||||
|
this._addActiveDisplaySets([addedDs]);
|
||||||
|
this.setDisplaySetMetadataInvalidated(
|
||||||
|
addedDs.displaySetInstanceUID
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// This means that all instances already existed or got added to
|
||||||
|
// existing display sets, and had an invalidated event fired
|
||||||
|
if (!instances.length) return allDisplaySets;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!instances.length) {
|
||||||
|
// Everything is already added - this is just an update caused
|
||||||
|
// by something else
|
||||||
this._addActiveDisplaySets(displaySets);
|
this._addActiveDisplaySets(displaySets);
|
||||||
} else {
|
return allDisplaySets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The instances array still contains some instances, so try
|
||||||
|
// creating additional display sets using the sop class handler
|
||||||
displaySets = handler.getDisplaySetsFromSeries(instances);
|
displaySets = handler.getDisplaySetsFromSeries(instances);
|
||||||
|
|
||||||
if (!displaySets || !displaySets.length) continue;
|
if (!displaySets || !displaySets.length) continue;
|
||||||
@ -252,18 +339,37 @@ export default class DisplaySetService extends PubSubService {
|
|||||||
this._addDisplaySetsToCache(displaySets);
|
this._addDisplaySetsToCache(displaySets);
|
||||||
this._addActiveDisplaySets(displaySets);
|
this._addActiveDisplaySets(displaySets);
|
||||||
|
|
||||||
instances = instances.filter(
|
// It is possible that this SOP class handler handled some instances
|
||||||
instance => !findInstance(instance, displaySets)
|
// but there may need to be other instances handled by other handlers,
|
||||||
);
|
// so remove the handled instances
|
||||||
}
|
instances = filterInstances(instances, displaySets);
|
||||||
|
|
||||||
allDisplaySets = allDisplaySets
|
allDisplaySets.push(...displaySets);
|
||||||
? [...allDisplaySets, ...displaySets]
|
|
||||||
: displaySets;
|
|
||||||
|
|
||||||
if (!instances.length) return allDisplaySets;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return allDisplaySets;
|
return allDisplaySets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates over displaysets and invokes comparator for each element.
|
||||||
|
* It returns a list of items that has being succeed by comparator method.
|
||||||
|
*
|
||||||
|
* @param comparator - method to be used on the validation
|
||||||
|
* @returns list of displaysets
|
||||||
|
*/
|
||||||
|
public getDisplaySetsBy(comparator: (DisplaySet) => boolean): DisplaySet[] {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
if (typeof comparator !== 'function') {
|
||||||
|
throw new Error(`The comparator ${comparator} was not a function`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getActiveDisplaySets().forEach(displaySet => {
|
||||||
|
if (comparator(displaySet)) {
|
||||||
|
result.push(displaySet);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ const match = (
|
|||||||
// Allow for matching against current or prior specifically
|
// Allow for matching against current or prior specifically
|
||||||
const prior = options?.studies?.[1];
|
const prior = options?.studies?.[1];
|
||||||
const current = options?.studies?.[0];
|
const current = options?.studies?.[0];
|
||||||
const instance = (metadataInstance.images || metadataInstance.others)?.[0];
|
const instance = metadataInstance.instances?.[0];
|
||||||
const fromSrc = {
|
const fromSrc = {
|
||||||
prior,
|
prior,
|
||||||
current,
|
current,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import * as Extensions from '../extensions/ExtensionManager';
|
|||||||
import * as HangingProtocol from './HangingProtocol';
|
import * as HangingProtocol from './HangingProtocol';
|
||||||
import Services from './Services';
|
import Services from './Services';
|
||||||
import Hotkey from '../classes/Hotkey';
|
import Hotkey from '../classes/Hotkey';
|
||||||
|
import { DisplaySet } from '../services/DisplaySetService/DisplaySetService';
|
||||||
|
|
||||||
export * from '../services/CustomizationService/types';
|
export * from '../services/CustomizationService/types';
|
||||||
// Separate out some generic types
|
// Separate out some generic types
|
||||||
@ -16,4 +17,4 @@ export * from './IPubSub';
|
|||||||
* Export the types used within the various services and managers, but
|
* Export the types used within the various services and managers, but
|
||||||
* not the services/managers themselves, which are exported at the top level.
|
* not the services/managers themselves, which are exported at the top level.
|
||||||
*/
|
*/
|
||||||
export { Extensions, HangingProtocol, Services, Hotkey };
|
export { Extensions, HangingProtocol, Services, Hotkey, DisplaySet };
|
||||||
|
|||||||
@ -59,7 +59,9 @@ describe('OHIF Measurement Panel', function() {
|
|||||||
cy.get('@viewportInfoTopRight').should('contains.text', '(14/');
|
cy.get('@viewportInfoTopRight').should('contains.text', '(14/');
|
||||||
|
|
||||||
// Click on first measurement item
|
// Click on first measurement item
|
||||||
cy.get('[data-cy="measurement-item"]').click();
|
cy.get('[data-cy="measurement-item"]')
|
||||||
|
.eq(0)
|
||||||
|
.click();
|
||||||
|
|
||||||
cy.get('@viewportInfoTopRight').should('contains.text', '(1/');
|
cy.get('@viewportInfoTopRight').should('contains.text', '(1/');
|
||||||
cy.get('@viewportInfoTopRight').should('not.contains.text', '(14/');
|
cy.get('@viewportInfoTopRight').should('not.contains.text', '(14/');
|
||||||
|
|||||||
18
yarn.lock
18
yarn.lock
@ -1451,6 +1451,14 @@
|
|||||||
detect-gpu "^4.0.45"
|
detect-gpu "^4.0.45"
|
||||||
lodash.clonedeep "4.5.0"
|
lodash.clonedeep "4.5.0"
|
||||||
|
|
||||||
|
"@cornerstonejs/core@^0.42.2":
|
||||||
|
version "0.42.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-0.42.2.tgz#be69adbfd7b63718316a4f18fa55ca473506cee5"
|
||||||
|
integrity sha512-sLRqCOzR8cxBtQel9fltRti/rQRiYs1Wgx/CaYRPXlWID9Gj9os4j2PJHa70OiMIp4shMsF3jyVW7BPK0t6ZJQ==
|
||||||
|
dependencies:
|
||||||
|
detect-gpu "^4.0.45"
|
||||||
|
lodash.clonedeep "4.5.0"
|
||||||
|
|
||||||
"@cornerstonejs/streaming-image-volume-loader@^0.16.0":
|
"@cornerstonejs/streaming-image-volume-loader@^0.16.0":
|
||||||
version "0.16.0"
|
version "0.16.0"
|
||||||
resolved "https://registry.npmjs.org/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.16.0.tgz#513f868c285963fd2f2dbf54ed7840a2dc05e33a"
|
resolved "https://registry.npmjs.org/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.16.0.tgz#513f868c285963fd2f2dbf54ed7840a2dc05e33a"
|
||||||
@ -1459,12 +1467,12 @@
|
|||||||
"@cornerstonejs/core" "^0.40.0"
|
"@cornerstonejs/core" "^0.40.0"
|
||||||
cornerstone-wado-image-loader "^4.10.2"
|
cornerstone-wado-image-loader "^4.10.2"
|
||||||
|
|
||||||
"@cornerstonejs/tools@^0.60.1":
|
"@cornerstonejs/tools@^0.61.11":
|
||||||
version "0.60.1"
|
version "0.61.11"
|
||||||
resolved "https://registry.npmjs.org/@cornerstonejs/tools/-/tools-0.60.1.tgz#88cf32fe79c7fb714a99c1a63c25822233a4d231"
|
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-0.61.11.tgz#dacd85967cd6ab22c27dd44c10b14664863631f9"
|
||||||
integrity sha512-0GsxN8INh8/1/uKH4pU1ZT8XLNn4Xi2GJug9CQBT6plfnppru//+P+xl+LEQbYVpsydD1iYoWzLe8ykPuIeQCA==
|
integrity sha512-TCUde2gmuyiyd0EXhoT4DhDZBHYy82MOiekGQe+IG24um3a7GNGJ7jOKBk7fVFxJCTAI2Cb6XI8K79h6BhuWdg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@cornerstonejs/core" "^0.40.0"
|
"@cornerstonejs/core" "^0.42.2"
|
||||||
lodash.clonedeep "4.5.0"
|
lodash.clonedeep "4.5.0"
|
||||||
lodash.get "^4.4.2"
|
lodash.get "^4.4.2"
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user