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:
Bill Wallace 2023-04-25 17:25:10 -04:00 committed by GitHub
parent dc61d87238
commit 69d8e6a191
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 457 additions and 172 deletions

View File

@ -53,6 +53,7 @@ function _getDisplaySetsFromSeries(
segments: {}, segments: {},
sopClassUids, sopClassUids,
instance, instance,
instances: [instance],
wadoRoot, wadoRoot,
wadoUriRoot, wadoUriRoot,
wadoUri, wadoUri,

View File

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

View File

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

View File

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

View File

@ -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,11 +126,12 @@ function _getDisplaySetsFromSeries(
ConceptNameCodeSequence, ConceptNameCodeSequence,
SOPClassUID, SOPClassUID,
} = instance; } = instance;
validateSameStudyUID(instance.StudyInstanceUID, instances);
if ( if (
!ConceptNameCodeSequence || !ConceptNameCodeSequence ||
ConceptNameCodeSequence.CodeValue !== ConceptNameCodeSequence.CodeValue !==
CodeNameCodeSequenceValues.ImagingMeasurementReport CodeNameCodeSequenceValues.ImagingMeasurementReport
) { ) {
console.log( console.log(
'Only support Imaging Measurement Report SRs (TID1500) for this renderer.' 'Only support Imaging Measurement Report SRs (TID1500) for this renderer.'
@ -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);
@ -327,7 +367,7 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
trackingUniqueIdentifier => { trackingUniqueIdentifier => {
const mergedContentSequence = const mergedContentSequence =
mergedContentSequencesByTrackingUniqueIdentifiers[ mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier trackingUniqueIdentifier
]; ];
const measurement = _processMeasurement(mergedContentSequence); const measurement = _processMeasurement(mergedContentSequence);
@ -367,7 +407,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(
if ( if (
mergedContentSequencesByTrackingUniqueIdentifiers[ mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier trackingUniqueIdentifier
] === undefined ] === undefined
) { ) {
// Add the full ContentSequence // Add the full ContentSequence
@ -473,18 +513,18 @@ 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 &&
item.ConceptNameCodeSequence.CodeValue === item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.FindingSite CodeNameCodeSequenceValues.FindingSite
); );
const measurement = { const measurement = {
@ -496,28 +536,28 @@ 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
) && ) &&
FindingSite.ConceptCodeSequence.CodeValue === FindingSite.ConceptCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.CornerstoneFreeText CodeNameCodeSequenceValues.CornerstoneFreeText
); );
if (cornerstoneFreeTextFindingSite) { if (cornerstoneFreeTextFindingSite) {
@ -633,17 +673,16 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
_getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => { _getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
const { ReferencedSOPSequence } = item; const { ReferencedSOPSequence } = item;
if (!ReferencedSOPSequence) return;
for (const ref of _getSequenceAsArray(ReferencedSOPSequence)) {
if (ref.ReferencedSOPClassUID) {
const { ReferencedSOPClassUID, ReferencedSOPInstanceUID } = ref;
if (item.hasOwnProperty('ReferencedSOPClassUID')) { referencedImages.push({
const { ReferencedSOPClassUID,
ReferencedSOPClassUID, ReferencedSOPInstanceUID,
ReferencedSOPInstanceUID, });
} = ReferencedSOPSequence; }
referencedImages.push({
ReferencedSOPClassUID,
ReferencedSOPInstanceUID,
});
} }
}); });
@ -651,6 +690,7 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
} }
function _getSequenceAsArray(sequence) { function _getSequenceAsArray(sequence) {
if (!sequence) return [];
return Array.isArray(sequence) ? sequence : [sequence]; return Array.isArray(sequence) ? sequence : [sequence];
} }

View File

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

View File

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

View File

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

View File

@ -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.
// This is only for ease of redisplay - the display set is stored in the
// usual manner in the display set service.
displaySet.keyImageDisplaySet = createReferencedImageDisplaySet(
displaySetService,
displaySet
);
}
const { displaySetInstanceUID } = measurement; const referencedDisplaySet = displaySet.keyImageDisplaySet;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
const image0 = referencedDisplaySet.images[0]; const image0 = referencedDisplaySet.images[0];
const referencedDisplaySetMetadata = { const referencedDisplaySetMetadata = {

View File

@ -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",

View File

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

View File

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

View File

@ -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 {
@ -11,14 +16,14 @@ const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
class CornerstoneCacheService { class CornerstoneCacheService {
static REGISTRATION = { static REGISTRATION = {
name: 'cornerstoneCacheService', name: 'cornerstoneCacheService',
altName: 'CornerstoneCacheService', altName: 'CornerstoneCacheService',
create: ({ create: ({
servicesManager, servicesManager,
}: Types.Extensions.ExtensionParams): CornerstoneCacheService => { }: Types.Extensions.ExtensionParams): CornerstoneCacheService => {
return new CornerstoneCacheService(servicesManager); return new CornerstoneCacheService(servicesManager);
}, },
}; };
stackImageIds: Map<string, string[]> = new Map(); stackImageIds: Map<string, string[]> = new Map();
volumeImageIds: Map<string, string[]> = new Map(); volumeImageIds: Map<string, string[]> = new Map();
@ -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

View File

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

View File

@ -40,7 +40,7 @@ export default function findSRWithSameSeriesDescription(
SeriesTime, SeriesTime,
SeriesNumber, SeriesNumber,
Modality, Modality,
InstanceNumber: sameSeries.others.length + 1, InstanceNumber: sameSeries.instances.length + 1,
}; };
} }

View File

@ -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',

View File

@ -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',

View File

@ -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",

View File

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

View File

@ -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();
return false; displaySets.forEach(ds => {
const dsInstances = ds.instances;
if (!dsInstances) {
console.warn('No instances in', ds);
} else {
dsInstances.forEach(instance =>
dsInstancesSOP.add(instance.SOPInstanceUID)
);
}
});
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,34 +295,81 @@ export default class DisplaySetService extends PubSubService {
); );
if (displaySets.length) { if (displaySets.length) {
this._addActiveDisplaySets(displaySets); // This case occurs when there are already display sets, so remove
} else { // any instances in existing display sets.
displaySets = handler.getDisplaySetsFromSeries(instances); 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 (!displaySets || !displaySets.length) continue; if (!instances.length) {
// Everything is already added - this is just an update caused
// applying hp-defined viewport settings to the displaysets // by something else
displaySets.forEach(ds => { this._addActiveDisplaySets(displaySets);
Object.keys(settings).forEach(key => { return allDisplaySets;
ds[key] = settings[key]; }
});
});
this._addDisplaySetsToCache(displaySets);
this._addActiveDisplaySets(displaySets);
instances = instances.filter(
instance => !findInstance(instance, displaySets)
);
} }
allDisplaySets = allDisplaySets // The instances array still contains some instances, so try
? [...allDisplaySets, ...displaySets] // creating additional display sets using the sop class handler
: displaySets; displaySets = handler.getDisplaySetsFromSeries(instances);
if (!instances.length) return allDisplaySets; if (!displaySets || !displaySets.length) continue;
// applying hp-defined viewport settings to the displaysets
displaySets.forEach(ds => {
Object.keys(settings).forEach(key => {
ds[key] = settings[key];
});
});
this._addDisplaySetsToCache(displaySets);
this._addActiveDisplaySets(displaySets);
// It is possible that this SOP class handler handled some instances
// but there may need to be other instances handled by other handlers,
// so remove the handled instances
instances = filterInstances(instances, displaySets);
allDisplaySets.push(...displaySets);
} }
} }
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;
}
} }

View File

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

View File

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

View File

@ -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/');

View File

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