From 0510b21634f5c5197565c2fd896098c430bb0650 Mon Sep 17 00:00:00 2001 From: Alireza Date: Thu, 18 Aug 2022 19:23:45 -0400 Subject: [PATCH 01/56] fix: product versoin for v32 (#2876) --- platform/viewer/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/viewer/package.json b/platform/viewer/package.json index 0e7d4bb27..6417365de 100644 --- a/platform/viewer/package.json +++ b/platform/viewer/package.json @@ -1,7 +1,7 @@ { "name": "@ohif/viewer", "version": "5.0.0", - "productVersion": "3.1.0", + "productVersion": "3.2.0", "description": "OHIF Viewer", "author": "OHIF Contributors", "license": "MIT", From 96986f992ac3802a3dc4a8e6be63e34bc918fd9c Mon Sep 17 00:00:00 2001 From: Nigel Date: Sat, 20 Aug 2022 08:42:20 +1000 Subject: [PATCH 02/56] bump: cornerstrone tools and core version to fix terser bug (#2881) --- extensions/cornerstone-dicom-sr/package.json | 4 ++-- extensions/cornerstone/package.json | 4 ++-- yarn.lock | 18 +++++++++++++----- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index 7bc459451..88bbe9f87 100644 --- a/extensions/cornerstone-dicom-sr/package.json +++ b/extensions/cornerstone-dicom-sr/package.json @@ -45,7 +45,7 @@ "dependencies": { "@babel/runtime": "7.16.3", "classnames": "^2.2.6", - "@cornerstonejs/core": "^0.14.4", - "@cornerstonejs/tools": "^0.21.7" + "@cornerstonejs/core": "^0.14.5", + "@cornerstonejs/tools": "^0.21.8" } } diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index 627ad3a44..848df6e81 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -43,9 +43,9 @@ }, "dependencies": { "@babel/runtime": "7.17.9", - "@cornerstonejs/core": "^0.14.4", + "@cornerstonejs/core": "^0.14.5", "@cornerstonejs/streaming-image-volume-loader": "^0.4.16", - "@cornerstonejs/tools": "^0.21.7", + "@cornerstonejs/tools": "^0.21.8", "@kitware/vtk.js": "^24.18.7", "dom-to-image": "^2.6.0", "lodash.debounce": "4.0.8", diff --git a/yarn.lock b/yarn.lock index 438fc5804..5794da345 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2306,6 +2306,14 @@ detect-gpu "^4.0.7" lodash.clonedeep "4.5.0" +"@cornerstonejs/core@^0.14.5": + version "0.14.5" + resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-0.14.5.tgz#a2a382593e84d702233ac4ee442a952348b6bc4e" + integrity sha512-i2btN00iqC9oa/cWSWCaIQQw9pBc9cNxYEI9sm+L7HB25MvOsBnq1qjUx8EV3UrcMNKdUVVBqRsNEjc2PAkzBA== + dependencies: + detect-gpu "^4.0.7" + lodash.clonedeep "4.5.0" + "@cornerstonejs/streaming-image-volume-loader@^0.4.16": version "0.4.16" resolved "https://registry.npmjs.org/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.4.16.tgz#5a26c7d07a19da3353b4e4cdfbf11eb001f5ab92" @@ -2314,12 +2322,12 @@ "@cornerstonejs/core" "^0.14.4" cornerstone-wado-image-loader "^4.2.1" -"@cornerstonejs/tools@^0.21.7": - version "0.21.7" - resolved "https://registry.npmjs.org/@cornerstonejs/tools/-/tools-0.21.7.tgz#9243a2f0b400cb869bc8c62e4377634a787aac6d" - integrity sha512-2alQr2JkaUiPyBi9aipeo/quKoo5Xwr/fABiO6ce9Ur0DQpp9WZ2jZWtC/xtC36QvrXIDAruEEq5WNV4ZASPJQ== +"@cornerstonejs/tools@^0.21.8": + version "0.21.8" + resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-0.21.8.tgz#fbe4d20a2f28b05a1516c3e925c43520988a2dcb" + integrity sha512-S4CkC8AHACfnQKIL43Vj35xiKiGKT8oRrRj+AT9kMeR7PdgbOXezZoYRBRSddj/vZTaOs9pV/hzmVDWw5ThOHg== dependencies: - "@cornerstonejs/core" "^0.14.4" + "@cornerstonejs/core" "^0.14.5" lodash.clonedeep "4.5.0" lodash.get "^4.4.2" From df6ddf15726fabbf9bb379bfdef52fdabcd2cf9a Mon Sep 17 00:00:00 2001 From: Salim Kanoun Date: Sat, 20 Aug 2022 00:43:04 +0200 Subject: [PATCH 03/56] fix: bone and brain preset in tmtv (#2877) --- modes/basic-dev-mode/src/toolbarButtons.js | 4 ++-- modes/tmtv/src/toolbarButtons.js | 4 ++-- platform/core/src/defaults/windowLevelPresets.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modes/basic-dev-mode/src/toolbarButtons.js b/modes/basic-dev-mode/src/toolbarButtons.js index 5731fa075..ff1b652d0 100644 --- a/modes/basic-dev-mode/src/toolbarButtons.js +++ b/modes/basic-dev-mode/src/toolbarButtons.js @@ -185,8 +185,8 @@ const toolbarButtons = [ _createWwwcPreset(1, 'Soft tissue', '400 / 40'), _createWwwcPreset(2, 'Lung', '1500 / -600'), _createWwwcPreset(3, 'Liver', '150 / 90'), - _createWwwcPreset(4, 'Bone', '80 / 40'), - _createWwwcPreset(5, 'Brain', '2500 / 480'), + _createWwwcPreset(4, 'Bone', '2500 / 480'), + _createWwwcPreset(5, 'Brain', '80 / 40'), ], }, }, diff --git a/modes/tmtv/src/toolbarButtons.js b/modes/tmtv/src/toolbarButtons.js index 7342887d7..bb7d4ff5c 100644 --- a/modes/tmtv/src/toolbarButtons.js +++ b/modes/tmtv/src/toolbarButtons.js @@ -219,8 +219,8 @@ const toolbarButtons = [ _createWwwcPreset(1, 'Soft tissue', '400 / 40'), _createWwwcPreset(2, 'Lung', '1500 / -600'), _createWwwcPreset(3, 'Liver', '150 / 90'), - _createWwwcPreset(4, 'Bone', '80 / 40'), - _createWwwcPreset(5, 'Brain', '2500 / 480'), + _createWwwcPreset(4, 'Bone', '2500 / 480'), + _createWwwcPreset(5, 'Brain', '80 / 40'), ], }, }, diff --git a/platform/core/src/defaults/windowLevelPresets.js b/platform/core/src/defaults/windowLevelPresets.js index 258b3d18a..ca6a1501a 100644 --- a/platform/core/src/defaults/windowLevelPresets.js +++ b/platform/core/src/defaults/windowLevelPresets.js @@ -2,8 +2,8 @@ export default { 1: { description: 'Soft tissue', window: '400', level: '40' }, 2: { description: 'Lung', window: '1500', level: '-600' }, 3: { description: 'Liver', window: '150', level: '90' }, - 4: { description: 'Bone', window: '80', level: '40' }, - 5: { description: 'Brain', window: '2500', level: '480' }, + 4: { description: 'Bone', window: '2500', level: '480' }, + 5: { description: 'Brain', window: '80', level: '40' }, 6: { description: 'Trest', window: '1', level: '1' }, 7: { description: 'Empty1', window: 'Empty1', level: 'Empty1' }, 8: { description: 'Empty2', window: 'Empty2', level: 'Empty2' }, From 2639d90e5e6996a3c142daedd5748164c23168e3 Mon Sep 17 00:00:00 2001 From: Alireza Date: Fri, 19 Aug 2022 19:35:29 -0400 Subject: [PATCH 04/56] fix: hanging protocol for the tmtv (#2882) --- extensions/tmtv/src/Panels/PanelPetSUV.tsx | 11 +++++++---- extensions/tmtv/src/getHangingProtocolModule.js | 13 +------------ modes/tmtv/src/index.js | 2 +- .../HangingProtocolService/ProtocolEngine.js | 7 +++++-- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/extensions/tmtv/src/Panels/PanelPetSUV.tsx b/extensions/tmtv/src/Panels/PanelPetSUV.tsx index 9f895cce5..3e6f102fe 100644 --- a/extensions/tmtv/src/Panels/PanelPetSUV.tsx +++ b/extensions/tmtv/src/Panels/PanelPetSUV.tsx @@ -24,8 +24,11 @@ const DEFAULT_MEATADATA = { */ export default function PanelPetSUV({ servicesManager, commandsManager }) { const { t } = useTranslation('PanelSUV'); - const { DisplaySetService, ToolGroupService, ToolBarService } = - servicesManager.services; + const { + DisplaySetService, + ToolGroupService, + ToolBarService, + } = servicesManager.services; const [metadata, setMetadata] = useState(DEFAULT_MEATADATA); const [ptDisplaySet, setPtDisplaySet] = useState(null); @@ -210,7 +213,7 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) { }} /> protocol.id === 'default'), + protocol: + this.protocols.find(protocol => protocol.id === 'default') ?? + this.protocols[0], }, ]; } From 7c0756824e8bbd5f7d4bf7d8f4e748481504ac80 Mon Sep 17 00:00:00 2001 From: Eric Taix Date: Mon, 5 Sep 2022 16:32:03 +0200 Subject: [PATCH 05/56] fix: modules name to reflect typescript usage (#2909) * Prevent IntelliJ config file from being pushed to the repository * Rename module's name to reflect the created file for a new mode * Rename the module's name to reflect the filed created for a new extension which uses now typescript --- .gitignore | 1 + platform/cli/templates/extension/dependencies.json | 2 +- platform/cli/templates/mode/dependencies.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 45d0fb3c8..4a9d08278 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ screenshots/ # autogenerated files platform/viewer/src/pluginImports.js +/Viewers.iml diff --git a/platform/cli/templates/extension/dependencies.json b/platform/cli/templates/extension/dependencies.json index e79d4c4ce..02970822d 100644 --- a/platform/cli/templates/extension/dependencies.json +++ b/platform/cli/templates/extension/dependencies.json @@ -1,7 +1,7 @@ { "repository": "OHIF/Viewers", "keywords": ["ohif-extension"], - "module": "src/index.js", + "module": "src/index.tsx", "engines": { "node": ">=14", "npm": ">=6", diff --git a/platform/cli/templates/mode/dependencies.json b/platform/cli/templates/mode/dependencies.json index 9e16162cc..55dc20621 100644 --- a/platform/cli/templates/mode/dependencies.json +++ b/platform/cli/templates/mode/dependencies.json @@ -1,7 +1,7 @@ { "repository": "OHIF/Viewers", "keywords": ["ohif-mode"], - "module": "src/index.js", + "module": "src/index.tsx", "engines": { "node": ">=14", "npm": ">=6", From e1d366e1e176d52dbaf3bb7d03fe4ea829bfe2ac Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Wed, 7 Sep 2022 11:59:11 -0400 Subject: [PATCH 06/56] feat: make hanging protocol work on displaySets instead of series (#2837) * feat(HP): Apply HP to display sets, fix race condition * fix: displaySetService no event not needed (#2912) * fix various styles and renamings * feat: refactored hp service * fix: use HP service event for viewport grid * remove unnecessary doc * fix test * apply review comment * fix: segmentation creation Co-authored-by: Alireza --- .../CornerstoneViewportService.ts | 32 +- .../src/services/ViewportService/Viewport.ts | 2 - .../src/utils/interleaveCenterLoader.ts | 6 +- .../src/utils/interleaveTopToBottom.ts | 2 +- .../default/src/DicomWebDataSource/index.js | 13 +- .../default/src/getHangingProtocolModule.js | 2 + extensions/tmtv/src/Panels/PanelPetSUV.tsx | 57 +- extensions/tmtv/src/commandsModule.js | 39 +- jest.config.base.js | 4 - modes/tmtv/src/index.js | 8 +- .../DicomMetadataStore/DicomMetadataStore.js | 7 + .../DicomMetadataStore/StudyMetadata.ts | 7 + .../DicomMetadataStore/createStudyMetadata.js | 12 + .../services/DisplaySetService/IDisplaySet.ts | 7 + .../HangingProtocolService/HPMatcher.js | 47 +- .../HangingProtocolService.test.js | 166 +++++ ...olService.js => HangingProtocolService.ts} | 367 +++++++--- .../HangingProtocolService/ProtocolEngine.js | 634 +----------------- .../HangingProtocolService/lib/validator.js | 73 +- .../lib/validator.test.js | 49 ++ .../services/data/DisplaySetService.md | 36 +- .../services/data/DisplaySetService.md | 5 +- .../services/data/HangingProtocolService.md | 182 +++-- .../version-3.0/platform/services/pubsub.md | 2 +- .../viewer/src/components/ViewportGrid.tsx | 47 +- platform/viewer/src/routes/Mode/Mode.tsx | 66 +- 26 files changed, 893 insertions(+), 979 deletions(-) create mode 100644 platform/core/src/services/DicomMetadataStore/StudyMetadata.ts create mode 100644 platform/core/src/services/DisplaySetService/IDisplaySet.ts create mode 100644 platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js rename platform/core/src/services/HangingProtocolService/{HangingProtocolService.js => HangingProtocolService.ts} (58%) create mode 100644 platform/core/src/services/HangingProtocolService/lib/validator.test.js diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index 89ee22f4e..85c6a4a55 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -39,6 +39,8 @@ class CornerstoneViewportService implements IViewportService { renderingEngine: Types.IRenderingEngine | null; viewportsInfo: Map; viewportGridResizeObserver: ResizeObserver | null; + // TODO - get the right type here. + hangingProtocolService: object; /** * Service-specific @@ -59,7 +61,7 @@ class CornerstoneViewportService implements IViewportService { this.listeners = {}; this.EVENTS = EVENTS; const { HangingProtocolService } = servicesManager.services; - this.HangingProtocolService = HangingProtocolService; + this.hangingProtocolService = HangingProtocolService; Object.assign(this, pubSubServiceInterface); // } @@ -166,12 +168,14 @@ class CornerstoneViewportService implements IViewportService { const viewportInfo = this.viewportsInfo.get(viewportIndex); viewportInfo.setRenderingEngineId(renderingEngine.id); - const { viewportOptions, displaySetOptions } = - this._getViewportAndDisplaySetOptions( - publicViewportOptions, - publicDisplaySetOptions, - viewportInfo - ); + const { + viewportOptions, + displaySetOptions, + } = this._getViewportAndDisplaySetOptions( + publicViewportOptions, + publicDisplaySetOptions, + viewportInfo + ); viewportInfo.setViewportOptions(viewportOptions); viewportInfo.setDisplaySetOptions(displaySetOptions); @@ -265,7 +269,6 @@ class CornerstoneViewportService implements IViewportService { viewportInfo: ViewportInfo ) { const displaySetOptions = viewportInfo.getDisplaySetOptions(); - const { imageIds, initialImageIndex } = viewportData; let initialImageIndexToUse = initialImageIndex; @@ -352,7 +355,7 @@ class CornerstoneViewportService implements IViewportService { // (This call may or may not create sub-requests for series metadata) const volumeInputArray = []; const displaySetOptionsArray = viewportInfo.getDisplaySetOptions(); - const { HangingProtocolService } = this; + const { hangingProtocolService } = this; for (let i = 0; i < viewportData.imageIds.length; i++) { const imageIds = viewportData.imageIds[i]; @@ -384,11 +387,11 @@ class CornerstoneViewportService implements IViewportService { } if ( - HangingProtocolService.hasCustomImageLoadStrategy() && - !HangingProtocolService.customImageLoadPerformed + hangingProtocolService.hasCustomImageLoadStrategy() && + !hangingProtocolService.customImageLoadPerformed ) { // delegate the volume loading to the hanging protocol service if it has a custom image load strategy - return HangingProtocolService.runImageLoadStrategy({ + return hangingProtocolService.runImageLoadStrategy({ viewportId: viewport.id, volumeInputArray, }); @@ -415,8 +418,9 @@ class CornerstoneViewportService implements IViewportService { ) { const { index, preset } = initialImageOptions; - const { numberOfSlices } = - csUtils.getImageSliceDataForVolumeViewport(viewport); + const { numberOfSlices } = csUtils.getImageSliceDataForVolumeViewport( + viewport + ); const imageIndex = this._getInitialImageIndex( numberOfSlices, diff --git a/extensions/cornerstone/src/services/ViewportService/Viewport.ts b/extensions/cornerstone/src/services/ViewportService/Viewport.ts index e6e30c83d..ecc1a21bb 100644 --- a/extensions/cornerstone/src/services/ViewportService/Viewport.ts +++ b/extensions/cornerstone/src/services/ViewportService/Viewport.ts @@ -16,7 +16,6 @@ export type ViewportOptions = { viewportId: string; orientation?: Types.Orientation; background?: Types.Point3; - initialView?: string; syncGroups?: SyncGroup[]; initialImageOptions?: InitialImageOptions; customViewportProps?: Record; @@ -28,7 +27,6 @@ export type PublicViewportOptions = { viewportId?: string; orientation?: string; background?: Types.Point3; - initialView?: string; syncGroups?: SyncGroup[]; initialImageOptions?: InitialImageOptions; customViewportProps?: Record; diff --git a/extensions/cornerstone/src/utils/interleaveCenterLoader.ts b/extensions/cornerstone/src/utils/interleaveCenterLoader.ts index dcf19ffd7..d0ea08fbc 100644 --- a/extensions/cornerstone/src/utils/interleaveCenterLoader.ts +++ b/extensions/cornerstone/src/utils/interleaveCenterLoader.ts @@ -8,8 +8,8 @@ const viewportIdVolumeInputArrayMap = new Map(); /** * This function caches the volumeUIDs until all the volumes inside the - * hangging protocol are initialized. Then it goes through the imageIds - * of the volumes, and interleav them, in order for the volumes to be loaded + * hanging protocol are initialized. Then it goes through the imageIds + * of the volumes, and interleave them, in order for the volumes to be loaded * together from middle to the start and the end. * @param {Object} props image loading properties from Cornerstone ViewportService * @returns @@ -17,7 +17,7 @@ const viewportIdVolumeInputArrayMap = new Map(); export default function interleaveCenterLoader({ data: { viewportId, volumeInputArray }, displaySetsMatchDetails, - matchDetails, + viewportMatchDetails: matchDetails, }) { viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray); diff --git a/extensions/cornerstone/src/utils/interleaveTopToBottom.ts b/extensions/cornerstone/src/utils/interleaveTopToBottom.ts index 2c7df60bc..1f1bed3f0 100644 --- a/extensions/cornerstone/src/utils/interleaveTopToBottom.ts +++ b/extensions/cornerstone/src/utils/interleaveTopToBottom.ts @@ -16,7 +16,7 @@ const viewportIdVolumeInputArrayMap = new Map(); export default function interleaveTopToBottom({ data: { viewportId, volumeInputArray }, displaySetsMatchDetails, - matchDetails, + viewportMatchDetails: matchDetails, }) { viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray); diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index cecd1c8cc..98e9a19a6 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -473,12 +473,13 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) { DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient); - const numberOfSeries = seriesPromises.length; - seriesPromises.forEach(async (seriesPromise, index) => { - const instances = await seriesPromise; - storeInstances(instances); - if (index === numberOfSeries - 1) setSuccessFlag(); - }); + const seriesDeliveredPromises = seriesPromises.map(promise => + promise.then(instances => { + storeInstances(instances); + }) + ); + await Promise.all(seriesDeliveredPromises); + setSuccessFlag(); }, deleteStudyMetadataPromise, getImageIdsForDisplaySet(displaySet) { diff --git a/extensions/default/src/getHangingProtocolModule.js b/extensions/default/src/getHangingProtocolModule.js index c85fac71a..0d3d6b1a8 100644 --- a/extensions/default/src/getHangingProtocolModule.js +++ b/extensions/default/src/getHangingProtocolModule.js @@ -23,7 +23,9 @@ const defaultProtocol = { displaySets: [ { id: 'displaySet', + // Unused currently imageMatchingRules: [], + // Matches displaysets, NOT series seriesMatchingRules: [], studyMatchingRules: [], }, diff --git a/extensions/tmtv/src/Panels/PanelPetSUV.tsx b/extensions/tmtv/src/Panels/PanelPetSUV.tsx index 3e6f102fe..0666f90a9 100644 --- a/extensions/tmtv/src/Panels/PanelPetSUV.tsx +++ b/extensions/tmtv/src/Panels/PanelPetSUV.tsx @@ -28,32 +28,32 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) { DisplaySetService, ToolGroupService, ToolBarService, + HangingProtocolService, } = servicesManager.services; const [metadata, setMetadata] = useState(DEFAULT_MEATADATA); const [ptDisplaySet, setPtDisplaySet] = useState(null); - const handleMetadataChange = useCallback( - metadata => { - setMetadata(prevState => { - const newState = { ...prevState }; - Object.keys(metadata).forEach(key => { - if (typeof metadata[key] === 'object') { - newState[key] = { - ...prevState[key], - ...metadata[key], - }; - } else { - newState[key] = metadata[key]; - } - }); - return newState; + const handleMetadataChange = metadata => { + setMetadata(prevState => { + const newState = { ...prevState }; + Object.keys(metadata).forEach(key => { + if (typeof metadata[key] === 'object') { + newState[key] = { + ...prevState[key], + ...metadata[key], + }; + } else { + newState[key] = metadata[key]; + } }); - }, - [metadata] - ); + return newState; + }); + }; - const getMatchingPTDisplaySet = useCallback(() => { - const ptDisplaySet = commandsManager.runCommand('getMatchingPTDisplaySet'); + const getMatchingPTDisplaySet = viewportMatchDetails => { + const ptDisplaySet = commandsManager.runCommand('getMatchingPTDisplaySet', { + viewportMatchDetails, + }); if (!ptDisplaySet) { return; @@ -67,16 +67,16 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) { ptDisplaySet, metadata, }; - }, []); + }; useEffect(() => { - const displaySets = DisplaySetService.activeDisplaySets; - + const displaySets = DisplaySetService.getActiveDisplaySets(); + const { viewportMatchDetails } = HangingProtocolService.getMatchDetails(); if (!displaySets.length) { return; } - const displaySetInfo = getMatchingPTDisplaySet(); + const displaySetInfo = getMatchingPTDisplaySet(viewportMatchDetails); if (!displaySetInfo) { return; @@ -89,15 +89,14 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) { // get the patientMetadata from the StudyInstanceUIDs and update the state useEffect(() => { - const { unsubscribe } = DisplaySetService.subscribe( - DisplaySetService.EVENTS.DISPLAY_SETS_ADDED, - () => { - const displaySetInfo = getMatchingPTDisplaySet(); + const { unsubscribe } = HangingProtocolService.subscribe( + HangingProtocolService.EVENTS.PROTOCOL_CHANGED, + ({ viewportMatchDetails }) => { + const displaySetInfo = getMatchingPTDisplaySet(viewportMatchDetails); if (!displaySetInfo) { return; } - const { ptDisplaySet, metadata } = displaySetInfo; setPtDisplaySet(ptDisplaySet); setMetadata(metadata); diff --git a/extensions/tmtv/src/commandsModule.js b/extensions/tmtv/src/commandsModule.js index e2633cca8..28abe8709 100644 --- a/extensions/tmtv/src/commandsModule.js +++ b/extensions/tmtv/src/commandsModule.js @@ -41,9 +41,9 @@ const commandsModule = ({ } function _getMatchedViewportsToolGroupIds() { - const [matchedViewports] = HangingProtocolService.getState(); + const { viewportMatchDetails } = HangingProtocolService.getMatchDetails(); const toolGroupIds = []; - matchedViewports.forEach(({ viewportOptions }) => { + viewportMatchDetails.forEach(({ viewportOptions }) => { const { toolGroupId } = viewportOptions; if (toolGroupIds.indexOf(toolGroupId) === -1) { toolGroupIds.push(toolGroupId); @@ -54,33 +54,30 @@ const commandsModule = ({ } const actions = { - getMatchingPTDisplaySet: () => { + getMatchingPTDisplaySet: ({ viewportMatchDetails }) => { // Todo: this is assuming that the hanging protocol has successfully matched // the correct PT. For future, we should have a way to filter out the PTs // that are in the viewer layout (but then we have the problem of the attenuation // corrected PT vs the non-attenuation correct PT) - const matches = HangingProtocolService.getDisplaySetsMatchDetails(); - - const matchedSeriesInstanceUIDs = Array.from(matches.values()).map( - ({ SeriesInstanceUID }) => SeriesInstanceUID - ); let ptDisplaySet = null; - for (const SeriesInstanceUID of matchedSeriesInstanceUIDs) { - const displaySets = DisplaySetService.getDisplaySetsForSeries( - SeriesInstanceUID + for (const matched of viewportMatchDetails) { + const { displaySetsInfo } = matched; + const displaySets = displaySetsInfo.map(({ displaySetInstanceUID }) => + DisplaySetService.getDisplaySetByUID(displaySetInstanceUID) ); if (!displaySets || displaySets.length === 0) { continue; } - const displaySet = displaySets[0]; - if (displaySet.Modality !== 'PT') { - continue; - } + ptDisplaySet = displaySets.find( + displaySet => displaySet.Modality === 'PT' + ); - ptDisplaySet = displaySet; + if (ptDisplaySet) { + break; + } } return ptDisplaySet; @@ -121,7 +118,10 @@ const commandsModule = ({ createNewLabelmapFromPT: async () => { // Create a segmentation of the same resolution as the source data // using volumeLoader.createAndCacheDerivedVolume. - const ptDisplaySet = actions.getMatchingPTDisplaySet(); + const { viewportMatchDetails } = HangingProtocolService.getMatchDetails(); + const ptDisplaySet = actions.getMatchingPTDisplaySet({ + viewportMatchDetails, + }); if (!ptDisplaySet) { UINotificationService.error('No matching PT display set found'); @@ -565,8 +565,11 @@ const commandsModule = ({ }, setFusionPTColormap: ({ toolGroupId, colormap }) => { const toolGroup = ToolGroupService.getToolGroup(toolGroupId); + const { viewportMatchDetails } = HangingProtocolService.getMatchDetails(); - const ptDisplaySet = actions.getMatchingPTDisplaySet(); + const ptDisplaySet = actions.getMatchingPTDisplaySet({ + viewportMatchDetails, + }); if (!ptDisplaySet) { return; diff --git a/jest.config.base.js b/jest.config.base.js index b9c22e271..afb3bdd26 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -6,10 +6,6 @@ module.exports = { verbose: true, roots: ['/src'], - transform: { - '^.+\\.js$': 'babel-jest', - '^.+\\.jsx$': 'babel-jest', - }, testMatch: ['/src/**/*.test.js'], testPathIgnorePatterns: ['/node_modules/'], moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'], diff --git a/modes/tmtv/src/index.js b/modes/tmtv/src/index.js index 734020e9e..f77422e94 100644 --- a/modes/tmtv/src/index.js +++ b/modes/tmtv/src/index.js @@ -100,17 +100,19 @@ function modeFactory({ modeConfiguration }) { // For fusion toolGroup we need to add the volumeIds for the crosshairs // since in the fusion viewport we don't want both PT and CT to render MIP // when slabThickness is modified - const matches = HangingProtocolService.getDisplaySetsMatchDetails(); + const { + displaySetMatchDetails, + } = HangingProtocolService.getMatchDetails(); setCrosshairsConfiguration( - matches, + displaySetMatchDetails, toolNames, ToolGroupService, DisplaySetService ); setFusionActiveVolume( - matches, + displaySetMatchDetails, toolNames, ToolGroupService, DisplaySetService diff --git a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js index 5380dd726..bf1aab1b6 100644 --- a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js +++ b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js @@ -211,7 +211,14 @@ const BaseImplementation = { let study = _getStudy(StudyInstanceUID); if (!study) { study = createStudyMetadata(StudyInstanceUID); + // Will typically be undefined with a compliant DICOMweb server, reset later study.StudyDescription = seriesSummaryMetadata[0].StudyDescription; + seriesSummaryMetadata.forEach(item => { + if (study.ModalitiesInStudy.indexOf(item.Modality) === -1) { + study.ModalitiesInStudy.push(item.Modality); + } + }); + study.NumberOfStudyRelatedSeries = seriesSummaryMetadata.length; _model.studies.push(study); } diff --git a/platform/core/src/services/DicomMetadataStore/StudyMetadata.ts b/platform/core/src/services/DicomMetadataStore/StudyMetadata.ts new file mode 100644 index 000000000..c391d09b0 --- /dev/null +++ b/platform/core/src/services/DicomMetadataStore/StudyMetadata.ts @@ -0,0 +1,7 @@ +/** Defines a typescript type for study metadata */ +interface StudyMetadata { + readonly StudyInstanceUID: string; + StudyDescription?: string; +} + +export default StudyMetadata; diff --git a/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js b/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js index bb72dc140..ce59aff66 100644 --- a/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js +++ b/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js @@ -4,6 +4,7 @@ function createStudyMetadata(StudyInstanceUID) { return { StudyInstanceUID, StudyDescription: '', + ModalitiesInStudy: [], isLoaded: false, series: [], /** @@ -13,6 +14,9 @@ function createStudyMetadata(StudyInstanceUID) { */ addInstanceToSeries: function(instance) { const { SeriesInstanceUID } = instance; + if (!this.StudyDescription) { + this.StudyDescription = instance.StudyDescription; + } const existingSeries = this.series.find( s => s.SeriesInstanceUID === SeriesInstanceUID ); @@ -22,16 +26,24 @@ function createStudyMetadata(StudyInstanceUID) { } else { const series = createSeriesMetadata([instance]); this.series.push(series); + const { Modality } = series; + if (this.ModalitiesInStudy.indexof(Modality) === -1) { + this.ModalitiesInStudy.push(Modality); + } } }, /** * * @param {object[]} instances * @param {string} instances[].SeriesInstanceUID + * @param {string} instances[].StudyDescription * @returns {bool} true if series were added; false if series already exist */ addInstancesToSeries: function(instances) { const { SeriesInstanceUID } = instances[0]; + if (!this.StudyDescription) { + this.StudyDescription = instances[0].StudyDescription; + } const existingSeries = this.series.find( s => s.SeriesInstanceUID === SeriesInstanceUID ); diff --git a/platform/core/src/services/DisplaySetService/IDisplaySet.ts b/platform/core/src/services/DisplaySetService/IDisplaySet.ts new file mode 100644 index 000000000..3d7dc2252 --- /dev/null +++ b/platform/core/src/services/DisplaySetService/IDisplaySet.ts @@ -0,0 +1,7 @@ +interface IDisplaySet { + displaySetInstanceUID: string; + StudyInstanceUID: string; + SeriesInstanceUID?: string; +} + +export default IDisplaySet; diff --git a/platform/core/src/services/HangingProtocolService/HPMatcher.js b/platform/core/src/services/HangingProtocolService/HPMatcher.js index 08b66daaf..48a552973 100644 --- a/platform/core/src/services/HangingProtocolService/HPMatcher.js +++ b/platform/core/src/services/HangingProtocolService/HPMatcher.js @@ -4,10 +4,18 @@ import validate from './lib/validator'; * Match a Metadata instance against rules using Validate.js for validation. * @param {InstanceMetadata} metadataInstance Metadata instance object * @param {Array} rules Array of MatchingRules instances (StudyMatchingRule|SeriesMatchingRule|ImageMatchingRule) for the match + * @param {object} options is an object containing additional information + * @param {object[]} options.studies is a list of all the studies + * @param {object[]} options.displaySets is a list of the display sets * @return {Object} Matching Object with score and details (which rule passed or failed) */ -const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => { - const options = { +const match = ( + metadataInstance, + rules, + customAttributeRetrievalCallbacks, + options +) => { + const validateOptions = { format: 'grouped', }; @@ -16,18 +24,32 @@ const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => { failed: [], }; + const readValues = {}; + let requiredFailed = false; let score = 0; rules.forEach(rule => { - const attribute = rule.attribute; - + const { attribute } = rule; // Do not use the custom attribute from the metadataInstance since it is subject to change if (customAttributeRetrievalCallbacks.hasOwnProperty(attribute)) { - const customAttribute = customAttributeRetrievalCallbacks[attribute]; - metadataInstance[attribute] = customAttribute.callback(metadataInstance); + readValues[attribute] = customAttributeRetrievalCallbacks[ + attribute + ].callback(metadataInstance, options); + } else { + readValues[attribute] = + metadataInstance[attribute] ?? + ((metadataInstance.images || metadataInstance.others || [])[0] || {})[ + attribute + ]; } + console.log( + 'Test', + attribute, + readValues[attribute], + JSON.stringify(rule.constraint) + ); // Format the constraint as required by Validate.js const testConstraint = { [attribute]: rule.constraint, @@ -35,13 +57,7 @@ const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => { // Create a single attribute object to be validated, since metadataInstance is an // instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata) - let attributeValue = metadataInstance[attribute]; - if (attributeValue === undefined) { - if (attribute === 'NumberOfStudyRelatedSeries') { - attributeValue = metadataInstance.series?.length; - } - // Add other computable values such as modalities in study - } + let attributeValue = readValues[attribute]; const attributeMap = { [attribute]: attributeValue, }; @@ -49,7 +65,7 @@ const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => { // Use Validate.js to evaluate the constraints on the specified metadataInstance let errorMessages; try { - errorMessages = validate(attributeMap, testConstraint, [options]); + errorMessages = validate(attributeMap, testConstraint, [validateOptions]); } catch (e) { errorMessages = ['Something went wrong during validation.', e]; } @@ -58,8 +74,7 @@ const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => { // If no errorMessages were returned, then validation passed. // Add the rule's weight to the total score - score += parseInt(rule.weight, 10); - + score += parseInt(rule.weight || 1, 10); // Log that this rule passed in the matching details object details.passed.push({ rule, diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js b/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js new file mode 100644 index 000000000..d39610f24 --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js @@ -0,0 +1,166 @@ +import HangingProtocolServiceClass from './HangingProtocolService'; + +const testProtocol = { + id: 'test', + locked: true, + hasUpdatedPriorsInformation: false, + name: 'Default', + createdDate: '2021-02-23T19:22:08.894Z', + modifiedDate: '2021-02-23T19:22:08.894Z', + availableTo: {}, + editableBy: {}, + toolGroupIds: ['ctToolGroup', 'ptToolGroup'], + imageLoadStrategy: 'interleaveTopToBottom', // "default" , "interleaveTopToBottom", "interleaveCenter" + protocolMatchingRules: [ + { + id: 'wauZK2QNEfDPwcAQo', + weight: 1, + attribute: 'StudyDescription', + constraint: { + contains: { + value: 'PETCT', + }, + }, + required: false, + }, + ], + stages: [ + { + id: 'hYbmMy3b7pz7GLiaT', + name: 'default', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 1, + }, + }, + displaySets: [ + { + id: 'displaySet', + seriesMatchingRules: [ + { + weight: 1, + attribute: 'Modality', + constraint: { + equals: 'CT', + }, + required: true, + }, + { + weight: 1, + attribute: 'numImageFrames', + constraint: { + greaterThan: 10, + }, + }, + ], + studyMatchingRules: [], + }, + ], + viewports: [ + { + viewportOptions: { + viewportId: 'ctAXIAL', + viewportType: 'volume', + orientation: 'axial', + toolGroupId: 'ctToolGroup', + customViewportOptions: { + initialScale: 2.5, + }, + initialImageOptions: { + // index: 5, + preset: 'first', // 'first', 'last', 'middle' + }, + syncGroups: [ + { + type: 'cameraPosition', + id: 'axialSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'displaySet', + }, + ], + }, + ], + }, + ], + numberOfPriorsReferenced: -1, +}; + +const studyMatch = { + StudyInstanceUID: 'studyMatch', + StudyDescription: 'A PETCT study type', +}; + +const displaySet1 = { + ...studyMatch, + SeriesInstanceUID: 'ds1', + displaySetInstanceUID: 'displaySet1', + numImageFrames: 11, + Modality: 'CT', +}; + +const displaySet2 = { + ...displaySet1, + SeriesInstanceUID: 'ds2', + displaySetInstanceUID: 'displaySet2', + Modality: 'PT', +}; + +const displaySet3 = { + ...displaySet1, + numImageFrames: 3, + displaySetInstanceUID: 'displaySet3', +}; + +const studyMatchDisplaySets = [displaySet3, displaySet2, displaySet1]; + +describe('HangingProtocolService', () => { + const commandsManager = {}; + const hps = new HangingProtocolServiceClass(commandsManager); + let initialScaling; + + beforeAll(() => { + hps.addProtocols([testProtocol]); + }); + + it('has one protocol', () => { + expect(hps.getProtocols().length).toBe(1); + }); + + describe('run', () => { + it('matches best image match', () => { + hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets }); + const { + hpAlreadyApplied, + viewportMatchDetails, + displaySetMatchDetails, + } = hps.getMatchDetails(); + expect(hpAlreadyApplied).toMatchObject([false]); + expect(viewportMatchDetails.length).toBe(1); + expect(viewportMatchDetails[0]).toMatchObject({ + viewportOptions: { + viewportId: 'ctAXIAL', + viewportType: 'volume', + orientation: 'axial', + toolGroupId: 'ctToolGroup', + }, + // Matches ds1 because it matches 2 rules, a required and an optional + // ds2 fails to match required and ds3 fails to match an optional. + displaySetsInfo: [ + { + SeriesInstanceUID: 'ds1', + displaySetInstanceUID: 'displaySet1', + displaySetOptions: {}, + }, + ], + }); + }); + }); +}); diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.js b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts similarity index 58% rename from platform/core/src/services/HangingProtocolService/HangingProtocolService.js rename to platform/core/src/services/HangingProtocolService/HangingProtocolService.ts index 1af9cc05e..573ae0617 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.js +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts @@ -1,53 +1,110 @@ import pubSubServiceInterface from '../_shared/pubSubServiceInterface'; -import sortBy from '../../utils/sortBy.js'; +import sortBy from '../../utils/sortBy'; import ProtocolEngine from './ProtocolEngine'; +import StudyMetadata from '../DicomMetadataStore/StudyMetadata'; +import IDisplaySet from '../DisplaySetService/IDisplaySet'; const EVENTS = { STAGE_CHANGE: 'event::hanging_protocol_stage_change', + PROTOCOL_CHANGED: 'event::hanging_protocol_changed', NEW_LAYOUT: 'event::hanging_protocol_new_layout', CUSTOM_IMAGE_LOAD_PERFORMED: 'event::hanging_protocol_custom_image_load_performed', }; +type ViewportOptions = { + orientation: string; + toolGroupId: string; + viewportId: string; + viewportType: string; + initialImageOptions: Record; + syncGroups: Record; +}; + +type ViewportMatchDetails = { + viewportOptions: ViewportOptions; + displaySetsInfo: { + SeriesInstanceUID: string; + displaySetInstanceUID: string; + displaySetOptions: Record; + }; +}; + +type DisplaySetMatchDetails = { + SeriesInstanceUID: string; + StudyInstanceUID: string; + displaySetInstanceUID: string; + matchDetails: any; + matchingScore: number; + sortingInfo: any; +}; + +type HangingProtocolMatchDetails = { + displaySetMatchDetails: Map; + viewportMatchDetails: ViewportMatchDetails[]; + hpAlreadyApplied: boolean[]; +}; + class HangingProtocolService { + studies: StudyMetadata[]; + protocols: Record[]; + protocol: Record; + stage: number; + _commandsManager: Record; + protocolEngine: ProtocolEngine; + hpAlreadyApplied: boolean[] = []; + customViewportSettings = []; + displaySets: IDisplaySet[] = []; + activeStudy: Record; + debugLogging: false; + + customAttributeRetrievalCallbacks = { + NumberOfStudyRelatedSeries: { + name: 'The number of series in the study', + callback: metadata => + metadata.NumberOfStudyRelatedSeries ?? metadata.series?.length, + }, + NumberOfSeriesRelatedInstances: { + name: 'The number of instances in the display set', + callback: metadata => metadata.numImageFrames, + }, + ModalitiesInStudy: { + name: 'Gets the array of the modalities for the series', + callback: metadata => + metadata.ModalitiesInStudy ?? + (metadata.series || []).reduce((prev, curr) => { + const { Modality } = curr; + if (Modality && prev.indexOf(Modality) == -1) prev.push(Modality); + return prev; + }, []), + }, + }; + listeners = {}; + registeredImageLoadStrategies = {}; + activeImageLoadStrategyName = null; + customImageLoadPerformed = false; + + /** + * displaySetMatchDetails = + * DisplaySetId is the id defined in the hangingProtocol object itself + * and match is an object that contains information about + */ + displaySetMatchDetails: Map = new Map(); + + /** + * An array that contains for each viewport (viewportIndex) specified in the + * hanging protocol, an object of the form + */ + viewportMatchDetails = [] as ViewportMatchDetails[]; + constructor(commandsManager) { this._commandsManager = commandsManager; this.protocols = []; - this.ProtocolEngine = undefined; + this.protocolEngine = undefined; this.protocol = undefined; this.stage = undefined; - /** - * An array that contains for each viewport (viewportIndex) specified in the - * hanging protocol, an object of the form - * - * { - * viewportOptions, - * displaySetsInfo, // contains array of [ { SeriesInstanceUID, displaySetOPtions}, ... ] - * } - */ - this.matchDetails = []; - /** - * displaySetMatchDetails = - * DisplaySetId is the id defined in the hangingProtocol - * match is an object that contains information about - * - * { - * SeriesInstanceUID, - * StudyInstanceUID, - * matchDetails, - * matchingScore, - * sortingInfo - * } - */ - this.displaySetMatchDetails = new Map(); - this.hpAlreadyApplied = []; + this.studies = []; - this.customViewportSettings = []; - this.customAttributeRetrievalCallbacks = {}; - this.listeners = {}; - this.registeredImageLoadStrategies = {}; - this.activeImageLoadStrategyName = null; - this.customImageLoadPerformed = false; Object.defineProperty(this, 'EVENTS', { value: EVENTS, writable: false, @@ -57,27 +114,27 @@ class HangingProtocolService { Object.assign(this, pubSubServiceInterface); } - reset() { + public reset() { this.studies = []; this.protocols = []; this.hpAlreadyApplied = []; - this.matchDetails = []; + this.viewportMatchDetails = []; // this.ProtocolEngine.reset() } - getDisplaySetsMatchDetails() { - return this.displaySetMatchDetails; + public getMatchDetails(): HangingProtocolMatchDetails { + return { + viewportMatchDetails: this.viewportMatchDetails, + displaySetMatchDetails: this.displaySetMatchDetails, + hpAlreadyApplied: this.hpAlreadyApplied, + }; } - getState() { - return [this.matchDetails, this.hpAlreadyApplied]; - } - - getProtocols() { + public getProtocols() { return this.protocols; } - addProtocols(protocols) { + public addProtocols(protocols) { protocols.forEach(protocol => { if (this.protocols.indexOf(protocol) === -1) { this.protocols.push(this._validateProtocol(protocol)); @@ -85,21 +142,37 @@ class HangingProtocolService { }); } - run(studyMetaData, protocol) { - if (!this.studies.includes(studyMetaData)) { - this.studies.push(studyMetaData); - } - // copy here so we don't mutate it - const metaData = Object.assign({}, studyMetaData); + /** + * Run the hanging protocol decisions tree on the active study, + * studies list and display sets, firing a hanging protocol event when + * complete to indicate the hanging protocol is ready. + * + * @param params is the dataset to run the hanging protocol on. + * @param params.activeStudy is the "primary" study to hang This may or may + * not be displayed by the actual viewports. + * @param params.studies is the list of studies to hang + * @param params.displaySets is the list of display sets associated with + * the studies to display in viewports. + * @param protocol is a specific protocol to apply. + * @returns + */ + public run({ studies, displaySets, activeStudy }, protocol) { + this.studies = [...studies]; + this.displaySets = displaySets; + this.activeStudy = activeStudy || studies[0]; - this.ProtocolEngine = new ProtocolEngine( + this.protocolEngine = new ProtocolEngine( this.protocols, this.customAttributeRetrievalCallbacks ); // if there is no pre-defined protocol if (!protocol || protocol.id === undefined) { - const matchedProtocol = this.ProtocolEngine.run(metaData); + const matchedProtocol = this.protocolEngine.run({ + studies: this.studies, + activeStudy, + displaySets, + }); this._setProtocol(matchedProtocol); return; } @@ -112,7 +185,7 @@ class HangingProtocolService { * and its callback has been added to the HangingProtocolService * @returns {boolean} true */ - hasCustomImageLoadStrategy() { + public hasCustomImageLoadStrategy() { return ( this.activeImageLoadStrategyName !== null && this.registeredImageLoadStrategies[ @@ -121,7 +194,7 @@ class HangingProtocolService { ); } - getCustomImageLoadPerformed() { + public getCustomImageLoadPerformed() { return this.customImageLoadPerformed; } @@ -130,13 +203,13 @@ class HangingProtocolService { * @param {string} name strategy name * @param {Function} callback image loader callback */ - registerImageLoadStrategy(name, callback) { + public registerImageLoadStrategy(name, callback) { if (callback instanceof Function && name) { this.registeredImageLoadStrategies[name] = callback; } } - setHangingProtocolAppliedForViewport(i) { + public setHangingProtocolAppliedForViewport(i) { this.hpAlreadyApplied[i] = true; } @@ -147,18 +220,21 @@ class HangingProtocolService { * @param attributeId The ID used to refer to the attribute (e.g. 'timepointType') * @param attributeName The name of the attribute to be displayed (e.g. 'Timepoint Type') * @param callback The function used to calculate the attribute value from the other attributes at its level (e.g. study/series/image) + * @param options to add to the "this" object for the custom attribute retriever */ - addCustomAttribute(attributeId, attributeName, callback) { + public addCustomAttribute(attributeId, attributeName, callback, options) { this.customAttributeRetrievalCallbacks[attributeId] = { + ...options, + id: attributeId, name: attributeName, - callback: callback, + callback, }; } /** * Switches to the next protocol stage in the display set sequence */ - nextProtocolStage() { + public nextProtocolStage() { console.log('ProtocolEngine::nextProtocolStage'); if (!this._setCurrentProtocolStage(1)) { @@ -169,7 +245,7 @@ class HangingProtocolService { /** * Switches to the previous protocol stage in the display set sequence */ - previousProtocolStage() { + public previousProtocolStage() { console.log('ProtocolEngine::previousProtocolStage'); if (!this._setCurrentProtocolStage(-1)) { @@ -187,8 +263,8 @@ class HangingProtocolService { ]; const loadedData = loader({ data, - displaySetsMatchDetails: this.getDisplaySetsMatchDetails(), - matchDetails: this.matchDetails, + displaySetsMatchDetails: this.displaySetMatchDetails, + viewportMatchDetails: this.viewportMatchDetails, }); // if loader successfully re-arranged the data with the custom strategy @@ -253,6 +329,12 @@ class HangingProtocolService { } } this._updateViewports(); + + this._broadcastChange(this.EVENTS.PROTOCOL_CHANGED, { + viewportMatchDetails: this.viewportMatchDetails, + displaySetMatchDetails: this.displaySetMatchDetails, + hpAlreadyApplied: this.hpAlreadyApplied, + }); } /** @@ -280,9 +362,13 @@ class HangingProtocolService { return this.protocol.stages[this.stage]; } + /** + * Updates the viewports with the selected protocol stage. + */ _updateViewports() { // Make sure we have an active protocol with a non-empty array of display sets if (!this._getNumProtocolStages()) { + console.log('No protocol stages - nothing to display'); return; } @@ -301,6 +387,7 @@ class HangingProtocolService { !stageModel.displaySets || !stageModel.viewports.length ) { + console.log('Stage cannot be applied', stageModel); return; } @@ -311,6 +398,7 @@ class HangingProtocolService { // If no such layout properties exist, stop here. const layoutProps = stageModel.viewportStructure.properties; if (!layoutProps) { + console.log('No viewportStructure.properties in', stageModel); return; } @@ -324,12 +412,13 @@ class HangingProtocolService { }); // Matching the displaySets - // Note: this is happening before displaySets are created. Here, displaySet - // only contains the information of the id of the displaySet to be matched - // based on some rules + stageModel.displaySets.forEach(displaySet => { - const { bestMatch } = this._matchImages(displaySet); + const { bestMatch, matchingScores } = this._matchImages(displaySet); this.displaySetMatchDetails.set(displaySet.id, bestMatch); + if (bestMatch) { + bestMatch.matchingScores = matchingScores; + } }); // Loop through each viewport @@ -339,30 +428,42 @@ class HangingProtocolService { // DisplaySets for the viewport, Note: this is not the actual displaySet, // but it is a info to locate the displaySet from the displaySetService - let displaySetsInfo = []; - viewport.displaySets.forEach(({ id, options: displaySetOptions }) => { - const viewportDisplaySet = this.displaySetMatchDetails.get(id); + const displaySetsInfo = []; + viewport.displaySets.forEach( + ({ id, displaySetIndex = 0, options: displaySetOptions }) => { + const viewportDisplaySetMain = this.displaySetMatchDetails.get(id); + // Use the display set index to allow getting the "next" match, eg + // matching all display sets, and get the displaySetIndex'th item + const viewportDisplaySet = + !viewportDisplaySetMain || displaySetIndex === 0 + ? viewportDisplaySetMain + : viewportDisplaySetMain.matchingScores[displaySetIndex]; - if (viewportDisplaySet) { - const { SeriesInstanceUID } = viewportDisplaySet; + if (viewportDisplaySet) { + const { + SeriesInstanceUID, + displaySetInstanceUID, + } = viewportDisplaySet; - const displaySetInfo = { - SeriesInstanceUID, - displaySetOptions, - }; + const displaySetInfo = { + SeriesInstanceUID, + displaySetInstanceUID, + displaySetOptions, + }; - displaySetsInfo.push(displaySetInfo); - } else { - console.warn( - ` + displaySetsInfo.push(displaySetInfo); + } else { + console.warn( + ` The hanging protocol viewport is requesting to display ${id} displaySet that is not matched based on the provided criteria (e.g. matching rules). ` - ); + ); + } } - }); + ); - this.matchDetails[viewportIndex] = { + this.viewportMatchDetails[viewportIndex] = { viewportOptions, displaySetsInfo, }; @@ -370,50 +471,77 @@ class HangingProtocolService { } // Match images given a list of Studies and a Viewport's image matching reqs - _matchImages(displaySet) { - console.log('ProtocolEngine::matchImages'); - + _matchImages(displaySetRules) { // TODO: matching is applied on study and series level, instance // level matching needs to be added in future // Todo: handle fusion viewports by not taking the first displaySet rule for the viewport - const { studyMatchingRules, seriesMatchingRules } = displaySet; + const { + studyMatchingRules = [], + seriesMatchingRules, + findAll = false, + } = displaySetRules; const matchingScores = []; let highestStudyMatchingScore = 0; let highestSeriesMatchingScore = 0; + console.log( + 'ProtocolEngine::matchImages', + studyMatchingRules, + seriesMatchingRules + ); this.studies.forEach(study => { - const studyMatchDetails = this.ProtocolEngine.findMatch( + const studyDisplaySets = this.displaySets.filter( + it => it.StudyInstanceUID === study.StudyInstanceUID + ); + const studyMatchDetails = this.protocolEngine.findMatch( study, - studyMatchingRules + studyMatchingRules, + { studies: this.studies, displaySets: studyDisplaySets } ); // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed - if ( - studyMatchDetails.requiredFailed === true || - studyMatchDetails.score < highestStudyMatchingScore - ) { + if (studyMatchDetails.requiredFailed === true) { return; } highestStudyMatchingScore = studyMatchDetails.score; - study.series.forEach(aSeries => { - const seriesMatchDetails = this.ProtocolEngine.findMatch( - aSeries, - seriesMatchingRules + this.debug( + 'study', + study.StudyInstanceUID, + 'display sets #', + this.displaySets.length + ); + this.displaySets.forEach(displaySet => { + const { + StudyInstanceUID, + SeriesInstanceUID, + displaySetInstanceUID, + } = displaySet; + if (StudyInstanceUID !== study.StudyInstanceUID) return; + const seriesMatchDetails = this.protocolEngine.findMatch( + displaySet, + seriesMatchingRules, + { studies: this.studies, instance: displaySet.images?.[0] } ); // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed - if ( - seriesMatchDetails.requiredFailed === true || - seriesMatchDetails.score < highestSeriesMatchingScore - ) { + if (seriesMatchDetails.requiredFailed === true) { + this.debug( + 'Display set required failed', + displaySet, + seriesMatchingRules + ); return; } - highestSeriesMatchingScore = seriesMatchDetails.score; + this.debug('Found displaySet for rules', displaySet); + highestSeriesMatchingScore = Math.max( + seriesMatchDetails.score, + highestSeriesMatchingScore + ); const matchDetails = { passed: [], @@ -438,21 +566,27 @@ class HangingProtocolService { seriesMatchDetails.score + studyMatchDetails.score; const imageDetails = { - StudyInstanceUID: study.StudyInstanceUID, - SeriesInstanceUID: aSeries.SeriesInstanceUID, + StudyInstanceUID, + SeriesInstanceUID, + displaySetInstanceUID, matchingScore: totalMatchScore, matchDetails: matchDetails, sortingInfo: { score: totalMatchScore, study: study.StudyInstanceUID, - series: parseInt(aSeries.SeriesNumber), + series: parseInt(displaySet.SeriesNumber), }, }; + this.debug('Adding display set', displaySet, imageDetails); matchingScores.push(imageDetails); }); }); + if (matchingScores.length === 0) { + console.log('No match found'); + } + // Sort the matchingScores const sortingFunction = sortBy( { @@ -473,7 +607,11 @@ class HangingProtocolService { const bestMatch = matchingScores[0]; - console.log('ProtocolEngine::matchImages bestMatch', bestMatch); + console.log( + 'ProtocolEngine::matchImages bestMatch', + bestMatch, + matchingScores + ); return { bestMatch, @@ -495,7 +633,7 @@ class HangingProtocolService { * Check if the previous stage is available * @return {Boolean} True if previous stage is available or false otherwise */ - _isPreviousStageAvailable() { + _isPreviousStageAvailable(): boolean { return this.stage - 1 >= 0; } @@ -506,7 +644,7 @@ class HangingProtocolService { * @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage * @return {Boolean} True if new stage has set or false, otherwise */ - _setCurrentProtocolStage(stageAction) { + _setCurrentProtocolStage(stageAction): boolean { //reseting the applied protocols this.hpAlreadyApplied = []; // Check if previous or next stage is available @@ -520,9 +658,7 @@ class HangingProtocolService { this.stage += stageAction; // Log the new stage - console.log( - `ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}` - ); + this.debug(`ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}`); // Since stage has changed, we need to update the viewports // and redo matchings @@ -530,11 +666,22 @@ class HangingProtocolService { // Everything went well this._broadcastChange(this.EVENTS.STAGE_CHANGE, { - matchDetails: this.matchDetails, + viewportMatchDetails: this.viewportMatchDetails, hpAlreadyApplied: this.hpAlreadyApplied, + displaySetMatchDetails: this.displaySetMatchDetails, }); return true; } + + /** Set this.debugLogging to true to show debug level logging - needed + * to be able to figure out why hanging protocols are or are not applying. + */ + debug(...args): void { + if (this.debugLogging) { + console.log(...args); + } + } + /** * Broadcasts hanging protocols changes. * diff --git a/platform/core/src/services/HangingProtocolService/ProtocolEngine.js b/platform/core/src/services/HangingProtocolService/ProtocolEngine.js index 260c7a248..2c8fddca9 100644 --- a/platform/core/src/services/HangingProtocolService/ProtocolEngine.js +++ b/platform/core/src/services/HangingProtocolService/ProtocolEngine.js @@ -10,8 +10,15 @@ export default class ProtocolEngine { this.study = undefined; } - run(studyMetaData) { - this.study = studyMetaData; + /** Evaluate the hanging protocol matches on the given: + * @param props.studies is a list of studies to compare against (for priors evaluation) + * @param props.activeStudy is the current metadata for the study to display. + * @param props.displaySets are the list of display sets which can be modified. + */ + run({ studies, displaySets, activeStudy }) { + this.studies = studies; + this.study = activeStudy || studies[0]; + this.displaySets = displaySets; return this.getBestProtocolMatch(); } @@ -49,9 +56,11 @@ export default class ProtocolEngine { // Clear all data currently in matchedProtocols this._clearMatchedProtocols(); - // TODO: handle more than one study - const study = this.study; - const matched = this.findMatchByStudy(study); + // TODO: handle more than one study - this.studies has the list of studies + const matched = this.findMatchByStudy(this.study, { + studies: this.studies, + displaySets: this.displaySets, + }); // For each matched protocol, check if it is already in MatchedProtocols matched.forEach(matchedDetail => { @@ -72,11 +81,12 @@ export default class ProtocolEngine { }); } - findMatch(metaData, rules) { + findMatch(metaData, rules, options) { return HPMatcher.match( metaData, rules, - this.customAttributeRetrievalCallbacks + this.customAttributeRetrievalCallbacks, + options ); } @@ -84,11 +94,12 @@ export default class ProtocolEngine { * Finds the best protocols from Protocol Store, matching each protocol matching rules * with the given study. The best protocol are ordered by score and returned in an array * @param {Object} study StudyMetadata instance object + * @param {object} options containing additional matching data. * @return {Array} Array of match objects or an empty array if no match was found * Each match object has the score of the matching and the matched * protocol */ - findMatchByStudy(study) { + findMatchByStudy(study, options) { const matched = []; this.protocols.forEach(protocol => { @@ -98,13 +109,14 @@ export default class ProtocolEngine { let rules = protocol.protocolMatchingRules.slice(); if (!rules || !rules.length) { console.warn( - 'ProtocolEngine::findMatchByStudy no matching rules - specify protocolMatchingRules' + 'ProtocolEngine::findMatchByStudy no matching rules - specify protocolMatchingRules', + protocol.id ); return; } // Run the matcher and get matching details - const matchedDetails = this.findMatch(study, rules); + const matchedDetails = this.findMatch(study, rules, options); const score = matchedDetails.score; // The protocol matched some rule, add it to the matched list @@ -156,605 +168,3 @@ export default class ProtocolEngine { return this.matchedProtocols.get(highestScoringProtocolId); } } - -/** - * Resets the ProtocolEngine to the best match - */ - -/** - * Retrieves the current Stage from the current Protocol and stage index - * - * @returns {*} The Stage model for the currently displayed Stage - */ -// getCurrentStageModel() { -// return this.protocol.stages[this.stage]; -// } - -// /** -// * Get the number of prior studies supplied in the priorStudies map property. -// * -// * @param {String} studyObjectID The study object ID of the study whose priors are needed -// * @returns {number} The number of available prior studies with the same PatientID -// */ -// getNumberOfAvailablePriors(studyObjectID) { -// return this.getAvailableStudyPriors(studyObjectID).length; -// } - -// /** -// * Get the array of prior studies from a specific study. -// * -// * @param {String} studyObjectID The study object ID of the study whose priors are needed -// * @returns {Array} The array of available priors or an empty array -// */ -// getAvailableStudyPriors(studyObjectID) { -// const priors = this.priorStudies.get(studyObjectID); - -// return priors instanceof Array ? priors : []; -// } - -// // Match images given a list of Studies and a Viewport's image matching reqs -// matchImages(viewport, viewportIndex) { -// log.trace('ProtocolEngine::matchImages'); - -// const { -// studyMatchingRules, -// seriesMatchingRules, -// imageMatchingRules: instanceMatchingRules, -// } = viewport; - -// const matchingScores = []; -// const currentStudy = this.studies[0]; // @TODO: Should this be: this.studies[this.currentStudy] ??? -// const firstInstance = currentStudy.getFirstInstance(); - -// let highestStudyMatchingScore = 0; -// let highestSeriesMatchingScore = 0; - -// // Set custom attribute for study metadata and it's first instance -// currentStudy.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0); -// if (firstInstance instanceof InstanceMetadata) { -// firstInstance.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0); -// } - -// // Only used if study matching rules has abstract prior values defined... -// let priorStudies; - -// studyMatchingRules.forEach(rule => { -// if (rule.attribute === ABSTRACT_PRIOR_VALUE) { -// const validatorType = Object.keys(rule.constraint)[0]; -// const validator = Object.keys(rule.constraint[validatorType])[0]; - -// let abstractPriorValue = rule.constraint[validatorType][validator]; -// abstractPriorValue = parseInt(abstractPriorValue, 10); -// // TODO: Restrict or clarify validators for abstractPriorValue? - -// // No need to call it more than once... -// if (!priorStudies) { -// priorStudies = this.getAvailableStudyPriors( -// currentStudy.getObjectID() -// ); -// } - -// // TODO: Revisit this later: What about two studies with the same -// // study date? - -// let priorStudy; -// if (abstractPriorValue === -1) { -// priorStudy = priorStudies[priorStudies.length - 1]; -// } else { -// const studyIndex = Math.max(abstractPriorValue - 1, 0); -// priorStudy = priorStudies[studyIndex]; -// } - -// // Invalid data -// if (!priorStudy instanceof StudyMetadata) { -// return; -// } - -// const priorStudyObjectID = priorStudy.getObjectID(); - -// // Check if study metadata is already in studies list -// if ( -// this.studies.find(study => study.getObjectID() === priorStudyObjectID) -// ) { -// return; -// } - -// // Get study metadata if necessary and load study in the viewer (each viewer should provide it's own load study method) -// this.studyMetadataSource.loadStudy(priorStudy).then( -// studyMetadata => { -// // Set the custom attribute abstractPriorValue for the study metadata -// studyMetadata.setCustomAttribute( -// ABSTRACT_PRIOR_VALUE, -// abstractPriorValue -// ); - -// // Also add custom attribute -// const firstInstance = studyMetadata.getFirstInstance(); -// if (firstInstance instanceof InstanceMetadata) { -// firstInstance.setCustomAttribute( -// ABSTRACT_PRIOR_VALUE, -// abstractPriorValue -// ); -// } - -// // Insert the new study metadata -// this.studies.push(studyMetadata); - -// // Update the viewport to refresh layout manager with new study -// this.updateViewports(viewportIndex); -// }, -// error => { -// log.warn(error); -// throw new OHIFError( -// `ProtocolEngine::matchImages could not get study metadata for the Study with the following ObjectID: ${priorStudyObjectID}` -// ); -// } -// ); -// } -// // TODO: Add relative Date / time -// }); - -// this.studies.forEach(study => { -// const studyMatchDetails = HPMatcher.match( -// study.getFirstInstance(), -// studyMatchingRules -// ); - -// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed -// if ( -// studyMatchDetails.requiredFailed === true || -// studyMatchDetails.score < highestStudyMatchingScore -// ) { -// return; -// } - -// highestStudyMatchingScore = studyMatchDetails.score; - -// study.forEachSeries(series => { -// const seriesMatchDetails = HPMatcher.match( -// series.getFirstInstance(), -// seriesMatchingRules -// ); - -// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed -// if ( -// seriesMatchDetails.requiredFailed === true || -// seriesMatchDetails.score < highestSeriesMatchingScore -// ) { -// return; -// } - -// highestSeriesMatchingScore = seriesMatchDetails.score; - -// series.forEachInstance((instance, index) => { -// // This tests to make sure there is actually image data in this instance -// // TODO: Change this when we add PDF and MPEG support -// // See https://ohiforg.atlassian.net/browse/LT-227 -// if ( -// !isImage(instance.getTagValue('SOPClassUID')) && -// !instance.getTagValue('Rows') -// ) { -// return; -// } - -// const instanceMatchDetails = HPMatcher.match( -// instance, -// instanceMatchingRules -// ); - -// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed -// if (instanceMatchDetails.requiredFailed === true) { -// return; -// } - -// const matchDetails = { -// passed: [], -// failed: [], -// }; - -// matchDetails.passed = matchDetails.passed.concat( -// instanceMatchDetails.details.passed -// ); -// matchDetails.passed = matchDetails.passed.concat( -// seriesMatchDetails.details.passed -// ); -// matchDetails.passed = matchDetails.passed.concat( -// studyMatchDetails.details.passed -// ); - -// matchDetails.failed = matchDetails.failed.concat( -// instanceMatchDetails.details.failed -// ); -// matchDetails.failed = matchDetails.failed.concat( -// seriesMatchDetails.details.failed -// ); -// matchDetails.failed = matchDetails.failed.concat( -// studyMatchDetails.details.failed -// ); - -// const totalMatchScore = -// instanceMatchDetails.score + -// seriesMatchDetails.score + -// studyMatchDetails.score; -// const currentSOPInstanceUID = instance.getSOPInstanceUID(); - -// const imageDetails = { -// StudyInstanceUID: study.getStudyInstanceUID(), -// SeriesInstanceUID: series.getSeriesInstanceUID(), -// SOPInstanceUID: currentSOPInstanceUID, -// currentImageIdIndex: index, -// matchingScore: totalMatchScore, -// matchDetails: matchDetails, -// sortingInfo: { -// score: totalMatchScore, -// study: -// instance.getTagValue('StudyDate') + -// instance.getTagValue('StudyTime'), -// series: parseInt(instance.getTagValue('SeriesNumber')), // TODO: change for seriesDateTime -// instance: parseInt(instance.getTagValue('InstanceNumber')), // TODO: change for acquisitionTime -// }, -// }; - -// // Find the displaySet -// const displaySet = study.findDisplaySet(displaySet => -// displaySet.images.find( -// image => image.getSOPInstanceUID() === currentSOPInstanceUID -// ) -// ); - -// // If the instance was found, set the displaySet ID -// if (displaySet) { -// imageDetails.displaySetInstanceUID = displaySet.getUID(); -// imageDetails.imageId = instance.getImageId(); -// } - -// matchingScores.push(imageDetails); -// }); -// }); -// }); - -// // Sort the matchingScores -// const sortingFunction = sortBy( -// { -// name: 'score', -// reverse: true, -// }, -// { -// name: 'study', -// reverse: true, -// }, -// { -// name: 'instance', -// }, -// { -// name: 'series', -// } -// ); -// matchingScores.sort((a, b) => -// sortingFunction(a.sortingInfo, b.sortingInfo) -// ); - -// const bestMatch = matchingScores[0]; - -// log.trace('ProtocolEngine::matchImages bestMatch', bestMatch); - -// return { -// bestMatch, -// matchingScores, -// }; -// } - -/** - * Sets the current layout - * - * @param {number} numRows - * @param {number} numColumns - */ -// setLayout(numRows, numColumns) { -// if (numRows < 1 && numColumns < 1) { -// log.error(`Invalid layout ${numRows} x ${numColumns}`); -// return; -// } - -// if (typeof this.options.setLayout !== 'function') { -// log.error('Hanging Protocol Engine setLayout callback is not defined'); -// return; -// } - -// let viewports = []; -// const numViewports = numRows * numColumns; - -// for (let i = 0; i < numViewports; i++) { -// viewports.push({}); -// } - -// this.options.setLayout({ numRows, numColumns, viewports }); -// } - -// /** -// * Rerenders viewports that are part of the current layout manager -// * using the matching rules internal to each viewport. -// * -// * If this function is provided the index of a viewport, only the specified viewport -// * is rerendered. -// * -// * @param viewportIndex -// */ -// updateViewports(viewportIndex) { -// log.trace( -// `ProtocolEngine::updateViewports viewportIndex: ${viewportIndex}` -// ); - -// // Make sure we have an active protocol with a non-empty array of display sets -// if (!this.getNumProtocolStages()) { -// return; -// } - -// // Retrieve the current stage -// const stageModel = this.getCurrentStageModel(); - -// // If the current stage does not fulfill the requirements to be displayed, -// // stop here. -// if ( -// !stageModel || -// !stageModel.viewportStructure || -// !stageModel.viewports || -// !stageModel.viewports.length -// ) { -// return; -// } - -// // Retrieve the layoutTemplate associated with the current display set's viewport structure -// // If no such template name exists, stop here. -// const layoutTemplateName = stageModel.viewportStructure.getLayoutTemplateName(); -// if (!layoutTemplateName) { -// return; -// } - -// // Retrieve the properties associated with the current display set's viewport structure template -// // If no such layout properties exist, stop here. -// const layoutProps = stageModel.viewportStructure.properties; -// if (!layoutProps) { -// return; -// } - -// // Create an empty array to store the output viewportData -// const viewportData = []; - -// // Empty the matchDetails associated with the ProtocolEngine. -// // This will be used to store the pass/fail details and score -// // for each of the viewport matching procedures -// this.matchDetails = []; - -// // Loop through each viewport -// stageModel.viewports.forEach((viewport, viewportIndex) => { -// const details = this.matchImages(viewport, viewportIndex); - -// this.matchDetails[viewportIndex] = details; - -// // Convert any YES/NO values into true/false for Cornerstone -// const cornerstoneViewportParams = {}; - -// // Cache viewportSettings keys -// const viewportSettingsKeys = Object.keys(viewport.viewportSettings); - -// viewportSettingsKeys.forEach(key => { -// let value = viewport.viewportSettings[key]; -// if (value === 'YES') { -// value = true; -// } else if (value === 'NO') { -// value = false; -// } - -// cornerstoneViewportParams[key] = value; -// }); - -// // imageViewerViewports occasionally needs relevant layout data in order to set -// // the element style of the viewport in question -// const currentViewportData = { -// viewportIndex, -// viewport: cornerstoneViewportParams, -// ...layoutProps, -// }; - -// const customSettings = []; -// viewportSettingsKeys.forEach(id => { -// const setting = CustomViewportSettings[id]; -// if (!setting) { -// return; -// } - -// customSettings.push({ -// id: id, -// value: viewport.viewportSettings[id], -// }); -// }); - -// currentViewportData.renderedCallback = element => { -// //console.log('renderedCallback for ' + element.id); -// customSettings.forEach(customSetting => { -// log.trace( -// `ProtocolEngine::currentViewportData.renderedCallback Applying custom setting: ${customSetting.id}` -// ); -// log.trace( -// `ProtocolEngine::currentViewportData.renderedCallback with value: ${customSetting.value}` -// ); - -// const setting = CustomViewportSettings[customSetting.id]; -// setting.callback(element, customSetting.value); -// }); -// }; - -// let currentMatch = details.bestMatch; -// let currentPosition = 1; -// const scoresLength = details.matchingScores.length; -// while ( -// currentPosition < scoresLength && -// viewportData.find(a => a.imageId === currentMatch.imageId) -// ) { -// currentMatch = details.matchingScores[currentPosition]; -// currentPosition++; -// } - -// if (currentMatch && currentMatch.imageId) { -// currentViewportData.StudyInstanceUID = currentMatch.StudyInstanceUID; -// currentViewportData.SeriesInstanceUID = currentMatch.SeriesInstanceUID; -// currentViewportData.SOPInstanceUID = currentMatch.SOPInstanceUID; -// currentViewportData.currentImageIdIndex = -// currentMatch.currentImageIdIndex; -// currentViewportData.displaySetInstanceUID = -// currentMatch.displaySetInstanceUID; -// currentViewportData.imageId = currentMatch.imageId; -// } - -// // @TODO Why should we throw an exception when a best match is not found? This was aborting the whole process. -// // if (!currentViewportData.displaySetInstanceUID) { -// // throw new OHIFError('ProtocolEngine::updateViewports No matching display set found?'); -// // } - -// viewportData.push(currentViewportData); -// }); - -// this.setLayout(layoutProps.Rows, layoutProps.Columns); - -// if (typeof this.options.setViewportSpecificData !== 'function') { -// log.error( -// 'Hanging Protocol Engine setViewportSpecificData callback is not defined' -// ); -// return; -// } - -// // If viewportIndex is defined, then update only that viewport -// if (viewportIndex !== undefined && viewportData[viewportIndex]) { -// this.options.setViewportSpecificData( -// viewportIndex, -// viewportData[viewportIndex] -// ); -// return; -// } - -// // Update all viewports -// viewportData.forEach(viewportSpecificData => { -// this.options.setViewportSpecificData( -// viewportSpecificData.viewportIndex, -// viewportSpecificData -// ); -// }); -// } - -// /** -// * Sets the current Hanging Protocol to the specified Protocol -// * An optional argument can also be used to prevent the updating of the Viewports -// * -// * @param newProtocol -// * @param updateViewports -// */ -// setHangingProtocol(newProtocol, updateViewports = true) { -// log.trace('ProtocolEngine::setHangingProtocol newProtocol', newProtocol); -// log.trace( -// `ProtocolEngine::setHangingProtocol updateViewports = ${updateViewports}` -// ); - -// // Reset the array of newStageIds -// this.newStageIds = []; - -// if (Protocol.prototype.isPrototypeOf(newProtocol)) { -// this.protocol = newProtocol; -// } else { -// this.protocol = new Protocol(); -// this.protocol.fromObject(newProtocol); -// } - -// this.stage = 0; - -// // Update viewports by default -// if (updateViewports) { -// this.updateViewports(); -// } -// } - -// /** -// * Check if the next stage is available -// * @return {Boolean} True if next stage is available or false otherwise -// */ -// isNextStageAvailable() { -// const numberOfStages = this.getNumProtocolStages(); - -// return this.stage + 1 < numberOfStages; -// } - -// /** -// * Check if the previous stage is available -// * @return {Boolean} True if previous stage is available or false otherwise -// */ -// isPreviousStageAvailable() { -// return this.stage - 1 >= 0; -// } - -// /** -// * Changes the current stage to a new stage index in the display set sequence. -// * It checks if the next stage exists. -// * -// * @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage -// * @return {Boolean} True if new stage has set or false, otherwise -// */ -// setCurrentProtocolStage(stageAction) { -// // Check if previous or next stage is available -// if (stageAction === -1 && !this.isPreviousStageAvailable()) { -// return false; -// } else if (stageAction === 1 && !this.isNextStageAvailable()) { -// return false; -// } - -// // Sets the new stage -// this.stage += stageAction; - -// // Log the new stage -// log.trace(`ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}`); - -// // Since stage has changed, we need to update the viewports -// // and redo matchings -// this.updateViewports(); - -// // Everything went well -// return true; -// } - -// /** -// * Retrieves the number of Stages in the current Protocol or -// * undefined if no protocol or stages are set -// */ -// getNumProtocolStages() { -// if ( -// !this.protocol || -// !this.protocol.stages || -// !this.protocol.stages.length -// ) { -// return; -// } - -// return this.protocol.stages.length; -// } - -// /** -// * Switches to the next protocol stage in the display set sequence -// */ -// nextProtocolStage() { -// log.trace('ProtocolEngine::nextProtocolStage'); - -// if (!this.setCurrentProtocolStage(1)) { -// log.trace('ProtocolEngine::nextProtocolStage failed'); -// } -// } - -// /** -// * Switches to the previous protocol stage in the display set sequence -// */ -// previousProtocolStage() { -// log.trace('ProtocolEngine::previousProtocolStage'); - -// if (!this.setCurrentProtocolStage(-1)) { -// log.trace('ProtocolEngine::previousProtocolStage failed'); -// } -// } -// } diff --git a/platform/core/src/services/HangingProtocolService/lib/validator.js b/platform/core/src/services/HangingProtocolService/lib/validator.js index 454143a03..8bdc06949 100644 --- a/platform/core/src/services/HangingProtocolService/lib/validator.js +++ b/platform/core/src/services/HangingProtocolService/lib/validator.js @@ -1,39 +1,88 @@ import validate from 'validate.js'; -validate.validators.equals = function (value, options, key, attributes) { - if (options && value !== options.value) { - return key + 'must equal ' + options.value; +validate.validators.equals = function(value, options, key, attributes) { + const testValue = options?.value ?? options; + if (value !== testValue) { + return key + 'must equal ' + testValue; } }; -validate.validators.doesNotEqual = function (value, options, key) { - if (options && value === options.value) { - return key + 'cannot equal ' + options.value; +validate.validators.doesNotEqual = function(value, options, key) { + const testValue = options?.value ?? options; + if (value === testValue) { + return key + 'cannot equal ' + testValue; } }; -validate.validators.contains = function (value, options, key) { - if (options && value.indexOf && value.indexOf(options.value) === -1) { - return key + 'must contain ' + options.value; +validate.validators.contains = function(value, options, key) { + const testValue = options?.value ?? options; + if (Array.isArray(value)) { + if (value.some(item => !validate.validators.contains(item, options, key))) { + return undefined; + } + return `No item of ${value.join(',')} contains ${JSON.stringify( + testValue + )}`; + } + if (Array.isArray(testValue)) { + if ( + testValue.some( + subTest => !validate.validators.contains(value, subTest, key) + ) + ) { + return; + } + return `${key} must contain at least one of ${testValue.join(',')}`; + } + if (testValue && value.indexOf && value.indexOf(testValue) === -1) { + return key + 'must contain ' + testValue; } }; -validate.validators.doesNotContain = function (value, options, key) { +validate.validators.doesNotContain = function(value, options, key) { if (options && value.indexOf && value.indexOf(options.value) !== -1) { return key + 'cannot contain ' + options.value; } }; -validate.validators.startsWith = function (value, options, key) { +validate.validators.startsWith = function(value, options, key) { if (options && value.startsWith && !value.startsWith(options.value)) { return key + 'must start with ' + options.value; } }; -validate.validators.endsWith = function (value, options, key) { +validate.validators.endsWith = function(value, options, key) { if (options && value.endsWith && !value.endsWith(options.value)) { return key + 'must end with ' + options.value; } }; +validate.validators.greaterThan = function(value, options, key) { + const testValue = options?.value ?? options; + if (testValue !== undefined && value <= testValue) { + return key + 'with value ' + value + ' must be greater than ' + testValue; + } +}; + +validate.validators.range = function(value, options, key) { + const testValue = options?.value ?? options; + if ( + (testValue !== undefined && value < testValue[0]) || + value > testValue[1] + ) { + return ( + key + + 'with value ' + + value + + ' must be between ' + + testValue[0] + + ' and ' + + testValue[1] + ); + } +}; + +validate.validators.notNull = value => + value === null || value === undefined ? 'Value is null' : undefined; + export default validate; diff --git a/platform/core/src/services/HangingProtocolService/lib/validator.test.js b/platform/core/src/services/HangingProtocolService/lib/validator.test.js new file mode 100644 index 000000000..4b20f8029 --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/lib/validator.test.js @@ -0,0 +1,49 @@ +import validate from "./validator.js"; + +describe("validator", () => { + const attributeMap = { + str: "string", + num: 3, + nullValue: null, + list: ["abc", "def"], + } + + const options = { + format: 'grouped', + }; + + describe("contains", () => { + it("returns match any list contains", () => { + expect(validate(attributeMap, { list: { contains: 'a' } }, [options])).toBeUndefined(); + expect(validate(attributeMap, { str: { contains: 'i' } }, [options])).toBeUndefined(); + expect(validate(attributeMap, { str: { contains: ['i'] } }, [options])).toBeUndefined(); + expect(validate(attributeMap, { list: { contains: ['a'] } }, [options])).toBeUndefined(); + expect(validate(attributeMap, { list: { contains: ['z', 'd'] } }, [options])).toBeUndefined(); + expect(validate(attributeMap, { list: { contains: ['z'] } }, [options])).not.toBeUndefined(); + }) + }) + + describe("equals", () => { + it("returned undefined on equals", () => { + expect(validate(attributeMap, { str: { equals: attributeMap.str } }, [options])).toBeUndefined(); + expect(validate(attributeMap, { num: { equals: { value: attributeMap.num } } }, [options])).toBeUndefined(); + }) + + it("returns error on not equals", () => { + expect(validate(attributeMap, { str: { equals: "abc" } }, [options])).not.toBeUndefined(); + expect(validate(attributeMap, { num: { equals: { value: 1 + attributeMap.num } } }, [options])).not.toBeUndefined(); + }) + }) + + describe("greaterThan", () => { + it("returns undefined on greaterThan", () => { + expect(validate(attributeMap, { num: { greaterThan: { value: attributeMap.num - 1 } } }, [options])).toBeUndefined(); + expect(validate(attributeMap, { num: { greaterThan: attributeMap.num - 1 } }, [options])).toBeUndefined(); + }) + + it("returns error on not greater than", () => { + expect(validate(attributeMap, { num: { greaterThan: { value: attributeMap.num } } }, [options])).not.toBeUndefined(); + expect(validate(attributeMap, { num: { greaterThan: attributeMap.num } }, [options])).not.toBeUndefined(); + }) + }) +}); diff --git a/platform/docs/docs/platform/services/data/DisplaySetService.md b/platform/docs/docs/platform/services/data/DisplaySetService.md index f6c053cb6..6a42d6269 100644 --- a/platform/docs/docs/platform/services/data/DisplaySetService.md +++ b/platform/docs/docs/platform/services/data/DisplaySetService.md @@ -10,41 +10,7 @@ sidebar_label: DisplaySet Service > Based on the instanceMetadata's `SOPClassHandlerId`, the correct module from the registered extensions is found by `OHIF` and its `getDisplaySetsFromSeries` runs to create a DisplaySet for the Series. - -```js title="platform/core/src/services/DisplaySetService/DisplaySetService.js" -init(extensionManager, SOPClassHandlerIds) { - this.extensionManager = extensionManager; - this.SOPClassHandlerIds = SOPClassHandlerIds; - this.activeDisplaySets = []; -} -``` - -in `Mode.jsx` - -```js title="platform/viewer/src/routes/Mode/Mode.jsx" -export default function ModeRoute(/** ... **/) { - /** ... **/ - const { DisplaySetService } = servicesManager.services - const { sopClassHandlers } = mode - /** ... **/ - useEffect( - () => { - /** ... **/ - - // Add SOPClassHandlers to a new SOPClassManager. - DisplaySetService.init(extensionManager, sopClassHandlers) - - /** ... **/ - } - /** ... **/ - ) - /** ... **/ - return <> /** ... **/ -} -``` - - - +DisplaySets are created synchronously when the instances metadata is retrieved and added to the [DicomMetaDataStore](../data//DicomMetadataStore.md). ## Events There are three events that get broadcasted in `DisplaySetService`: diff --git a/platform/docs/versioned_docs/version-3.0/platform/services/data/DisplaySetService.md b/platform/docs/versioned_docs/version-3.0/platform/services/data/DisplaySetService.md index f6c053cb6..defc022c6 100644 --- a/platform/docs/versioned_docs/version-3.0/platform/services/data/DisplaySetService.md +++ b/platform/docs/versioned_docs/version-3.0/platform/services/data/DisplaySetService.md @@ -59,7 +59,6 @@ There are three events that get broadcasted in `DisplaySetService`: - ## API Let's find out about the public API for `DisplaySetService`. @@ -83,3 +82,7 @@ Let's find out about the public API for `DisplaySetService`. - `getActiveDisplaySets`: Returns the active displaySets - `deleteDisplaySet`: Deletes the displaySets from the displaySets cache + +- `holdChangeEvents`: Prevents firing change events (currently only works on add event). + +- `fireHoldChangeEvents`: Causes the change event to be fired IF there were any changes. No longer holds events. diff --git a/platform/docs/versioned_docs/version-3.0/platform/services/data/HangingProtocolService.md b/platform/docs/versioned_docs/version-3.0/platform/services/data/HangingProtocolService.md index 575873953..641a6c3ee 100644 --- a/platform/docs/versioned_docs/version-3.0/platform/services/data/HangingProtocolService.md +++ b/platform/docs/versioned_docs/version-3.0/platform/services/data/HangingProtocolService.md @@ -29,7 +29,7 @@ You can find the skeleton of the hanging protocols here: ```js const defaultProtocol = { - id: 'defaultProtocol', + id: 'test', locked: true, hasUpdatedPriorsInformation: false, name: 'Default', @@ -37,31 +37,91 @@ const defaultProtocol = { modifiedDate: '2021-02-23T19:22:08.894Z', availableTo: {}, editableBy: {}, - protocolMatchingRules: [], + toolGroupIds: [ + 'ctToolGroup', + 'ptToolGroup', + ], + imageLoadStrategy: 'interleaveTopToBottom', // "default" , "interleaveTopToBottom", "interleaveCenter" + protocolMatchingRules: [ + { + id: 'wauZK2QNEfDPwcAQo', + weight: 1, + attribute: 'StudyDescription', + constraint: { + contains: { + value: 'PETCT', + }, + }, + required: false, + }, + ], stages: [ { - id: 'nwzau7jDkEkL8djfr', - name: 'oneByOne', + id: 'hYbmMy3b7pz7GLiaT', + name: 'default', viewportStructure: { - type: 'grid', + layoutType: 'grid', properties: { rows: 1, columns: 1, }, }, - viewports: [ + displaySets: [ { - viewportSettings: [], - imageMatchingRules: [], - seriesMatchingRules: [], + id: 'displaySet', + seriesMatchingRules: [ + { + id: 'GPEYqFLv2dwzCM322', + weight: 1, + attribute: 'Modality', + constraint: { + equals: 'CT', + }, + required: true, + }, + { + id: 'vSjk7NCYjtdS3XZAw', + weight: 1, + attribute: 'numImageFrames', + constraint: { + greaterThan: 10, + }, + }, + ], studyMatchingRules: [], }, ], - createdDate: '2021-02-23T19:22:08.894Z', + viewports: [ + { + viewportOptions: { + viewportId: 'ctAXIAL', + viewportType: 'volume', + orientation: 'axial', + toolGroupId: 'ctToolGroup', + initialImageOptions: { + // index: 5, + preset: 'first', // 'first', 'last', 'middle' + }, + syncGroups: [ + { + type: 'cameraPosition', + id: 'axialSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'displaySet', + }, + ], + }, + ], }, ], numberOfPriorsReferenced: -1, -}; +} ``` Let's discuss each property in depth. @@ -74,7 +134,7 @@ Let's discuss each property in depth. - `weight`: weight for the matching rule. Eventually, all the registered protocols get sorted based on the weights, and the winning protocol gets applied to the viewer. - - `attriubte`: tag that needs to be matched against. This can be either + - `attribute`: tag that needs to be matched against. This can be either Study-level metadata or a custom attribute. [Learn more about custom attribute matching](#custom-attribute) @@ -85,92 +145,65 @@ Let's discuss each property in depth. ```js { - id: 'wauZK2QNEfDPwcAQo', weight: 1, attribute: 'StudyInstanceUID', constraint: { - equals: { - value: '1.3.6.1.4.1.25403.345050719074.3824.20170125112931.11', - }, + equals: '1.3.6.1.4.1.25403.345050719074.3824.20170125112931.11', }, required: true, } ``` + - `stages`: Each protocol can define one or more stages. Each stage defines a certain layout and viewport rules. Therefore, the `stages` property is array of objects, each object being one stage. + - `displaySets`: Defines the matching rules for which display sets to use. - `viewportStructure`: Defines the layout of the viewer. You can define the - number of `rows` and `columns`. There should be `rows * columns` number of - viewport configuration in the `viewports` property. Note that order of - viewports are rows first then columns. + number of `rows` and `columns`. + - `viewports` defines the actual viewports to display. There should be `rows * columns` number of + these `viewports` property, ordered rows first, then columns. - - `viewportSettings`: custom settings to be applied to the viewport. This can - be a `voi` being applied to the viewer or a tool to get enabled. We will - discuss viewport-specific settings [below](#viewport-settings) - - - `imageMatchingRules (comming soon)`: setting the image slice for the - viewport. - - - `seriesMatchingRules`: the most important rule that matches series in the - viewport. For instance, the following stage configuration will create a - one-by-two layout and put the series whose description contains `t2` on the - left, and a series with description that contains `adc` on the right. (order - of viewports are rows, first then columns) ```js stages: [ { id: 'hYbmMy3b7pz7GLiaT', - name: 'oneByThree', + name: 'oneByTwo', viewportStructure: { type: 'grid', properties: { rows: 1, - columns: 2, + columns: 3, }, }, viewports: [ // viewport 1 - { - viewportSettings: [], - imageMatchingRules: [], - seriesMatchingRules: [ + { + viewportOptions: { + viewportId: 'ctAXIAL', + viewportType: 'volume', + orientation: 'axial', + toolGroupId: 'ctToolGroup', + initialImageOptions: { + // index: 5, + preset: 'first', // 'first', 'last', 'middle' + }, + syncGroups: [ { - id: 'vSjk7NCYjtdS3XZAw', - weight: 1, - attribute: 'SeriesDescription', - constraint: { - contains: { - value: 't2', - }, - }, - required: false, + type: 'cameraPosition', + id: 'axialSync', + source: true, + target: true, }, ], - studyMatchingRules: [], }, - // viewport 2 - { - viewportSettings: [], - imageMatchingRules: [], - seriesMatchingRules: [ - { - id: 'vSjk7NCYjtdS3XZAw', - weight: 1, - attribute: 'SeriesDescription', - constraint: { - contains: { - value: 'ADC', - }, - }, - required: true, - }, - ], - studyMatchingRules: [], - }, - ], - }, + displaySets: [ + { + id: 'displaySet', + }, + ], + }, ]; ``` @@ -194,8 +227,9 @@ There are two events that get publish in `HangingProtocolService`: - `addProtocols`: adds provided protocols to the list of registered protocols for matching -- `run(studyMetaData, protocol)`: runs the HPService with the provided - studyMetaData and optional protocol. If protocol is not given, HP Matching +- `run({ studies, displaySets }, protocol)`: runs the HPService with the provided + list of studies, display sets and optional protocol. + If protocol is not given, HP Matching engine will search all the registered protocols for the best matching one based on the constraints. @@ -206,7 +240,11 @@ There are two events that get publish in `HangingProtocolService`: protocol definitions. `addCustomViewportSetting` is another way to set these settings which is exposed by API -- +- `hps.applyCustomViewportSettings(viewportOptions, viewport,...args)` will run + the callback registered with addCustomViewportSetting for all custom settings + whose name matches the id of the custom viewport, with the arguments (id, value, viewport, ...args) + + Default initialization of the modes handles running the `HangingProtocolService` @@ -278,7 +316,7 @@ function modeFactory() { metaData => getFirstMeasurementSeriesInstanceUID(metaData) ); - HangingProtocolService.run(studyMetadata); + HangingProtocolService.run(studyMetadata, DisplaySetService.getActiveDisplaySets()); }; DicomMetadataStore.subscribe( diff --git a/platform/docs/versioned_docs/version-3.0/platform/services/pubsub.md b/platform/docs/versioned_docs/version-3.0/platform/services/pubsub.md index 4fdc8748c..fae6978e6 100644 --- a/platform/docs/versioned_docs/version-3.0/platform/services/pubsub.md +++ b/platform/docs/versioned_docs/version-3.0/platform/services/pubsub.md @@ -59,7 +59,7 @@ async function defaultRouteInit({ DicomMetadataStore.EVENTS.SERIES_ADDED, ({ StudyInstanceUID }) => { const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID); - HangingProtocolService.run(studyMetadata); + HangingProtocolService.run(studyMetadata, DisplaySetService.getActiveDisplaySets()); } ); unsubscriptions.push(seriesAddedUnsubscribe); diff --git a/platform/viewer/src/components/ViewportGrid.tsx b/platform/viewer/src/components/ViewportGrid.tsx index e57d633ce..8416daa6c 100644 --- a/platform/viewer/src/components/ViewportGrid.tsx +++ b/platform/viewer/src/components/ViewportGrid.tsx @@ -26,12 +26,12 @@ function ViewerViewportGrid(props) { return; } - const [ - matchDetails, + const { + viewportMatchDetails, hpAlreadyApplied, - ] = HangingProtocolService.getState(); + } = HangingProtocolService.getMatchDetails(); - if (!matchDetails.length) { + if (!viewportMatchDetails.length) { return; } @@ -43,24 +43,24 @@ function ViewerViewportGrid(props) { } // if current viewport doesn't have a match - if (matchDetails[i] === undefined) return; + if (viewportMatchDetails[i] === undefined) { + return; + } - const { displaySetsInfo, viewportOptions } = matchDetails[i]; + const { displaySetsInfo, viewportOptions } = viewportMatchDetails[i]; const displaySetUIDsToHang = []; const displaySetUIDsToHangOptions = []; - displaySetsInfo.forEach(({ SeriesInstanceUID, displaySetOptions }) => { - const matchingDisplaySet = availableDisplaySets.find(ds => { - return ds.SeriesInstanceUID === SeriesInstanceUID; - }); + displaySetsInfo.forEach( + ({ displaySetInstanceUID, displaySetOptions }) => { + if (!displaySetInstanceUID) { + return; + } - if (!matchingDisplaySet) { - return; + displaySetUIDsToHang.push(displaySetInstanceUID); + displaySetUIDsToHangOptions.push(displaySetOptions); } - - displaySetUIDsToHang.push(matchingDisplaySet.displaySetInstanceUID); - displaySetUIDsToHangOptions.push(displaySetOptions); - }); + ); if (!displaySetUIDsToHang.length) { continue; @@ -113,10 +113,11 @@ function ViewerViewportGrid(props) { // Using Hanging protocol engine to match the displaySets useEffect(() => { - const { unsubscribe } = DisplaySetService.subscribe( - DisplaySetService.EVENTS.DISPLAY_SETS_CHANGED, - activeDisplaySets => { - updateDisplaySetsForViewports(activeDisplaySets); + const { unsubscribe } = HangingProtocolService.subscribe( + HangingProtocolService.EVENTS.PROTOCOL_CHANGED, + () => { + const displaySets = DisplaySetService.getActiveDisplaySets(); + updateDisplaySetsForViewports(displaySets); } ); @@ -369,6 +370,11 @@ function _getViewportComponent(displaySets, viewportComponents) { const SOPClassHandlerId = displaySets[0].SOPClassHandlerId; for (let i = 0; i < viewportComponents.length; i++) { + if (!viewportComponents[i]) + throw new Error('viewport components not defined'); + if (!viewportComponents[i].displaySetsToDisplay) { + throw new Error('displaySetsToDisplay is null'); + } if ( viewportComponents[i].displaySetsToDisplay.includes(SOPClassHandlerId) ) { @@ -376,6 +382,7 @@ function _getViewportComponent(displaySets, viewportComponents) { return component; } } + throw new Error(`No display set handler for ${SOPClassHandlerId}`); } export default ViewerViewportGrid; diff --git a/platform/viewer/src/routes/Mode/Mode.tsx b/platform/viewer/src/routes/Mode/Mode.tsx index f2e164bbf..d49606849 100644 --- a/platform/viewer/src/routes/Mode/Mode.tsx +++ b/platform/viewer/src/routes/Mode/Mode.tsx @@ -9,24 +9,26 @@ import { useQuery } from '@hooks'; import ViewportGrid from '@components/ViewportGrid'; import Compose from './Compose'; -async function defaultRouteInit({ - servicesManager, - studyInstanceUIDs, - dataSource, -}) { +/** + * Initialize the route. + * + * @param props.servicesManager to read services from + * @param props.studyInstanceUIDs for a list of studies to read + * @param props.dataSource to read the data from + * @returns array of subscriptions to cancel + */ +function defaultRouteInit({ servicesManager, studyInstanceUIDs, dataSource }) { const { DisplaySetService, HangingProtocolService, } = servicesManager.services; const unsubscriptions = []; - // TODO: This should be baked into core, not manual? - // DisplaySetService would wire this up? const { unsubscribe: instanceAddedUnsubscribe, } = DicomMetadataStore.subscribe( DicomMetadataStore.EVENTS.INSTANCES_ADDED, - ({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) => { + function({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) { const seriesMetadata = DicomMetadataStore.getSeries( StudyInstanceUID, SeriesInstanceUID @@ -38,19 +40,43 @@ async function defaultRouteInit({ unsubscriptions.push(instanceAddedUnsubscribe); - const { unsubscribe: seriesAddedUnsubscribe } = DicomMetadataStore.subscribe( - DicomMetadataStore.EVENTS.SERIES_ADDED, - ({ StudyInstanceUID, madeInClient }) => { - const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID); - if (!madeInClient) { - HangingProtocolService.run(studyMetadata); - } - } + const allRetrieves = studyInstanceUIDs.map(StudyInstanceUID => + dataSource.retrieve.series.metadata({ StudyInstanceUID }) ); - unsubscriptions.push(seriesAddedUnsubscribe); - studyInstanceUIDs.forEach(StudyInstanceUID => { - dataSource.retrieve.series.metadata({ StudyInstanceUID }); + // The hanging protocol matching service is fairly expensive to run multiple + // times, and doesn't allow partial matches to be made (it will simply fail + // to display anything if a required match fails), so we wait here until all metadata + // is retrieved (which will synchronously trigger the display set creation) + // until we run the hanging protocol matching service. + + Promise.allSettled(allRetrieves).then(() => { + const displaySets = DisplaySetService.getActiveDisplaySets(); + + if (!displaySets || !displaySets.length) { + return; + } + + const studyMap = {}; + + // Prior studies don't quite work properly yet, but the studies list + // is at least being generated and passed in. + const studies = displaySets.reduce((prev, curr) => { + const { StudyInstanceUID } = curr; + if (!studyMap[StudyInstanceUID]) { + const study = DicomMetadataStore.getStudy(StudyInstanceUID); + studyMap[StudyInstanceUID] = study; + prev.push(study); + } + return prev; + }, []); + + // The assumption is that the display set at position 0 is the first + // study being displayed, and is thus the "active" study. + const activeStudy = studies[0]; + + // run the hanging protocol matching service on the displaySets + HangingProtocolService.run({ studies, activeStudy, displaySets }); }); return unsubscriptions; @@ -236,7 +262,7 @@ export default function ModeRoute({ }); } - return await defaultRouteInit({ + return defaultRouteInit({ servicesManager, studyInstanceUIDs, dataSource, From a16d7e140219fb29fc13b160f4635c0e2df81553 Mon Sep 17 00:00:00 2001 From: Alireza Date: Thu, 8 Sep 2022 09:03:37 -0400 Subject: [PATCH 07/56] fix: bump packages for crosshairs to get fixed (#2917) --- extensions/cornerstone-dicom-sr/package.json | 4 +-- extensions/cornerstone/package.json | 6 ++-- extensions/measurement-tracking/package.json | 4 +-- yarn.lock | 36 ++++++++------------ 4 files changed, 21 insertions(+), 29 deletions(-) diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index 88bbe9f87..44f1833ae 100644 --- a/extensions/cornerstone-dicom-sr/package.json +++ b/extensions/cornerstone-dicom-sr/package.json @@ -45,7 +45,7 @@ "dependencies": { "@babel/runtime": "7.16.3", "classnames": "^2.2.6", - "@cornerstonejs/core": "^0.14.5", - "@cornerstonejs/tools": "^0.21.8" + "@cornerstonejs/core": "^0.15.3", + "@cornerstonejs/tools": "^0.23.3" } } diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index 848df6e81..acf277b44 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -43,9 +43,9 @@ }, "dependencies": { "@babel/runtime": "7.17.9", - "@cornerstonejs/core": "^0.14.5", - "@cornerstonejs/streaming-image-volume-loader": "^0.4.16", - "@cornerstonejs/tools": "^0.21.8", + "@cornerstonejs/core": "^0.15.3", + "@cornerstonejs/streaming-image-volume-loader": "^0.4.23", + "@cornerstonejs/tools": "^0.23.3", "@kitware/vtk.js": "^24.18.7", "dom-to-image": "^2.6.0", "lodash.debounce": "4.0.8", diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index fd788789b..b127ac4a2 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -32,8 +32,8 @@ "peerDependencies": { "@ohif/core": "^3.0.0", "classnames": "^2.2.6", - "@cornerstonejs/core": "^0.14.4", - "@cornerstonejs/tools": "^0.21.7", + "@cornerstonejs/core": "^0.15.3", + "@cornerstonejs/tools": "^0.23.3", "@ohif/extension-cornerstone-dicom-sr": "^3.0.0", "dcmjs": "^0.24.5", "prop-types": "^15.6.2", diff --git a/yarn.lock b/yarn.lock index 5794da345..ecce22712 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2298,36 +2298,28 @@ resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjpeg/-/codec-openjpeg-0.1.1.tgz#5bd1c52a33a425299299e970312731fa0cc2711b" integrity sha512-HOMMOLV6xy8O/agNGGvrl0a8DwShpBvWxAzEzv2pqq12d3r5z/3MyIgNA3Oj/8bIBVvvVXxh9RX7rMDRHJdowg== -"@cornerstonejs/core@^0.14.4": - version "0.14.4" - resolved "https://registry.npmjs.org/@cornerstonejs/core/-/core-0.14.4.tgz#bb98dfd5cda0328fd921bd066e5465109e8aaaf5" - integrity sha512-l/Lb4Q5LoanpythYRUVRmKxqzORg101ILFXGldqQh1U7iRT7SkPf1HBxPLlqMd5nX2P5FighpBNqnlKszUxZ1g== +"@cornerstonejs/core@^0.15.3": + version "0.15.3" + resolved "https://registry.npmjs.org/@cornerstonejs/core/-/core-0.15.3.tgz#ab7a960fe74641c0f183b391f0c11b6f353af248" + integrity sha512-W6SiJfN3BvC8JWpUnWJzRFbQfP45qU/myKrum7kUyNyvwxyFnAaENYFpnq2FXCUS24HYRXCp4uc+C4dHDQpAFA== dependencies: detect-gpu "^4.0.7" lodash.clonedeep "4.5.0" -"@cornerstonejs/core@^0.14.5": - version "0.14.5" - resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-0.14.5.tgz#a2a382593e84d702233ac4ee442a952348b6bc4e" - integrity sha512-i2btN00iqC9oa/cWSWCaIQQw9pBc9cNxYEI9sm+L7HB25MvOsBnq1qjUx8EV3UrcMNKdUVVBqRsNEjc2PAkzBA== +"@cornerstonejs/streaming-image-volume-loader@^0.4.23": + version "0.4.23" + resolved "https://registry.npmjs.org/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.4.23.tgz#77d8d0de03ea7343ff73af7b98ec2035212c1215" + integrity sha512-lsRLqE4MDzxU/p5EF1JkCQuxWECdg7W4JRkRmiGri8HG89NtrESh/bomk/G1Z0Yx5gqb2umHZh+5RhgvQYu4Ag== dependencies: - detect-gpu "^4.0.7" - lodash.clonedeep "4.5.0" - -"@cornerstonejs/streaming-image-volume-loader@^0.4.16": - version "0.4.16" - resolved "https://registry.npmjs.org/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.4.16.tgz#5a26c7d07a19da3353b4e4cdfbf11eb001f5ab92" - integrity sha512-hU3W5VyDMGBtNdglqRF+QDhEhblSrnY44ZZkMwichQyUc0wRxk6y2KkZKqgh+n8ZaGVedS5kcDr98gy0ThpTLw== - dependencies: - "@cornerstonejs/core" "^0.14.4" + "@cornerstonejs/core" "^0.15.3" cornerstone-wado-image-loader "^4.2.1" -"@cornerstonejs/tools@^0.21.8": - version "0.21.8" - resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-0.21.8.tgz#fbe4d20a2f28b05a1516c3e925c43520988a2dcb" - integrity sha512-S4CkC8AHACfnQKIL43Vj35xiKiGKT8oRrRj+AT9kMeR7PdgbOXezZoYRBRSddj/vZTaOs9pV/hzmVDWw5ThOHg== +"@cornerstonejs/tools@^0.23.3": + version "0.23.3" + resolved "https://registry.npmjs.org/@cornerstonejs/tools/-/tools-0.23.3.tgz#4fde6b4218605eec4ba21ffe4ec25a75f48f1da1" + integrity sha512-j2nbwVQDihSBsbDhSa0tSmqFoqhNbx9w4TX2szXW6toqT2jgshwZQ4c3/UCWr5wBRBhKDe/gVRlU9IfjJSt89A== dependencies: - "@cornerstonejs/core" "^0.14.5" + "@cornerstonejs/core" "^0.15.3" lodash.clonedeep "4.5.0" lodash.get "^4.4.2" From 83264bb5a553d65257fb93119c0bf3b3985284dc Mon Sep 17 00:00:00 2001 From: Alireza Date: Thu, 8 Sep 2022 14:42:25 -0400 Subject: [PATCH 08/56] fix: imagScrollbar should not stop cine if not playing (#2920) --- .../src/Viewport/Overlays/ViewportImageScrollbar.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx b/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx index b061b60d0..9e978cd91 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx +++ b/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx @@ -25,9 +25,13 @@ function CornerstoneImageScrollbar({ viewportId ); - // on image scrollbar change, stop the CINE if it is playing - CineService.stopClip(element); - CineService.setCine({ id: viewportIndex, isPlaying: false }); + const { isCineEnabled } = CineService.getState(); + + if (isCineEnabled) { + // on image scrollbar change, stop the CINE if it is playing + CineService.stopClip(element); + CineService.setCine({ id: viewportIndex, isPlaying: false }); + } csToolsUtils.jumpToSlice(viewport.element, { imageIndex, From aec48b065c82931e9af606d27e545dbc67cbb954 Mon Sep 17 00:00:00 2001 From: Alireza Date: Thu, 8 Sep 2022 14:42:37 -0400 Subject: [PATCH 09/56] fix: total dose unit (#2921) --- extensions/tmtv/src/Panels/PanelPetSUV.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/tmtv/src/Panels/PanelPetSUV.tsx b/extensions/tmtv/src/Panels/PanelPetSUV.tsx index 0666f90a9..7ba238fe1 100644 --- a/extensions/tmtv/src/Panels/PanelPetSUV.tsx +++ b/extensions/tmtv/src/Panels/PanelPetSUV.tsx @@ -176,7 +176,7 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) { }} /> Date: Thu, 8 Sep 2022 22:02:20 -0400 Subject: [PATCH 10/56] fix: left right hotkeys and remove non working ones (#2922) * fix: constant should be enums in cs3d * remove non working hotkeys for now --- extensions/cornerstone-dicom-sr/package.json | 4 +-- extensions/cornerstone/package.json | 4 +-- extensions/cornerstone/src/commandsModule.js | 19 ++++++++++ extensions/cornerstone/src/index.tsx | 2 -- .../src/services/ViewportService/Viewport.ts | 4 +-- .../src/utils/getCornerstoneOrientation.ts | 14 ++++---- extensions/dicom-pdf/src/index.tsx | 24 ++++++------- extensions/dicom-video/src/index.tsx | 36 +++++++------------ extensions/measurement-tracking/package.json | 4 +-- extensions/measurement-tracking/src/index.tsx | 21 ----------- platform/core/src/defaults/hotkeyBindings.js | 24 ++++++------- .../platform/extensions/modules/utility.md | 1 - platform/viewer/public/config/aws.js | 20 +++++------ platform/viewer/public/config/default.js | 23 +++++++----- platform/viewer/public/config/demo.js | 20 +++++------ .../viewer/public/config/dicomweb_relative.js | 20 +++++------ platform/viewer/public/config/e2e.js | 20 +++++------ platform/viewer/public/config/local_static.js | 20 +++++------ platform/viewer/public/config/netlify.js | 20 +++++------ .../viewer/public/config/public_dicomweb.js | 20 +++++------ yarn.lock | 18 +++++++--- 21 files changed, 168 insertions(+), 170 deletions(-) diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json index 44f1833ae..f921626e0 100644 --- a/extensions/cornerstone-dicom-sr/package.json +++ b/extensions/cornerstone-dicom-sr/package.json @@ -45,7 +45,7 @@ "dependencies": { "@babel/runtime": "7.16.3", "classnames": "^2.2.6", - "@cornerstonejs/core": "^0.15.3", - "@cornerstonejs/tools": "^0.23.3" + "@cornerstonejs/core": "^0.16.1", + "@cornerstonejs/tools": "^0.24.1" } } diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index acf277b44..9b6689f14 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -43,9 +43,9 @@ }, "dependencies": { "@babel/runtime": "7.17.9", - "@cornerstonejs/core": "^0.15.3", + "@cornerstonejs/core": "^0.16.1", "@cornerstonejs/streaming-image-volume-loader": "^0.4.23", - "@cornerstonejs/tools": "^0.23.3", + "@cornerstonejs/tools": "^0.24.1", "@kitware/vtk.js": "^24.18.7", "dom-to-image": "^2.6.0", "lodash.debounce": "4.0.8", diff --git a/extensions/cornerstone/src/commandsModule.js b/extensions/cornerstone/src/commandsModule.js index d03894480..46af1fac6 100644 --- a/extensions/cornerstone/src/commandsModule.js +++ b/extensions/cornerstone/src/commandsModule.js @@ -404,6 +404,17 @@ const commandsModule = ({ servicesManager }) => { viewport.render(); } }, + incrementActiveViewport: () => { + const { activeViewportIndex, viewports } = ViewportGridService.getState(); + const nextViewportIndex = (activeViewportIndex + 1) % viewports.length; + ViewportGridService.setActiveViewportIndex(nextViewportIndex); + }, + decrementActiveViewport: () => { + const { activeViewportIndex, viewports } = ViewportGridService.getState(); + const nextViewportIndex = + (activeViewportIndex - 1 + viewports.length) % viewports.length; + ViewportGridService.setActiveViewportIndex(nextViewportIndex); + }, }; const definitions = { @@ -432,6 +443,14 @@ const commandsModule = ({ servicesManager }) => { storeContexts: [], options: { rotation: -90 }, }, + incrementActiveViewport: { + commandFn: actions.incrementActiveViewport, + storeContexts: [], + }, + decrementActiveViewport: { + commandFn: actions.decrementActiveViewport, + storeContexts: [], + }, flipViewportHorizontal: { commandFn: actions.flipViewportHorizontal, storeContexts: [], diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx index 3e83998df..44e12fc95 100644 --- a/extensions/cornerstone/src/index.tsx +++ b/extensions/cornerstone/src/index.tsx @@ -3,7 +3,6 @@ import * as cornerstone from '@cornerstonejs/core'; import * as cornerstoneTools from '@cornerstonejs/tools'; import { Enums as cs3DEnums, - CONSTANTS, imageLoadPoolManager, imageRetrievalPoolManager, } from '@cornerstonejs/core'; @@ -119,7 +118,6 @@ const cornerstoneExtension = { name: 'core', exports: { Enums: cs3DEnums, - CONSTANTS, }, }, { diff --git a/extensions/cornerstone/src/services/ViewportService/Viewport.ts b/extensions/cornerstone/src/services/ViewportService/Viewport.ts index ecc1a21bb..f537ce123 100644 --- a/extensions/cornerstone/src/services/ViewportService/Viewport.ts +++ b/extensions/cornerstone/src/services/ViewportService/Viewport.ts @@ -1,4 +1,4 @@ -import { Types, Enums, CONSTANTS } from '@cornerstonejs/core'; +import { Types, Enums } from '@cornerstonejs/core'; import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode'; import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation'; import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType'; @@ -137,7 +137,7 @@ class ViewportInfo { if (viewportOptionsEntry.viewportType?.toLowerCase() === VOLUME) { orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation); } else { - orientation = CONSTANTS.ORIENTATION.AXIAL; + orientation = Enums.OrientationAxis.AXIAL; } if (!toolGroupId) { diff --git a/extensions/cornerstone/src/utils/getCornerstoneOrientation.ts b/extensions/cornerstone/src/utils/getCornerstoneOrientation.ts index ace5bcef3..398db9535 100644 --- a/extensions/cornerstone/src/utils/getCornerstoneOrientation.ts +++ b/extensions/cornerstone/src/utils/getCornerstoneOrientation.ts @@ -1,4 +1,4 @@ -import { CONSTANTS } from '@cornerstonejs/core'; +import { Enums } from '@cornerstonejs/core'; import { log } from '@ohif/core'; const AXIAL = 'axial'; @@ -7,16 +7,16 @@ const CORONAL = 'coronal'; export default function getCornerstoneOrientation( orientation: string -): CONSTANTS.ORIENTATION { +): Enums.OrientationAxis { switch (orientation.toLowerCase()) { case AXIAL: - return CONSTANTS.ORIENTATION.AXIAL; + return Enums.OrientationAxis.AXIAL; case SAGITTAL: - return CONSTANTS.ORIENTATION.SAGITTAL; + return Enums.OrientationAxis.SAGITTAL; case CORONAL: - return CONSTANTS.ORIENTATION.CORONAL; + return Enums.OrientationAxis.CORONAL; default: - log.wanr('Choosing default orientation: axial'); - return CONSTANTS.ORIENTATION.AXIAL; + log.wanr('Choosing acquisition plane orientation'); + return Enums.OrientationAxis.ACQUISITION; } } diff --git a/extensions/dicom-pdf/src/index.tsx b/extensions/dicom-pdf/src/index.tsx index dddcef665..6c35c4a5f 100644 --- a/extensions/dicom-pdf/src/index.tsx +++ b/extensions/dicom-pdf/src/index.tsx @@ -45,18 +45,18 @@ const dicomPDFExtension = { { name: 'dicom-pdf', component: ExtendedOHIFCornerstonePdfViewport }, ]; }, - getCommandsModule({ servicesManager }) { - return { - definitions: { - setToolActive: { - commandFn: () => null, - storeContexts: [], - options: {}, - }, - }, - defaultContext: 'ACTIVE_VIEWPORT::PDF', - }; - }, + // getCommandsModule({ servicesManager }) { + // return { + // definitions: { + // setToolActive: { + // commandFn: () => null, + // storeContexts: [], + // options: {}, + // }, + // }, + // defaultContext: 'ACTIVE_VIEWPORT::PDF', + // }; + // }, getSopClassHandlerModule, }; diff --git a/extensions/dicom-video/src/index.tsx b/extensions/dicom-video/src/index.tsx index 17b4e5dec..1bc3a3359 100644 --- a/extensions/dicom-video/src/index.tsx +++ b/extensions/dicom-video/src/index.tsx @@ -46,29 +46,19 @@ const dicomVideoExtension = { { name: 'dicom-video', component: ExtendedOHIFCornerstoneVideoViewport }, ]; }, - getCommandsModule({ servicesManager }) { - return { - definitions: { - setToolActive: { - commandFn: ({ toolName, element }) => { - if (!toolName) { - console.warn('No toolname provided to setToolActive command'); - } - - // Set same tool or alt tool - const toolAlias = _getToolAlias(toolName); - - cornerstoneTools.setToolActiveForElement(element, toolAlias, { - mouseButtonMask: 1, - }); - }, - storeContexts: [], - options: {}, - }, - }, - defaultContext: 'ACTIVE_VIEWPORT::VIDEO', - }; - }, + // getCommandsModule({ servicesManager }) { + // return { + // definitions: { + // setToolActive: { + // commandFn: ({ toolName, element }) => { + // }, + // storeContexts: [], + // options: {}, + // }, + // }, + // defaultContext: 'ACTIVE_VIEWPORT::VIDEO', + // }; + // }, getSopClassHandlerModule, }; diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index b127ac4a2..ea7911dbf 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -32,8 +32,8 @@ "peerDependencies": { "@ohif/core": "^3.0.0", "classnames": "^2.2.6", - "@cornerstonejs/core": "^0.15.3", - "@cornerstonejs/tools": "^0.23.3", + "@cornerstonejs/core": "^0.16.1", + "@cornerstonejs/tools": "^0.24.1", "@ohif/extension-cornerstone-dicom-sr": "^3.0.0", "dcmjs": "^0.24.5", "prop-types": "^15.6.2", diff --git a/extensions/measurement-tracking/src/index.tsx b/extensions/measurement-tracking/src/index.tsx index b24134ab0..27a489eb0 100644 --- a/extensions/measurement-tracking/src/index.tsx +++ b/extensions/measurement-tracking/src/index.tsx @@ -12,27 +12,6 @@ const measurementTrackingExtension = { getContextModule, getPanelModule, getViewportModule, - getCommandsModule({ servicesManager }) { - return { - definitions: { - setToolActive: { - commandFn: ({ toolName, element }) => { - if (!toolName) { - console.warn('No toolname provided to setToolActive command'); - } - - // Set same tool or alt tool - cornerstoneTools.setToolActiveForElement(element, toolName, { - mouseButtonMask: 1, - }); - }, - storeContexts: [], - options: {}, - }, - }, - defaultContext: 'ACTIVE_VIEWPORT::TRACKED', - }; - }, }; export default measurementTrackingExtension; diff --git a/platform/core/src/defaults/hotkeyBindings.js b/platform/core/src/defaults/hotkeyBindings.js index 2c7b5f9da..cd38c43c5 100644 --- a/platform/core/src/defaults/hotkeyBindings.js +++ b/platform/core/src/defaults/hotkeyBindings.js @@ -76,18 +76,18 @@ const bindings = [ keys: ['left'], isEditable: true, }, - { - commandName: 'nextViewportDisplaySet', - label: 'Next Series', - keys: ['pageup'], - isEditable: true, - }, - { - commandName: 'previousViewportDisplaySet', - label: 'Previous Series', - keys: ['pagedown'], - isEditable: true, - }, + // { + // commandName: 'nextViewportDisplaySet', + // label: 'Next Series', + // keys: ['pageup'], + // isEditable: true, + // }, + // { + // commandName: 'previousViewportDisplaySet', + // label: 'Previous Series', + // keys: ['pagedown'], + // isEditable: true, + // }, { commandName: 'nextImage', label: 'Next Image', diff --git a/platform/docs/docs/platform/extensions/modules/utility.md b/platform/docs/docs/platform/extensions/modules/utility.md index 1d0f8e674..d538a51d5 100644 --- a/platform/docs/docs/platform/extensions/modules/utility.md +++ b/platform/docs/docs/platform/extensions/modules/utility.md @@ -29,7 +29,6 @@ getUtilityModule({ servicesManager }) { name: 'core', exports: { Enums: cs3DEnums, - CONSTANTS, }, }, { diff --git a/platform/viewer/public/config/aws.js b/platform/viewer/public/config/aws.js index 4de6f2b5b..a4da1a57d 100644 --- a/platform/viewer/public/config/aws.js +++ b/platform/viewer/public/config/aws.js @@ -98,16 +98,16 @@ window.config = { { commandName: 'resetViewport', label: 'Reset', keys: ['space'] }, { commandName: 'nextImage', label: 'Next Image', keys: ['down'] }, { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] }, - { - commandName: 'previousViewportDisplaySet', - label: 'Previous Series', - keys: ['pagedown'], - }, - { - commandName: 'nextViewportDisplaySet', - label: 'Next Series', - keys: ['pageup'], - }, + // { + // commandName: 'previousViewportDisplaySet', + // label: 'Previous Series', + // keys: ['pagedown'], + // }, + // { + // commandName: 'nextViewportDisplaySet', + // label: 'Next Series', + // keys: ['pageup'], + // }, { commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] }, // ~ Window level presets { diff --git a/platform/viewer/public/config/default.js b/platform/viewer/public/config/default.js index eaf2947b6..703d5eef0 100644 --- a/platform/viewer/public/config/default.js +++ b/platform/viewer/public/config/default.js @@ -102,17 +102,22 @@ window.config = { { commandName: 'resetViewport', label: 'Reset', keys: ['space'] }, { commandName: 'nextImage', label: 'Next Image', keys: ['down'] }, { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] }, + // { + // commandName: 'previousViewportDisplaySet', + // label: 'Previous Series', + // keys: ['pagedown'], + // }, + // { + // commandName: 'nextViewportDisplaySet', + // label: 'Next Series', + // keys: ['pageup'], + // }, { - commandName: 'previousViewportDisplaySet', - label: 'Previous Series', - keys: ['pagedown'], + commandName: 'setToolActive', + commandOptions: { toolName: 'Zoom' }, + label: 'Zoom', + keys: ['z'], }, - { - commandName: 'nextViewportDisplaySet', - label: 'Next Series', - keys: ['pageup'], - }, - { commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] }, // ~ Window level presets { commandName: 'windowLevelPreset1', diff --git a/platform/viewer/public/config/demo.js b/platform/viewer/public/config/demo.js index f91c342e7..3faf66f5d 100644 --- a/platform/viewer/public/config/demo.js +++ b/platform/viewer/public/config/demo.js @@ -46,16 +46,16 @@ window.config = { { commandName: 'resetViewport', label: 'Reset', keys: ['space'] }, { commandName: 'nextImage', label: 'Next Image', keys: ['down'] }, { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] }, - { - commandName: 'previousViewportDisplaySet', - label: 'Previous Series', - keys: ['pagedown'], - }, - { - commandName: 'nextViewportDisplaySet', - label: 'Next Series', - keys: ['pageup'], - }, + // { + // commandName: 'previousViewportDisplaySet', + // label: 'Previous Series', + // keys: ['pagedown'], + // }, + // { + // commandName: 'nextViewportDisplaySet', + // label: 'Next Series', + // keys: ['pageup'], + // }, { commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] }, // ~ Window level presets { diff --git a/platform/viewer/public/config/dicomweb_relative.js b/platform/viewer/public/config/dicomweb_relative.js index 634d85385..a408d351e 100644 --- a/platform/viewer/public/config/dicomweb_relative.js +++ b/platform/viewer/public/config/dicomweb_relative.js @@ -99,16 +99,16 @@ window.config = { { commandName: 'resetViewport', label: 'Reset', keys: ['space'] }, { commandName: 'nextImage', label: 'Next Image', keys: ['down'] }, { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] }, - { - commandName: 'previousViewportDisplaySet', - label: 'Previous Series', - keys: ['pagedown'], - }, - { - commandName: 'nextViewportDisplaySet', - label: 'Next Series', - keys: ['pageup'], - }, + // { + // commandName: 'previousViewportDisplaySet', + // label: 'Previous Series', + // keys: ['pagedown'], + // }, + // { + // commandName: 'nextViewportDisplaySet', + // label: 'Next Series', + // keys: ['pageup'], + // }, { commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] }, // ~ Window level presets { diff --git a/platform/viewer/public/config/e2e.js b/platform/viewer/public/config/e2e.js index d166fdf28..f8a0373f6 100644 --- a/platform/viewer/public/config/e2e.js +++ b/platform/viewer/public/config/e2e.js @@ -117,16 +117,16 @@ window.config = { { commandName: 'resetViewport', label: 'Reset', keys: ['space'] }, { commandName: 'nextImage', label: 'Next Image', keys: ['down'] }, { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] }, - { - commandName: 'previousViewportDisplaySet', - label: 'Previous Series', - keys: ['pagedown'], - }, - { - commandName: 'nextViewportDisplaySet', - label: 'Next Series', - keys: ['pageup'], - }, + // { + // commandName: 'previousViewportDisplaySet', + // label: 'Previous Series', + // keys: ['pagedown'], + // }, + // { + // commandName: 'nextViewportDisplaySet', + // label: 'Next Series', + // keys: ['pageup'], + // }, { commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] }, // ~ Window level presets { diff --git a/platform/viewer/public/config/local_static.js b/platform/viewer/public/config/local_static.js index d377776a9..342b166b9 100644 --- a/platform/viewer/public/config/local_static.js +++ b/platform/viewer/public/config/local_static.js @@ -99,16 +99,16 @@ window.config = { { commandName: 'resetViewport', label: 'Reset', keys: ['space'] }, { commandName: 'nextImage', label: 'Next Image', keys: ['down'] }, { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] }, - { - commandName: 'previousViewportDisplaySet', - label: 'Previous Series', - keys: ['pagedown'], - }, - { - commandName: 'nextViewportDisplaySet', - label: 'Next Series', - keys: ['pageup'], - }, + // { + // commandName: 'previousViewportDisplaySet', + // label: 'Previous Series', + // keys: ['pagedown'], + // }, + // { + // commandName: 'nextViewportDisplaySet', + // label: 'Next Series', + // keys: ['pageup'], + // }, { commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] }, // ~ Window level presets { diff --git a/platform/viewer/public/config/netlify.js b/platform/viewer/public/config/netlify.js index 711f78b40..237704cbf 100644 --- a/platform/viewer/public/config/netlify.js +++ b/platform/viewer/public/config/netlify.js @@ -96,16 +96,16 @@ window.config = { { commandName: 'resetViewport', label: 'Reset', keys: ['space'] }, { commandName: 'nextImage', label: 'Next Image', keys: ['down'] }, { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] }, - { - commandName: 'previousViewportDisplaySet', - label: 'Previous Series', - keys: ['pagedown'], - }, - { - commandName: 'nextViewportDisplaySet', - label: 'Next Series', - keys: ['pageup'], - }, + // { + // commandName: 'previousViewportDisplaySet', + // label: 'Previous Series', + // keys: ['pagedown'], + // }, + // { + // commandName: 'nextViewportDisplaySet', + // label: 'Next Series', + // keys: ['pageup'], + // }, { commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] }, // ~ Window level presets { diff --git a/platform/viewer/public/config/public_dicomweb.js b/platform/viewer/public/config/public_dicomweb.js index d7cbf153b..61999fe82 100644 --- a/platform/viewer/public/config/public_dicomweb.js +++ b/platform/viewer/public/config/public_dicomweb.js @@ -50,16 +50,16 @@ window.config = { { commandName: 'resetViewport', label: 'Reset', keys: ['space'] }, { commandName: 'nextImage', label: 'Next Image', keys: ['down'] }, { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] }, - { - commandName: 'previousViewportDisplaySet', - label: 'Previous Series', - keys: ['pagedown'], - }, - { - commandName: 'nextViewportDisplaySet', - label: 'Next Series', - keys: ['pageup'], - }, + // { + // commandName: 'previousViewportDisplaySet', + // label: 'Previous Series', + // keys: ['pagedown'], + // }, + // { + // commandName: 'nextViewportDisplaySet', + // label: 'Next Series', + // keys: ['pageup'], + // }, { commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] }, // ~ Window level presets { diff --git a/yarn.lock b/yarn.lock index ecce22712..a541d4621 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2306,6 +2306,14 @@ detect-gpu "^4.0.7" lodash.clonedeep "4.5.0" +"@cornerstonejs/core@^0.16.1": + version "0.16.1" + resolved "https://registry.npmjs.org/@cornerstonejs/core/-/core-0.16.1.tgz#796016bae79950bd76cfb8aaf57bc7c3d131059c" + integrity sha512-cWu/NswDKcbdnheiJ8w0BhUL9960z9uL9MVV378f1/k3oh2lW3IIwSlUPk2oQfh9qDVH6MY89KjbKDvooDwvag== + dependencies: + detect-gpu "^4.0.7" + lodash.clonedeep "4.5.0" + "@cornerstonejs/streaming-image-volume-loader@^0.4.23": version "0.4.23" resolved "https://registry.npmjs.org/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.4.23.tgz#77d8d0de03ea7343ff73af7b98ec2035212c1215" @@ -2314,12 +2322,12 @@ "@cornerstonejs/core" "^0.15.3" cornerstone-wado-image-loader "^4.2.1" -"@cornerstonejs/tools@^0.23.3": - version "0.23.3" - resolved "https://registry.npmjs.org/@cornerstonejs/tools/-/tools-0.23.3.tgz#4fde6b4218605eec4ba21ffe4ec25a75f48f1da1" - integrity sha512-j2nbwVQDihSBsbDhSa0tSmqFoqhNbx9w4TX2szXW6toqT2jgshwZQ4c3/UCWr5wBRBhKDe/gVRlU9IfjJSt89A== +"@cornerstonejs/tools@^0.24.1": + version "0.24.1" + resolved "https://registry.npmjs.org/@cornerstonejs/tools/-/tools-0.24.1.tgz#d6c38af6167b515b3545b7dc2db43b34f6e43d30" + integrity sha512-X98m1MZahte1m+hd/J8oSUH5zxj+hv/7Stav3AHOUfyMPTm9tcl32bH87WpLK7cm9bLRzMNp23/6U1biEH2+Gg== dependencies: - "@cornerstonejs/core" "^0.15.3" + "@cornerstonejs/core" "^0.16.1" lodash.clonedeep "4.5.0" lodash.get "^4.4.2" From 0090605cf73537fc3607bdf10502d59f899c2be1 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Thu, 8 Sep 2022 22:09:28 -0400 Subject: [PATCH 11/56] fix: various type issues and error handlings (#2831) * fix(error):Fix a few error conditions, mostly around seriesNumber * apply few fixes --- .../src/Viewport/OHIFCornerstoneViewport.tsx | 2 +- modes/longitudinal/src/index.js | 17 +++++++++++----- platform/ui/src/Types.ts | 20 +++++++++++++++++++ .../components/StudyBrowser/StudyBrowser.tsx | 3 ++- .../ui/src/components/Thumbnail/Thumbnail.tsx | 3 ++- .../ThumbnailList/ThumbnailList.tsx | 14 ++++--------- .../ThumbnailTracked/ThumbnailTracked.tsx | 3 ++- .../ViewportActionBar/ViewportActionBar.tsx | 3 ++- platform/ui/src/index.js | 4 ++++ .../viewer/src/components/ViewportGrid.tsx | 3 ++- 10 files changed, 51 insertions(+), 21 deletions(-) create mode 100644 platform/ui/src/Types.ts diff --git a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx index dfdbf4d70..7c5120452 100644 --- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx +++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx @@ -419,7 +419,7 @@ OHIFCornerstoneViewport.propTypes = { displaySets: PropTypes.array.isRequired, dataSource: PropTypes.object.isRequired, viewportOptions: PropTypes.object, - displaySetOptions: PropTypes.arrayOf(PropTypes.object), + displaySetOptions: PropTypes.arrayOf(PropTypes.any), servicesManager: PropTypes.object.isRequired, onElementEnabled: PropTypes.func, // Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation diff --git a/modes/longitudinal/src/index.js b/modes/longitudinal/src/index.js index e902ba1ff..f3072cf2e 100644 --- a/modes/longitudinal/src/index.js +++ b/modes/longitudinal/src/index.js @@ -3,6 +3,10 @@ import toolbarButtons from './toolbarButtons.js'; import { id } from './id.js'; import initToolGroups from './initToolGroups.js'; +// Allow this mode by excluding non-imaging modalities such as SR, SEG +// Also, SM is not a simple imaging modalities, so exclude it. +const NON_IMAGE_MODALITIES = ['SM', 'ECG', 'SR', 'SEG']; + const ohif = { layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', @@ -44,7 +48,7 @@ const extensionDependencies = { '@ohif/extension-dicom-video': '^3.0.1', }; -function modeFactory({ modeConfiguration }) { +function modeFactory() { return { // TODO: We're using this as a route segment // We should not be. @@ -119,11 +123,14 @@ function modeFactory({ modeConfiguration }) { study: [], series: [], }, - isValidMode: ({ modalities }) => { + + isValidMode: function({ modalities }) { const modalities_list = modalities.split('\\'); - // Slide Microscopy modality not supported by basic mode yet - return !modalities_list.includes('SM'); + // Exclude non-image modalities + return !!modalities_list.filter( + modality => NON_IMAGE_MODALITIES.indexOf(modality) === -1 + ).length; }, routes: [ { @@ -131,7 +138,7 @@ function modeFactory({ modeConfiguration }) { /*init: ({ servicesManager, extensionManager }) => { //defaultViewerRouteInit },*/ - layoutTemplate: ({ location, servicesManager }) => { + layoutTemplate: () => { return { id: ohif.layout, props: { diff --git a/platform/ui/src/Types.ts b/platform/ui/src/Types.ts new file mode 100644 index 000000000..d6c8581e9 --- /dev/null +++ b/platform/ui/src/Types.ts @@ -0,0 +1,20 @@ +import PropTypes from 'prop-types'; + +/** + * StringNumber often comes back from DICOMweb for integer valued items. + */ +const StringNumber = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); + +/** + * StringArray often comes back from dcmjs for single valued strings that + * might have multiple values such as window level descriptions. + */ +const StringArray = PropTypes.oneOfType([PropTypes.string, PropTypes.array]); + +const ThumbnailType = PropTypes.oneOf([ + 'thumbnail', + 'thumbnailTracked', + 'thumbnailNoImage', +]); + +export { StringNumber, StringArray, ThumbnailType }; diff --git a/platform/ui/src/components/StudyBrowser/StudyBrowser.tsx b/platform/ui/src/components/StudyBrowser/StudyBrowser.tsx index d75313b76..5b1b31b0d 100644 --- a/platform/ui/src/components/StudyBrowser/StudyBrowser.tsx +++ b/platform/ui/src/components/StudyBrowser/StudyBrowser.tsx @@ -4,6 +4,7 @@ import classnames from 'classnames'; import { useTranslation } from 'react-i18next'; import { ButtonGroup, Button, StudyItem, ThumbnailList } from '../'; +import { StringNumber } from '../../Types'; const buttonClasses = 'text-white text-base border-none bg-black p-2 min-w-18'; const activeButtonClasses = 'bg-primary-main'; @@ -140,7 +141,7 @@ StudyBrowser.propTypes = { imageSrc: PropTypes.string, imageAltText: PropTypes.string, seriesDate: PropTypes.string, - seriesNumber: PropTypes.string, + seriesNumber: StringNumber, numInstances: PropTypes.number, description: PropTypes.string, componentType: PropTypes.oneOf([ diff --git a/platform/ui/src/components/Thumbnail/Thumbnail.tsx b/platform/ui/src/components/Thumbnail/Thumbnail.tsx index db0743cf2..176ea36a4 100644 --- a/platform/ui/src/components/Thumbnail/Thumbnail.tsx +++ b/platform/ui/src/components/Thumbnail/Thumbnail.tsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import { useDrag } from 'react-dnd'; import { Icon } from '../'; +import { StringNumber } from '../../Types'; /** * @@ -98,7 +99,7 @@ Thumbnail.propTypes = { }), imageAltText: PropTypes.string, description: PropTypes.string.isRequired, - seriesNumber: PropTypes.string.isRequired, + seriesNumber: StringNumber.isRequired, numInstances: PropTypes.number.isRequired, isActive: PropTypes.bool.isRequired, onClick: PropTypes.func.isRequired, diff --git a/platform/ui/src/components/ThumbnailList/ThumbnailList.tsx b/platform/ui/src/components/ThumbnailList/ThumbnailList.tsx index 75cb70013..46ff0ab94 100644 --- a/platform/ui/src/components/ThumbnailList/ThumbnailList.tsx +++ b/platform/ui/src/components/ThumbnailList/ThumbnailList.tsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Thumbnail, ThumbnailNoImage, ThumbnailTracked } from '../'; +import * as Types from '../../Types'; const ThumbnailList = ({ thumbnails, @@ -110,18 +111,11 @@ ThumbnailList.propTypes = { imageSrc: PropTypes.string, imageAltText: PropTypes.string, seriesDate: PropTypes.string, - seriesNumber: PropTypes.string, + seriesNumber: Types.StringNumber, numInstances: PropTypes.number, description: PropTypes.string, - componentType: PropTypes.oneOf([ - 'thumbnail', - 'thumbnailTracked', - 'thumbnailNoImage', - ]).isRequired, - viewportIdentificator: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.array, - ]), + componentType: Types.ThumbnailType.isRequired, + viewportIdentificator: Types.StringArray, isTracked: PropTypes.bool, /** * Data the thumbnail should expose to a receiving drop target. Use a matching diff --git a/platform/ui/src/components/ThumbnailTracked/ThumbnailTracked.tsx b/platform/ui/src/components/ThumbnailTracked/ThumbnailTracked.tsx index fd9d74014..2e9f5dcf0 100644 --- a/platform/ui/src/components/ThumbnailTracked/ThumbnailTracked.tsx +++ b/platform/ui/src/components/ThumbnailTracked/ThumbnailTracked.tsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import { Icon, Thumbnail, Tooltip } from '../'; +import { StringNumber } from '../../Types'; const ThumbnailTracked = ({ displaySetInstanceUID, @@ -137,7 +138,7 @@ ThumbnailTracked.propTypes = { imageSrc: PropTypes.string, imageAltText: PropTypes.string, description: PropTypes.string.isRequired, - seriesNumber: PropTypes.string.isRequired, + seriesNumber: StringNumber.isRequired, numInstances: PropTypes.number.isRequired, onClick: PropTypes.func.isRequired, onDoubleClick: PropTypes.func.isRequired, diff --git a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.tsx b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.tsx index 8ca11ae58..d7916bbb1 100644 --- a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.tsx +++ b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.tsx @@ -4,6 +4,7 @@ import classnames from 'classnames'; import { Icon, ButtonGroup, Button, Tooltip, CinePlayer } from '../'; import useOnClickOutside from '../../utils/useOnClickOutside'; import { useTranslation } from 'react-i18next'; +import { StringNumber } from '../../Types'; const classes = { infoHeader: 'text-base text-primary-light', @@ -336,7 +337,7 @@ ViewportActionBar.propTypes = { isTracked: PropTypes.bool.isRequired, isRehydratable: PropTypes.bool.isRequired, studyDate: PropTypes.string.isRequired, - currentSeries: PropTypes.string.isRequired, + currentSeries: StringNumber.isRequired, seriesDescription: PropTypes.string.isRequired, modality: PropTypes.string.isRequired, patientInformation: PropTypes.shape({ diff --git a/platform/ui/src/index.js b/platform/ui/src/index.js index 22661ea13..4d74012df 100644 --- a/platform/ui/src/index.js +++ b/platform/ui/src/index.js @@ -3,6 +3,9 @@ //export { utils }; /** CONTEXT/HOOKS */ +// Export types - need to do as two lines due to a bug in babel +import * as Types from './Types'; + export { useCine, CineProvider, @@ -104,3 +107,4 @@ export { export { getIcon, ICONS } from './components/Icon/getIcon'; export { BackgroundColor } from './pages/Colors/BackgroundColor'; export { ModalComponent } from './contextProviders/ModalComponent'; +export { Types }; diff --git a/platform/viewer/src/components/ViewportGrid.tsx b/platform/viewer/src/components/ViewportGrid.tsx index 8416daa6c..0bfb7fcb2 100644 --- a/platform/viewer/src/components/ViewportGrid.tsx +++ b/platform/viewer/src/components/ViewportGrid.tsx @@ -370,8 +370,9 @@ function _getViewportComponent(displaySets, viewportComponents) { const SOPClassHandlerId = displaySets[0].SOPClassHandlerId; for (let i = 0; i < viewportComponents.length; i++) { - if (!viewportComponents[i]) + if (!viewportComponents[i]) { throw new Error('viewport components not defined'); + } if (!viewportComponents[i].displaySetsToDisplay) { throw new Error('displaySetsToDisplay is null'); } From 6e3dc1e9d9a5ee225c47b7b49fc24fd53a38cc92 Mon Sep 17 00:00:00 2001 From: Ouwen Huang Date: Thu, 8 Sep 2022 22:18:17 -0400 Subject: [PATCH 12/56] fix: enabled multiple studies in local mode (#2889) --- extensions/default/src/DicomLocalDataSource/index.js | 2 +- platform/viewer/src/routes/Local/Local.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/default/src/DicomLocalDataSource/index.js b/extensions/default/src/DicomLocalDataSource/index.js index 54f40b400..faa2b95ae 100644 --- a/extensions/default/src/DicomLocalDataSource/index.js +++ b/extensions/default/src/DicomLocalDataSource/index.js @@ -25,7 +25,7 @@ function createDicomLocalApi(dicomLocalConfig) { const implementation = { initialize: ({ params, query }) => { const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params; - const queryStudyInstanceUIDs = query.get('StudyInstanceUIDs'); + const queryStudyInstanceUIDs = query.getAll('StudyInstanceUIDs'); const StudyInstanceUIDs = queryStudyInstanceUIDs || paramsStudyInstanceUIDs; diff --git a/platform/viewer/src/routes/Local/Local.tsx b/platform/viewer/src/routes/Local/Local.tsx index 9129c54e5..aba010614 100644 --- a/platform/viewer/src/routes/Local/Local.tsx +++ b/platform/viewer/src/routes/Local/Local.tsx @@ -62,7 +62,9 @@ function Local() { const onDrop = async acceptedFiles => { const studies = await filesToStudies(acceptedFiles, dataSource); // Todo: navigate to work list and let user select a mode - navigate(`/viewer/dicomlocal?StudyInstanceUIDs=${studies[0]}`); + let query = new URLSearchParams(); + studies.forEach(id=> query.append('StudyInstanceUIDs', id)) + navigate(`/viewer/dicomlocal?${decodeURIComponent(query.toString())}`); }; // Set body style From 1b30ff83a5cf5172018aeb42c65dc56d0b999b76 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Thu, 8 Sep 2022 22:22:51 -0400 Subject: [PATCH 13/56] fix: ohif cli bug for linking (#2892) * fix(cli):Make the linkage in the root node modules * fix(cli):Fix the index.tsx referencce --- platform/cli/src/commands/linkPackage.js | 5 ++++- platform/cli/templates/extension/dependencies.json | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/platform/cli/src/commands/linkPackage.js b/platform/cli/src/commands/linkPackage.js index 34a3b553a..96478b2e8 100644 --- a/platform/cli/src/commands/linkPackage.js +++ b/platform/cli/src/commands/linkPackage.js @@ -36,10 +36,13 @@ async function linkPackage(packageDir, options, addToConfig, keyword) { results = await execa(`yarn`, ['link']); // change directory to OHIF Platform root and execute yarn link - process.chdir(viewerDirectory); + process.chdir(`${viewerDirectory}/../..`); results = await execa(`yarn`, ['link', packageName]); console.log(results.stdout); + + // change directory to viewer packages and add the config item + process.chdir(viewerDirectory); addToConfig(packageName, { version }); } diff --git a/platform/cli/templates/extension/dependencies.json b/platform/cli/templates/extension/dependencies.json index 02970822d..8b6e193ec 100644 --- a/platform/cli/templates/extension/dependencies.json +++ b/platform/cli/templates/extension/dependencies.json @@ -42,6 +42,7 @@ "babel-plugin-inline-react-svg": "^2.0.1", "@babel/preset-env": "^7.5.0", "@babel/preset-react": "^7.0.0", + "@babel/preset-typescript": "^7.18.6", "babel-eslint": "^8.0.3", "babel-loader": "^8.0.0-beta.4", "clean-webpack-plugin": "^4.0.0", From f9449e97df0d412860004fe568a7f3c7e2331561 Mon Sep 17 00:00:00 2001 From: Alireza Date: Tue, 13 Sep 2022 08:57:22 -0400 Subject: [PATCH 14/56] fix: storybook with typescript (#2926) * fix storybook for typescript * fix babel --- babel.config.js | 4 +- package.json | 1 + platform/ui/.storybook/main.js | 19 ++- .../ui/.storybook/{preview.js => preview.tsx} | 5 +- .../__stories__/aboutModal.stories.mdx | 2 +- .../Button/__stories__/button.stories.mdx | 148 +++++++++--------- .../__stories__/buttonGroup.stories.mdx | 2 +- .../__stories__/cinePlayer.stories.mdx | 2 +- .../__stories__/contextMenu.stories.mdx | 6 +- .../__stories__/dateRange.stories.mdx | 4 +- .../Dialog/__stories__/dialog.stories.mdx | 2 +- .../Dropdown/__stories__/dropdown.stories.mdx | 4 +- .../Header/__stories__/Header.stories.mdx | 2 +- .../Icon/__stories__/icon.stories.mdx | 8 +- ...ton.stories.jsx => IconButton.stories.tsx} | 0 .../Input/__stories__/input.stories.mdx | 2 +- .../__stories__/layoutSelector.stories.mdx | 2 +- .../__stories__/measurementTable.stories.mdx | 4 +- .../__stories__/notification.stories.mdx | 2 +- .../__stories__/segmentationTable.stories.mdx | 4 +- .../__stories__/sidePanel.stories.mdx | 4 +- .../__stories__/splitButton.stories.mdx | 4 +- .../__stories__/studyBrowser.stories.mdx | 4 +- .../__stories__/studyItem.stories.mdx | 4 +- .../Viewport/__stories__/Viewport.stories.mdx | 36 ++--- ...ries.jsx => ViewportActionBar.stories.tsx} | 0 .../storybook/color/{color.jsx => color.tsx} | 0 platform/ui/src/storybook/colors.stories.mdx | 2 +- ...hor-list-item.jsx => anchor-list-item.tsx} | 0 .../footer/{footer.jsx => footer.tsx} | 0 .../heading/{heading.jsx => heading.tsx} | 0 .../components/image/{image.jsx => image.tsx} | 0 .../components/{index.jsx => index.tsx} | 0 ...{link-component.jsx => link-component.tsx} | 0 .../{section-name.jsx => section-name.tsx} | 0 .../ui/src/storybook/contribute.stories.mdx | 6 +- ...nt-story.js => create-component-story.tsx} | 0 .../header/{header.jsx => header.tsx} | 0 38 files changed, 153 insertions(+), 130 deletions(-) rename platform/ui/.storybook/{preview.js => preview.tsx} (90%) rename platform/ui/src/components/IconButton/{IconButton.stories.jsx => IconButton.stories.tsx} (100%) rename platform/ui/src/components/ViewportActionBar/{ViewportActionBar.stories.jsx => ViewportActionBar.stories.tsx} (100%) rename platform/ui/src/storybook/color/{color.jsx => color.tsx} (100%) rename platform/ui/src/storybook/components/anchor-list-item/{anchor-list-item.jsx => anchor-list-item.tsx} (100%) rename platform/ui/src/storybook/components/footer/{footer.jsx => footer.tsx} (100%) rename platform/ui/src/storybook/components/heading/{heading.jsx => heading.tsx} (100%) rename platform/ui/src/storybook/components/image/{image.jsx => image.tsx} (100%) rename platform/ui/src/storybook/components/{index.jsx => index.tsx} (100%) rename platform/ui/src/storybook/components/link-component/{link-component.jsx => link-component.tsx} (100%) rename platform/ui/src/storybook/components/section-name/{section-name.jsx => section-name.tsx} (100%) rename platform/ui/src/storybook/functions/{create-component-story.js => create-component-story.tsx} (100%) rename platform/ui/src/storybook/header/{header.jsx => header.tsx} (100%) diff --git a/babel.config.js b/babel.config.js index 3650eee2d..de2e1b498 100644 --- a/babel.config.js +++ b/babel.config.js @@ -8,8 +8,10 @@ module.exports = { ], plugins: [ 'inline-react-svg', - '@babel/plugin-proposal-class-properties', + ['@babel/plugin-proposal-class-properties', { loose: true }], '@babel/plugin-transform-typescript', + ['@babel/plugin-proposal-private-property-in-object', { loose: true }], + ['@babel/plugin-proposal-private-methods', { loose: true }], ], env: { test: { diff --git a/package.json b/package.json index 6aba7aec6..38594fb5a 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "devDependencies": { "@babel/core": "^7.17.8", "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-object-rest-spread": "^7.17.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.16.7", diff --git a/platform/ui/.storybook/main.js b/platform/ui/.storybook/main.js index bb96ab74d..399f439b4 100644 --- a/platform/ui/.storybook/main.js +++ b/platform/ui/.storybook/main.js @@ -2,7 +2,11 @@ const path = require('path'); module.exports = { stories: ['../src/**/*.stories.@(mdx)'], - addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-google-analytics'], + addons: [ + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/addon-google-analytics', + ], core: { builder: 'webpack5', }, @@ -11,11 +15,22 @@ module.exports = { // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION' // You can change the configuration based on that. // 'PRODUCTION' is used when building the static version of storybook. + // config.module.rules[0].use[0].options.plugins[1] = [ // '@babel/plugin-proposal-class-properties', // { loose: true }, // ]; + // config.module.rules[0].use[0].options.plugins[3] = [ + // '@babel/plugin-proposal-private-methods', + // { loose: true }, + // ]; + + // config.module.rules[0].use[0].options.plugins[4] = [ + // '@babel/plugin-proposal-private-property-in-object', + // { loose: true }, + // ]; + // Make whatever fine-grained changes you need config.module.rules.push({ test: /\.m?js/, @@ -30,8 +45,6 @@ module.exports = { ); fileLoaderRule.exclude = /\.svg$/; - // console.log(JSON.stringify(config.module.rules, null, 2)); - config.module.rules.push({ test: /\.svg$/, use: [ diff --git a/platform/ui/.storybook/preview.js b/platform/ui/.storybook/preview.tsx similarity index 90% rename from platform/ui/.storybook/preview.js rename to platform/ui/.storybook/preview.tsx index 2fde6ba92..111a3276c 100644 --- a/platform/ui/.storybook/preview.js +++ b/platform/ui/.storybook/preview.tsx @@ -1,6 +1,6 @@ -import { parameters } from '@storybook/addon-docs/dist/esm/frameworks/react/config'; +import React from 'react'; import { addParameters } from '@storybook/react'; -import { DocsPage, DocsContainer } from '@storybook/addon-docs/blocks'; +import { DocsPage, DocsContainer } from '@storybook/addon-docs'; import { Heading, SectionName, @@ -15,7 +15,6 @@ import './custom.css'; // https://github.com/mondaycom/monday-ui-react-core/tree/master/.storybook addParameters({ docs: { - ...parameters.docs, inlineStories: true, container: ({ children, context }) => ( {children} diff --git a/platform/ui/src/components/AboutModal/__stories__/aboutModal.stories.mdx b/platform/ui/src/components/AboutModal/__stories__/aboutModal.stories.mdx index 5e48f2b23..3e5127be6 100644 --- a/platform/ui/src/components/AboutModal/__stories__/aboutModal.stories.mdx +++ b/platform/ui/src/components/AboutModal/__stories__/aboutModal.stories.mdx @@ -20,7 +20,7 @@ export const aboutTemplate = args => ( - [Overview](#overview) diff --git a/platform/ui/src/components/Button/__stories__/button.stories.mdx b/platform/ui/src/components/Button/__stories__/button.stories.mdx index 9e90e3a8d..bb62b5927 100644 --- a/platform/ui/src/components/Button/__stories__/button.stories.mdx +++ b/platform/ui/src/components/Button/__stories__/button.stories.mdx @@ -14,22 +14,20 @@ export const argTypes = { export const buttonTemplate = createComponentTemplate(Button); - + - [Overview](#overview) - [Props](#props) - [Usage](#usage) - [Contribute](#contribute) - ## Overview -You can use the button component to create a button. It can be used in different ways, the default -button is a simple button with a text. +You can use the button component to create a button. It can be used in different +ways, the default button is a simple button with a text. - + {buttonTemplate.bind({})} @@ -39,13 +37,14 @@ button is a simple button with a text. ## Usage -### Variants -There can be different variants of buttons: `text`, `outlined`, -`contained`, and `disabled`. +### Variants + +There can be different variants of buttons: `text`, `outlined`, `contained`, and +`disabled`. - +
@@ -55,13 +54,13 @@ There can be different variants of buttons: `text`, `outlined`, - ### Colors -There are different colors for the button: `default`, `primary`, `secondary`, `white`, `black`, `inherit`, `light`. +There are different colors for the button: `default`, `primary`, `secondary`, +`white`, `black`, `inherit`, `light`. - +
@@ -75,96 +74,105 @@ There are different colors for the button: `default`, `primary`, `secondary`, `w ### Mixing props + You can mix different props together to create a button. - - + + ### Disabled + You can disable the button by setting the variant to `disabled`. - - + + - ### Start/End Icons + You can add an icon to the start of the button. It accepts an icon component. - - - {() => { - // svg icon for github - const Github = () => { - return ( - - - - ) - } - return ; - }} + + {() => { + // svg icon for github + const Github = () => { + return ( + + + + ); + }; + return ; + }} }} End Icon is the same as start icon, but for the end of the button. - - - {() => { - // svg icon for github - const Github = () => { - return ( - - - - ) - } - return ; - }} + + {() => { + // svg icon for github + const Github = () => { + return ( + + + + ); + }; + return ( + + ); + }} }} - ### Full width + You can make the button full width by setting the `fullWidth` prop to `true`. - - + + ## Contribute -