diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js b/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js index 5132638a0..160410bd3 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js @@ -79,6 +79,14 @@ const testProtocol = { numberOfPriorsReferenced: -1, }; +function testProtocolGenerator({ servicesManager }) { + servicesManager.services.TestService.toCall(); + + return { + protocol: testProtocol, + }; +} + const studyMatch = { StudyInstanceUID: 'studyMatch', StudyDescription: 'A PETCT study type', @@ -107,41 +115,80 @@ const displaySet3 = { const studyMatchDisplaySets = [displaySet3, displaySet2, displaySet1]; +function checkHpsBestMatch(hps) { + hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets }); + const { hpAlreadyApplied, viewportMatchDetails } = hps.getMatchDetails(); + expect(hpAlreadyApplied).toMatchObject(new Map([[0, false]])); + expect(viewportMatchDetails.size).toBe(1); + expect(viewportMatchDetails.get(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: {}, + }, + ], + }); +} + describe('HangingProtocolService', () => { + const mockedFunction = jest.fn(); const commandsManager = {}; - const hps = new HangingProtocolServiceClass(commandsManager); + const servicesManager = { + services: { + TestService: { + toCall: mockedFunction, + }, + }, + }; + const hps = new HangingProtocolServiceClass(commandsManager, servicesManager); let initialScaling; - beforeAll(() => { - hps.addProtocol(testProtocol.id, testProtocol); + afterEach(() => { + mockedFunction.mockClear(); }); - it('has one protocol', () => { - expect(hps.getProtocols().length).toBe(1); + describe('with a static protocol', () => { + beforeAll(() => { + hps.addProtocol(testProtocol.id, testProtocol); + }); + + it('has one protocol', () => { + expect(hps.getProtocols().length).toBe(1); + }); + + describe('run', () => { + it('matches best image match', () => { + checkHpsBestMatch(hps); + }); + }); }); - describe('run', () => { - it('matches best image match', () => { - hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets }); - const { hpAlreadyApplied, viewportMatchDetails } = hps.getMatchDetails(); - expect(hpAlreadyApplied).toMatchObject(new Map([[0, false]])); - expect(viewportMatchDetails.size).toBe(1); - expect(viewportMatchDetails.get(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: {}, - }, - ], + describe('with protocol generator', () => { + beforeAll(() => { + hps.addProtocol(testProtocol.id, testProtocolGenerator); + }); + + it('has one protocol', () => { + expect(hps.getProtocols().length).toBe(1); + }); + + describe('run', () => { + it('matches best image match', () => { + checkHpsBestMatch(hps); + }); + + it('uses services manager', () => { + hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets }); + expect(mockedFunction).toHaveBeenCalledTimes(1); }); }); }); diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts index 91b6c108a..4d650ee1a 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts @@ -16,7 +16,7 @@ const EVENTS = { 'event::hanging_protocol_applied_for_viewport', }; -type Protocol = HangingProtocol.Protocol; +type Protocol = HangingProtocol.Protocol | HangingProtocol.ProtocolGenerator; class HangingProtocolService { studies: StudyMetadata[]; @@ -180,7 +180,21 @@ class HangingProtocolService { public getProtocolById(id: string): HangingProtocol.Protocol { const protocol = this.protocols.get(id); - return protocol; + if (protocol instanceof Function) { + try { + const { protocol: generatedProtocol } = this._getProtocolFromGenerator( + protocol + ); + + return generatedProtocol; + } catch (error) { + console.warn( + `Error while executing protocol generator for protocol ${id}: ${error}` + ); + } + } else { + return this._validateProtocol(protocol); + } } /** @@ -434,6 +448,23 @@ class HangingProtocolService { return protocol; } + private _getProtocolFromGenerator( + protocolGenerator: HangingProtocol.ProtocolGenerator + ): { + protocol: HangingProtocol.Protocol; + } { + const { protocol } = protocolGenerator({ + servicesManager: this._servicesManager, + commandsManager: this._commandsManager, + }); + + const validatedProtocol = this._validateProtocol(protocol); + + return { + protocol: validatedProtocol, + }; + } + getViewportsRequireUpdate(viewportIndex, displaySetInstanceUID) { const newDisplaySetInstanceUID = displaySetInstanceUID; const protocol = this.protocol; diff --git a/platform/core/src/types/HangingProtocol.ts b/platform/core/src/types/HangingProtocol.ts index de29c0ff0..a513e308e 100644 --- a/platform/core/src/types/HangingProtocol.ts +++ b/platform/core/src/types/HangingProtocol.ts @@ -151,6 +151,9 @@ type Protocol = { syncDataForViewports?: boolean; }; +type ProtocolGenerator = ({ servicesManager: any, commandsManager: any }) => { + protocol: Protocol; +}; export type { SetProtocolOptions, @@ -171,6 +174,6 @@ export type { DisplaySetInfo, GlobalProtocolOptions, ViewportSpecificProtocolOptions, - DisplaySetAndViewportOptions - + DisplaySetAndViewportOptions, + ProtocolGenerator, }; diff --git a/platform/docs/docs/platform/services/data/HangingProtocolService.md b/platform/docs/docs/platform/services/data/HangingProtocolService.md index 1e50ede0b..c43fd6fd8 100644 --- a/platform/docs/docs/platform/services/data/HangingProtocolService.md +++ b/platform/docs/docs/platform/services/data/HangingProtocolService.md @@ -78,7 +78,20 @@ do not overlap, with the suggested id being `${moduleId}.${simpleName}`. The 'default' name is used as the hanging protocol id when no other protocol applies, and can be set as the last module listed containing 'default'. -See the typescript definitions for more details on the structure. +A hanging protocol can also be defined with a generator. +A generator is a function we can write this way: + +```ts +function protocolGenerator({ servicesManager, commandsManager }) { + // Some computations using services and commands ... + + return { + protocol: generatedProtocol + } +} +``` + +See the typescript definitions for more details on the structure of protocols. ## Custom Attribute In some situations, you might want to match based on a custom attribute and not the DICOM tags. For instance, diff --git a/yarn.lock b/yarn.lock index ce688dc90..7a52ed704 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2405,6 +2405,14 @@ detect-gpu "^4.0.45" lodash.clonedeep "4.5.0" +"@cornerstonejs/core@^0.25.1": + version "0.25.1" + resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-0.25.1.tgz#7e5c70858018d99417b61965ac606754542f77ad" + integrity sha512-/Ve9qwGyRRK1uUCrefh9cVG3OIaaS9LQoTUk9YMUbsoWxd6JitXh31XVHjlvNtBYGMRVdz+uGPS2zSxq1vi4aA== + dependencies: + detect-gpu "^4.0.45" + lodash.clonedeep "4.5.0" + "@cornerstonejs/streaming-image-volume-loader@^0.8.2": version "0.8.2" resolved "https://registry.npmjs.org/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.8.2.tgz#058a591501e7fe7ebbe195695c92d047a288290f" @@ -2413,12 +2421,12 @@ "@cornerstonejs/core" "^0.25.0" cornerstone-wado-image-loader "^4.7.0" -"@cornerstonejs/tools@^0.34.0": - version "0.34.0" - resolved "https://registry.npmjs.org/@cornerstonejs/tools/-/tools-0.34.0.tgz#d164f575b4870cea2f3300aaf88722a5f47e61f4" - integrity sha512-J+pcLsAobcX3oHuDOmaWvXLfofBCa7wivuKjkuEajssgm+pUHp+hAt+LAZz0fy1m2ZGOFUZn6TQjGbQg/nrYEQ== +"@cornerstonejs/tools@^0.34.2": + version "0.34.2" + resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-0.34.2.tgz#645c97583066a872fd35188153cd21b26625473e" + integrity sha512-rbdQuSXGJ++ARS6eUAkBZi0tjmUKVtad0QPk0SZZXlDC2QAQRyIfq7n7oF2AaIR/0ngarTekUzuIy2nKiaOT+Q== dependencies: - "@cornerstonejs/core" "^0.25.0" + "@cornerstonejs/core" "^0.25.1" lodash.clonedeep "4.5.0" lodash.get "^4.4.2"