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

This commit is contained in:
Matthis Duclos 2023-01-25 16:11:08 +01:00 committed by GitHub
parent f7c886878d
commit 514ea3ccea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 139 additions and 37 deletions

View File

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

View File

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

View File

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

View File

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

View File

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