feat(HangingProtocol): add protocol generator back (#3128)

This commit is contained in:
Matthis Duclos authored and GitHub committed 2023-01-25 10:11:08 -05:00
1 parent f7c886878d
commit 514ea3ccea
5 files changed
+139 -37

No files matched your search

@@ -79,6 +79,14 @@ const testProtocol = {
numberOfPriorsReferenced: -1, numberOfPriorsReferenced: -1,
}; };
function testProtocolGenerator({ servicesManager }) {
servicesManager.services.TestService.toCall();
return {
protocol: testProtocol,
};
}
const studyMatch = { const studyMatch = {
StudyInstanceUID: 'studyMatch', StudyInstanceUID: 'studyMatch',
StudyDescription: 'A PETCT study type', StudyDescription: 'A PETCT study type',
@@ -107,41 +115,80 @@ const displaySet3 = {
const studyMatchDisplaySets = [displaySet3, displaySet2, displaySet1]; 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', () => { describe('HangingProtocolService', () => {
const mockedFunction = jest.fn();
const commandsManager = {}; const commandsManager = {};
const hps = new HangingProtocolServiceClass(commandsManager); const servicesManager = {
services: {
TestService: {
toCall: mockedFunction,
},
},
};
const hps = new HangingProtocolServiceClass(commandsManager, servicesManager);
let initialScaling; let initialScaling;
beforeAll(() => { afterEach(() => {
hps.addProtocol(testProtocol.id, testProtocol); mockedFunction.mockClear();
}); });
it('has one protocol', () => { describe('with a static protocol', () => {
expect(hps.getProtocols().length).toBe(1); 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', () => { describe('with protocol generator', () => {
it('matches best image match', () => { beforeAll(() => {
hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets }); hps.addProtocol(testProtocol.id, testProtocolGenerator);
const { hpAlreadyApplied, viewportMatchDetails } = hps.getMatchDetails(); });
expect(hpAlreadyApplied).toMatchObject(new Map([[0, false]]));
expect(viewportMatchDetails.size).toBe(1); it('has one protocol', () => {
expect(viewportMatchDetails.get(0)).toMatchObject({ expect(hps.getProtocols().length).toBe(1);
viewportOptions: { });
viewportId: 'ctAXIAL',
viewportType: 'volume', describe('run', () => {
orientation: 'axial', it('matches best image match', () => {
toolGroupId: 'ctToolGroup', checkHpsBestMatch(hps);
}, });
// Matches ds1 because it matches 2 rules, a required and an optional
// ds2 fails to match required and ds3 fails to match an optional. it('uses services manager', () => {
displaySetsInfo: [ hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets });
{ expect(mockedFunction).toHaveBeenCalledTimes(1);
SeriesInstanceUID: 'ds1',
displaySetInstanceUID: 'displaySet1',
displaySetOptions: {},
},
],
}); });
}); });
}); });
@@ -16,7 +16,7 @@ const EVENTS = {
'event::hanging_protocol_applied_for_viewport', 'event::hanging_protocol_applied_for_viewport',
}; };
type Protocol = HangingProtocol.Protocol; type Protocol = HangingProtocol.Protocol | HangingProtocol.ProtocolGenerator;
class HangingProtocolService { class HangingProtocolService {
studies: StudyMetadata[]; studies: StudyMetadata[];
@@ -180,7 +180,21 @@ class HangingProtocolService {
public getProtocolById(id: string): HangingProtocol.Protocol { public getProtocolById(id: string): HangingProtocol.Protocol {
const protocol = this.protocols.get(id); 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; 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) { getViewportsRequireUpdate(viewportIndex, displaySetInstanceUID) {
const newDisplaySetInstanceUID = displaySetInstanceUID; const newDisplaySetInstanceUID = displaySetInstanceUID;
const protocol = this.protocol; const protocol = this.protocol;
+5 -2
View File
@@ -151,6 +151,9 @@ type Protocol = {
syncDataForViewports?: boolean; syncDataForViewports?: boolean;
}; };
type ProtocolGenerator = ({ servicesManager: any, commandsManager: any }) => {
protocol: Protocol;
};
export type { export type {
SetProtocolOptions, SetProtocolOptions,
@@ -171,6 +174,6 @@ export type {
DisplaySetInfo, DisplaySetInfo,
GlobalProtocolOptions, GlobalProtocolOptions,
ViewportSpecificProtocolOptions, ViewportSpecificProtocolOptions,
DisplaySetAndViewportOptions DisplaySetAndViewportOptions,
ProtocolGenerator,
}; };
@@ -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, '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'. 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 ## Custom Attribute
In some situations, you might want to match based on a custom attribute and not the DICOM tags. For instance, In some situations, you might want to match based on a custom attribute and not the DICOM tags. For instance,
+13 -5
View File
@@ -2405,6 +2405,14 @@
detect-gpu "^4.0.45" detect-gpu "^4.0.45"
lodash.clonedeep "4.5.0" 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": "@cornerstonejs/streaming-image-volume-loader@^0.8.2":
version "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" 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" "@cornerstonejs/core" "^0.25.0"
cornerstone-wado-image-loader "^4.7.0" cornerstone-wado-image-loader "^4.7.0"
"@cornerstonejs/tools@^0.34.0": "@cornerstonejs/tools@^0.34.2":
version "0.34.0" version "0.34.2"
resolved "https://registry.npmjs.org/@cornerstonejs/tools/-/tools-0.34.0.tgz#d164f575b4870cea2f3300aaf88722a5f47e61f4" resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-0.34.2.tgz#645c97583066a872fd35188153cd21b26625473e"
integrity sha512-J+pcLsAobcX3oHuDOmaWvXLfofBCa7wivuKjkuEajssgm+pUHp+hAt+LAZz0fy1m2ZGOFUZn6TQjGbQg/nrYEQ== integrity sha512-rbdQuSXGJ++ARS6eUAkBZi0tjmUKVtad0QPk0SZZXlDC2QAQRyIfq7n7oF2AaIR/0ngarTekUzuIy2nKiaOT+Q==
dependencies: dependencies:
"@cornerstonejs/core" "^0.25.0" "@cornerstonejs/core" "^0.25.1"
lodash.clonedeep "4.5.0" lodash.clonedeep "4.5.0"
lodash.get "^4.4.2" lodash.get "^4.4.2"