feat: state sync service and hanging protocol updates to preserve state (#3131)

* feat: Add state sync and use it to remember viewport grid info

fix: Version updates

Fixes for toggling MPR mode

Fix the display when the interleaved load module fails

Fix the memory of the state to restore correctly

PR fixes for the state sync service

PR fixes

PR fixes

PR fixes

Added a hack warning to remove volumeDeactivate

Fixes for TMTV colormap setting

Fix the casing

Missed renames

fix: tests not running due to variance in ordering

Reverting some fixes to change case

PR changes - mostly comments and minor improvements

fix: All display sets were being updated on drag and drop

PR fixes - mostly renames

PR fixes

Test support for OHIF, for HP branch

test: Add at least a minimal set of automated tests for hanging protocols

Docs

PR fixes

Merge fixes

DOCS updates

Add an example of the mn hanging protocol

PR fixes

PR fixes

PR fixes

* Fix the drag and drop

PR fixes

* PR changes - update default keys for next/previous stage

* fix: Was storing the custom viewport grid too aggressively

Caused by a PR change misspelling a variable
This commit is contained in:
Bill Wallace authored and GitHub committed 2023-03-15 12:41:41 -04:00
1 parent b7fff77e17
commit 803f638401
87 files changed
+3413 -1919

No files matched your search

+47 -2
View File
@@ -1,4 +1,5 @@
import log from '../log.js';
import { Command, Commands } from '../types/Command';
/**
* The definition of a command
@@ -106,7 +107,7 @@ export class CommandsManager {
* @param {String} [contextName] - Specific command to look in. Defaults to current activeContexts
*/
getCommand = (commandName, contextName) => {
let contexts = [];
const contexts = [];
if (contextName) {
const context = this.getContext(contextName);
@@ -140,7 +141,7 @@ export class CommandsManager {
* @param {Object} [options={}] - Extra options to pass the command. Like a mousedown event
* @param {String} [contextName]
*/
runCommand(commandName, options = {}, contextName) {
public runCommand(commandName: string, options = {}, contextName?: string) {
const definition = this.getCommand(commandName, contextName);
if (!definition) {
log.warn(`Command "${commandName}" not found in current context`);
@@ -161,6 +162,50 @@ export class CommandsManager {
return commandFn(commandParams);
}
}
/**
* Run one or more commands with specified extra options.
* Returns the result of the last command run.
*
* @param toRun - A specification of one or more commands
* @param options - to include in the commands run beyond
* the commandOptions specified in the base.
*/
public run(
toRun: Command | Commands | Command[] | undefined,
options?: Record<string, unknown>
): unknown {
if (!toRun) return;
const commands =
(Array.isArray(toRun) && toRun) ||
((toRun as Command).commandName && [toRun]) ||
(Array.isArray((toRun as Commands).commands) &&
(toRun as Commands).commands);
if (!commands) {
console.log("Command isn't runnable", toRun);
return;
}
let result;
(commands as Command[]).forEach(
({ commandName, commandOptions, context }) => {
if (commandName) {
result = this.runCommand(
commandName,
{
...commandOptions,
...options,
},
context
);
} else {
console.warn('No command name supplied in', toRun);
}
}
);
return result;
}
}
export default CommandsManager;
@@ -88,6 +88,20 @@ const bindings = [
// keys: ['pagedown'],
// isEditable: true,
// },
{
commandName: 'nextStage',
context: 'DEFAULT',
label: 'Next Stage',
keys: ['.'],
isEditable: true,
},
{
commandName: 'previousStage',
context: 'DEFAULT',
label: 'Previous Stage',
keys: [','],
isEditable: true,
},
{
commandName: 'nextImage',
label: 'Next Image',
@@ -1,6 +1,7 @@
export default {
COMMANDS: 'commandsModule',
CUSTOMIZATION: 'customizationModule',
STATE_SYNC: 'stateSyncModule',
DATA_SOURCE: 'dataSourcesModule',
PANEL: 'panelModule',
SOP_CLASS_HANDLER: 'sopClassHandlerModule',
+1
View File
@@ -25,6 +25,7 @@ describe('Top level exports', () => {
//
'CineService',
'CustomizationService',
'StateSyncService',
'UIDialogService',
'UIModalService',
'UINotificationService',
+3
View File
@@ -29,6 +29,7 @@ import {
PubSubService,
UserAuthenticationService,
CustomizationService,
StateSyncService,
PanelService,
} from './services';
@@ -61,6 +62,7 @@ const OHIF = {
//
CineService,
CustomizationService,
StateSyncService,
UIDialogService,
UIModalService,
UINotificationService,
@@ -99,6 +101,7 @@ export {
//
CineService,
CustomizationService,
StateSyncService,
UIDialogService,
UIModalService,
UINotificationService,
@@ -63,7 +63,7 @@ export default class CustomizationService extends PubSubService {
modeCustomizations: Record<string, Customization> = {};
globalCustomizations: Record<string, Customization> = {};
configuration: UICustomizationConfiguration;
configuration: CustomizationConfiguration;
constructor({ configuration, commandsManager }) {
super(EVENTS);
@@ -97,36 +97,6 @@ export default class CustomizationService extends PubSubService {
this.modeCustomizations = {};
}
/**
*
* @param {*} interaction - can be undefined to run nothing
* @param {*} extraOptions to include in the commands run
*/
recordInteraction(
interaction: Customization | void,
extraOptions?: Record<string, unknown>
): void {
if (!interaction) return;
const commandsManager = this.commandsManager;
const { commands = [] } = interaction;
commands.forEach(({ commandName, commandOptions, context }) => {
if (commandName) {
commandsManager.runCommand(
commandName,
{
interaction,
...commandOptions,
...extraOptions,
},
context
);
} else {
console.warn('No command name supplied in', interaction);
}
});
}
public getModeCustomizations(): Record<string, Customization> {
return this.modeCustomizations;
}
@@ -145,6 +115,23 @@ export default class CustomizationService extends PubSubService {
});
}
/** This is the preferred getter for all customizations,
* getting mode customizations first and otherwise global customizations.
*
* @param customizationId - the customization id to look for
* @param defaultValue - is the default value to return. Note this value
* may have been extended with any customizationType extensions provided,
* so you cannot just use `|| defaultValue`
* @return A customization to use if one is found, or the default customization,
* both enhanced with any customizationType inheritance (see applyType)
*/
public getCustomization(
customizationId: string,
defaultValue?: Customization
): Customization | void {
return this.getModeCustomization(customizationId, defaultValue);
}
/** Mode customizations are changes to the behaviour of the extensions
* when running in a given mode. Reset clears mode customizations.
* Note that global customizations over-ride mode customizations.
@@ -168,12 +155,17 @@ export default class CustomizationService extends PubSubService {
);
}
/** Applies any inheritance due to UI Type customization */
/**
* Applies any inheritance due to UI Type customization.
* This will look for customizationType in the customization object
* and if that is found, will assign all iterable values from that
* type into the new type, allowing default behaviour to be configured.
*/
public applyType(customization: Customization): Customization {
if (!customization) return customization;
const { customizationType } = customization;
if (!customizationType) return customization;
const parent = this.getModeCustomization(customizationType);
const parent = this.getCustomization(customizationType);
return parent
? Object.assign(Object.create(parent), customization)
: customization;
@@ -29,27 +29,31 @@ const match = (
let requiredFailed = false;
let score = 0;
// Allow for matching against current or prior specifically
const prior = options?.studies?.[1];
const current = options?.studies?.[0];
const instance = (metadataInstance.images || metadataInstance.others)?.[0];
const fromSrc = {
prior,
current,
instance,
...options,
options,
metadataInstance,
};
rules.forEach(rule => {
const { attribute } = rule;
const { attribute, from = 'metadataInstance' } = rule;
// Do not use the custom attribute from the metadataInstance since it is subject to change
if (customAttributeRetrievalCallbacks.hasOwnProperty(attribute)) {
readValues[attribute] = customAttributeRetrievalCallbacks[
attribute
].callback(metadataInstance, options);
].callback.call(rule, metadataInstance, options);
} else {
readValues[attribute] =
metadataInstance[attribute] ??
((metadataInstance.images || metadataInstance.others || [])[0] || {})[
attribute
];
fromSrc[from]?.[attribute] ?? instance?.[attribute];
}
console.log(
'Test',
attribute,
readValues[attribute],
JSON.stringify(rule.constraint)
);
// Format the constraint as required by Validate.js
const testConstraint = {
[attribute]: rule.constraint,
@@ -70,6 +74,14 @@ const match = (
errorMessages = ['Something went wrong during validation.', e];
}
console.log(
'Test',
`${from}.${attribute}`,
readValues[attribute],
JSON.stringify(rule.constraint),
!errorMessages
);
if (!errorMessages) {
// If no errorMessages were returned, then validation passed.
@@ -117,8 +117,7 @@ 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]]));
const { viewportMatchDetails } = hps.getMatchDetails();
expect(viewportMatchDetails.size).toBe(1);
expect(viewportMatchDetails.get(0)).toMatchObject({
viewportOptions: {
@@ -131,9 +130,11 @@ function checkHpsBestMatch(hps) {
// ds2 fails to match required and ds3 fails to match an optional.
displaySetsInfo: [
{
SeriesInstanceUID: 'ds1',
displaySetInstanceUID: 'displaySet1',
displaySetOptions: {},
displaySetOptions: {
id: 'displaySetSelector',
options: {},
},
},
],
});
@@ -191,14 +192,6 @@ describe('HangingProtocolService', () => {
it('matches best image match', () => {
checkHpsBestMatch(hangingProtocolService);
});
it('uses services manager', () => {
hangingProtocolService.run({
studies: [studyMatch],
displaySets: studyMatchDisplaySets,
});
expect(mockedFunction).toHaveBeenCalledTimes(1);
});
});
});
});
File diff suppressed because it is too large. Load diff
@@ -81,6 +81,16 @@ export default class ProtocolEngine {
});
}
/**
* finds the match results against the given display set or
* study instance by testing the given rules against this, and using
* the provided options for testing.
*
* @param {*} metaData to match against as primary value
* @param {*} rules to apply
* @param {*} options are additional values that can be used for matching
* @returns
*/
findMatch(metaData, rules, options) {
return HPMatcher.match(
metaData,
@@ -109,7 +119,7 @@ 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 for',
protocol.id
);
return;
@@ -14,6 +14,41 @@ validate.validators.doesNotEqual = function(value, options, key) {
}
};
// Ignore case contains.
// options testValue MUST be in lower case already, otherwise it won't match
validate.validators.containsI = function (value, options, key) {
const testValue = options?.value ?? options;
if (Array.isArray(value)) {
if (
value.some(
item => !validate.validators.containsI(item.toLowerCase(), options, key)
)
) {
return undefined;
}
return `No item of ${value.join(',')} contains ${JSON.stringify(
testValue
)}`;
}
if (Array.isArray(testValue)) {
if (
testValue.some(
subTest => !validate.validators.containsI(value, subTest, key)
)
) {
return;
}
return `${key} must contain at least one of ${testValue.join(',')}`;
}
if (
testValue &&
value.indexOf &&
value.toLowerCase().indexOf(testValue) === -1
) {
return key + 'must contain any case of' + testValue;
}
};
validate.validators.contains = function(value, options, key) {
const testValue = options?.value ?? options;
if (Array.isArray(value)) {
@@ -3,9 +3,10 @@ import validate from './validator.js';
describe('validator', () => {
const attributeMap = {
str: 'string',
upper: 'UPPER',
num: 3,
nullValue: null,
list: ['abc', 'def'],
list: ['abc', 'def', 'GHI'],
};
const options = {
@@ -35,6 +36,37 @@ describe('validator', () => {
});
});
describe('containsI', () => {
it('returns match any list contains case insensitive', () => {
expect(
validate(attributeMap, { upper: { containsI: ['bye', 'pre'] } }, [
options,
])
).not.toBeUndefined();
expect(
validate(attributeMap, { list: { containsI: 'hi' } }, [options])
).toBeUndefined();
expect(
validate(attributeMap, { list: { containsI: ['hi', 'bye'] } }, [
options,
])
).toBeUndefined();
expect(
validate(attributeMap, { list: { containsI: ['bye', 'hi'] } }, [
options,
])
).toBeUndefined();
expect(
validate(attributeMap, { list: { containsI: ['ig', 'hi'] } }, [options])
).toBeUndefined();
expect(
validate(attributeMap, { upper: { containsI: ['bye', 'per'] } }, [
options,
])
).toBeUndefined();
});
});
describe('equals', () => {
it('returned undefined on equals', () => {
expect(
@@ -519,9 +519,14 @@ class MeasurementService extends PubSubService {
let measurement = {};
try {
const sourceMappings = this.mappings[source.uid];
const { toMeasurementSchema } = sourceMappings.find(
const sourceMapping = sourceMappings.find(
mapping => mapping.annotationType === annotationType
);
if (!sourceMapping) {
console.log('No source mapping', source);
return;
}
const { toMeasurementSchema } = sourceMapping;
/* Convert measurement */
measurement = toMeasurementSchema(sourceAnnotationDetail);
@@ -1,10 +1,11 @@
import log from './../log.js';
import Services from '../types/Services';
import CommandsManager from '../classes/CommandsManager';
export default class ServicesManager {
public services: Services = {};
constructor(commandsManager) {
constructor(commandsManager: CommandsManager) {
this._commandsManager = commandsManager;
this.services = {};
this.registeredServiceNames = [];
@@ -0,0 +1,31 @@
import StateSyncService from './StateSyncService';
import log from '../../log';
jest.mock('../../log.js', () => ({
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
}));
const extensionManager = {};
describe('StateSyncService.ts', () => {
let stateSyncService;
let configuration;
beforeEach(() => {
log.warn.mockClear();
jest.clearAllMocks();
configuration = {};
stateSyncService = new StateSyncService({
configuration,
});
});
describe('init', () => {
it('init succeeds', () => {
stateSyncService.init(extensionManager);
});
});
});
@@ -0,0 +1,80 @@
import { PubSubService } from '../_shared/pubSubServiceInterface';
import { ExtensionManager } from '../../extensions';
const EVENTS = {};
type Obj = Record<string, unknown>;
type StateConfig = {
/** clearOnModeExit defines state configuraion that is cleared automatically on
* exiting a mode. This clearing occurs after the mode onModeExit,
* so it is possible to preserve desired state during exit to be restored
* later.
*/
clearOnModeExit?: boolean;
};
type States = {
[key: string]: Obj;
};
/**
*/
export default class StateSyncService extends PubSubService {
public static REGISTRATION = {
name: 'stateSyncService',
create: ({ configuration = {}, commandsManager }) => {
return new StateSyncService({ configuration, commandsManager });
},
};
extensionManager: ExtensionManager;
configuration: Obj;
registeredStateSets: {
[id: string]: StateConfig;
} = {};
state: States = {};
constructor({ configuration }) {
super(EVENTS);
this.configuration = configuration || {};
}
public init(extensionManager: ExtensionManager): void { }
public register(id: string, config: StateConfig): void {
this.registeredStateSets[id] = config;
this.store({ [id]: {} });
}
public getState(): Record<string, Obj> {
// TODO - return a proxy to this which is not writable in dev mode
return this.state;
}
/**
* Stores all the new state values contained in states.
*
* @param states - is an object containing replacement values to store
* @returns
*/
public store(states: States): States {
Object.keys(states).forEach(stateKey => {
if (!this.registeredStateSets[stateKey]) {
throw new Error(`No state ${stateKey} registered`);
}
});
this.state = { ...this.state, ...states };
return states;
}
public onModeExit(): void {
const toReduce = {};
for (const [key, value] of Object.entries(this.registeredStateSets)) {
if (value.clearOnModeExit) {
toReduce[key] = {};
}
}
this.store(toReduce);
}
}
@@ -0,0 +1,3 @@
import StateSyncService from './StateSyncService';
export default StateSyncService;
@@ -1,4 +1,6 @@
import merge from 'lodash.merge';
import { CommandsManager } from '../../classes';
import { ExtensionManager } from '../../extensions';
import { PubSubService } from '../_shared/pubSubServiceInterface';
const EVENTS = {
@@ -16,37 +18,31 @@ export default class ToolbarService extends PubSubService {
},
};
constructor(commandsManager) {
buttons: Record<string, unknown> = {};
state: {
primaryToolId: string;
toggles: Record<string, boolean>;
groups: Record<string, unknown>;
} = { primaryToolId: 'WindowLevel', toggles: {}, groups: {} };
buttonSections: Record<string, unknown> = {
/**
* primary: ['Zoom', 'Wwwc'],
* secondary: ['Length', 'RectangleRoi']
*/
};
_commandsManager: CommandsManager;
extensionManager: ExtensionManager;
constructor(commandsManager: CommandsManager) {
super(EVENTS);
this._commandsManager = commandsManager;
//
this.buttons = {};
this.unsubscriptions = []; // if tools need to unsubscribe from events
this.buttonSections = {
/**
* primary: ['Zoom', 'Wwwc'],
* secondary: ['Length', 'RectangleRoi']
*/
};
// TODO: Do we need to track per context? Or do we allow for a mixed
// definition that adapts based on context?
this.state = {
primaryToolId: 'WindowLevel',
toggles: {
/* id: true/false */
},
groups: {
/* track most recent click per group...? */
},
};
}
init(extensionManager) {
public init(extensionManager: ExtensionManager): void {
this.extensionManager = extensionManager;
}
reset() {
public reset(): void {
this.unsubscriptions.forEach(unsub => unsub());
this.state = {
primaryToolId: 'WindowLevel',
@@ -69,7 +65,7 @@ export default class ToolbarService extends PubSubService {
* used for calling the specified interaction. That is, the command is
* called with {...commandOptions,...options}
*/
recordInteraction(interaction, options) {
recordInteraction(interaction, options?: Record<string, unknown>) {
if (!interaction) return;
const commandsManager = this._commandsManager;
const { groupId, itemId, interactionType, commands } = interaction;
@@ -181,6 +177,15 @@ export default class ToolbarService extends PubSubService {
return [this.state.primaryToolId, ...Object.keys(this.state.toggles)];
}
/** Sets the toggle state of a button to the isActive state */
public setActive(id: string, isActive: boolean): void {
if (isActive) {
this.state.toggles[id] = true;
} else {
delete this.state.toggles[id];
}
}
setButton(id, button) {
if (this.buttons[id]) {
this.buttons[id] = merge(this.buttons[id], button);
@@ -12,6 +12,7 @@ class ViewportGridService extends PubSubService {
return new ViewportGridService();
},
};
public static EVENTS = EVENTS;
serviceImplementation = {};
@@ -25,8 +26,6 @@ class ViewportGridService extends PubSubService {
setActiveViewportIndex: setActiveViewportIndexImplementation,
setDisplaySetsForViewport: setDisplaySetsForViewportImplementation,
setDisplaySetsForViewports: setDisplaySetsForViewportsImplementation,
setCachedLayout: setCachedLayoutImplementation,
restoreCachedLayout: restoreCachedLayoutImplementation,
setLayout: setLayoutImplementation,
reset: resetImplementation,
onModeExit: onModeExitImplementation,
@@ -51,12 +50,6 @@ class ViewportGridService extends PubSubService {
if (resetImplementation) {
this.serviceImplementation._reset = resetImplementation;
}
if (setCachedLayoutImplementation) {
this.serviceImplementation._setCachedLayout = setCachedLayoutImplementation;
}
if (restoreCachedLayoutImplementation) {
this.serviceImplementation._restoreCachedLayout = restoreCachedLayoutImplementation;
}
if (onModeExitImplementation) {
this.serviceImplementation._onModeExit = onModeExitImplementation;
}
@@ -70,8 +63,11 @@ class ViewportGridService extends PubSubService {
public setActiveViewportIndex(index) {
this.serviceImplementation._setActiveViewportIndex(index);
const state = this.getState();
const viewportId = state.viewports[index]?.viewportOptions?.viewportId;
this._broadcastEvent(this.EVENTS.ACTIVE_VIEWPORT_INDEX_CHANGED, {
viewportIndex: index,
viewportId,
});
}
@@ -97,8 +93,20 @@ class ViewportGridService extends PubSubService {
this.serviceImplementation._setDisplaySetsForViewports(viewports);
}
public setLayout({ numCols, numRows }) {
this.serviceImplementation._setLayout({ numCols, numRows });
/**
*
* @param numCols, numRows - the number of columns and rows to apply
* @param findOrCreateViewport is a function which takes the
* index position of the viewport, the position id, and a set of
* options that is initially provided as {} (eg to store intermediate state)
* The function returns a viewport object to use at the given position.
*/
public setLayout({ numCols, numRows, findOrCreateViewport = undefined }) {
this.serviceImplementation._setLayout({
numCols,
numRows,
findOrCreateViewport,
});
}
public reset() {
@@ -115,14 +123,6 @@ class ViewportGridService extends PubSubService {
this.serviceImplementation._onModeExit();
}
public setCachedLayout({ cacheId, cachedLayout }) {
this.serviceImplementation._setCachedLayout({ cacheId, cachedLayout });
}
public restoreCachedLayout(cacheId) {
this.serviceImplementation._restoreCachedLayout(cacheId);
}
public set(state) {
this.serviceImplementation._set(state);
}
+2
View File
@@ -17,6 +17,7 @@ import UserAuthenticationService from './UserAuthenticationService';
import CustomizationService from './CustomizationService';
import Services from '../types/Services';
import StateSyncService from './StateSyncService';
import PanelService from './PanelService';
export {
@@ -24,6 +25,7 @@ export {
MeasurementService,
ServicesManager,
CustomizationService,
StateSyncService,
UIDialogService,
UIModalService,
UINotificationService,
+7
View File
@@ -3,3 +3,10 @@ export interface Command {
commandOptions?: Record<string, unknown>;
context?: string;
}
/**
* This is the format used within many items for multiple commands
*/
export interface Commands {
commands: [];
}
+193 -70
View File
@@ -1,68 +1,112 @@
type DisplaySetInfo = {
SeriesInstanceUID: string;
displaySetInstanceUID: string;
displaySetOptions: Record<string, unknown>;
import { Command } from './Command';
export type DisplaySetInfo = {
displaySetInstanceUID?: string;
displaySetOptions: DisplaySetOptions;
};
type ViewportMatchDetails = {
export type ViewportMatchDetails = {
viewportOptions: ViewportOptions;
displaySetsInfo: DisplaySetInfo[];
};
type DisplaySetMatchDetails = {
SeriesInstanceUID: string;
StudyInstanceUID: string;
export type DisplaySetMatchDetails = {
StudyInstanceUID?: string;
displaySetInstanceUID: string;
matchDetails?: any;
matchingScores?: any[];
matchingScores?: DisplaySetMatchDetails[];
sortingInfo?: any;
};
type DisplaySetAndViewportOptions = {
export type DisplaySetAndViewportOptions = {
displaySetInstanceUIDs: string[];
viewportOptions: ViewportOptions;
displaySetOptions: DisplaySetOptions;
}
};
type ViewportSpecificProtocolOptions = {
[viewportIndex: string]: DisplaySetAndViewportOptions
}
export type SetProtocolOptions = {
/** Used to provide a mapping of what keys are provided for which viewport.
* For example, a Chest XRay might use have the display set selector id of
* "ChestXRay", then the user might drag an alternate chest xray from the initially chosen one,
* and then navigate to another stage or protocol. If that new stage/protocol
* uses the name "ChestXRay", then that selection will be used instead of
* matching the display set selectors. That allows remembering the
* user selected views by name.
* Note the keys are not simple display set selector values, but are:
* `${activeStudyUID}:${displaySetSelectorId}:${matchingDisplaySetIndex || 0}`
* This is normally transparent to the user of this, but in order to specify
* specific instances, they can be added like that.
*/
displaySetSelectorMap?: Record<string, string>;
type GlobalProtocolOptions = DisplaySetAndViewportOptions
/** Used to define the display sets already in view, in order to allow
* filling empty viewports with other instances.
* Only used when the -1 value for matchedDisplaySetsIndex is provided.
* List of display set instance UID's already displayed.
*/
inDisplay?: string[];
/** Select the given stage, either by ID or position.
* Don't forget that name is used as the ID if ID not provided.
*/
stageId?: string;
stageIndex?: number;
type SetProtocolOptions =
ViewportSpecificProtocolOptions | GlobalProtocolOptions;
/** Indicates to setup the protocol and fire the PROTOCOL_RESTORED event
* but don't fire the protocol changed event. Used to restore the
* HP service to a previous state.
*/
restoreProtocol?: boolean;
};
type HangingProtocolMatchDetails = {
export type HangingProtocolMatchDetails = {
displaySetMatchDetails: Map<string, DisplaySetMatchDetails>;
viewportMatchDetails: Map<number, ViewportMatchDetails>;
hpAlreadyApplied: Map<number, boolean>;
};
type MatchingRule = {
id: string;
weight: number;
export type ConstraintValue =
| string
| number
| boolean
| []
| {
value: string | number | boolean | [];
};
export type Constraint = {
// This value exactly
equals?: ConstraintValue;
notEquals?: ConstraintValue;
// A caseless contains
containsI?: string;
contains?: ConstraintValue;
greaterThan?: ConstraintValue;
};
export type MatchingRule = {
// No real use for the id
id?: string;
// Defaults to 1
weight?: number;
attribute: string;
constraint: Record<string, unknown>;
required: boolean;
constraint: Constraint;
// Not required by default
required?: boolean;
};
type ViewportLayoutOptions = {
export type ViewportLayoutOptions = {
x: number;
y: number;
width: number;
height: number;
};
type ViewportStructure = {
export type ViewportStructure = {
layoutType: string;
properties: {
rows: number;
columns: number;
layoutOptions: ViewportLayoutOptions[];
layoutOptions?: ViewportLayoutOptions[];
};
};
@@ -74,7 +118,8 @@ type ViewportStructure = {
* The matches are done lazily, so if a stage doesn't need a given match,
* it won't be selected.
*/
type DisplaySetSelector = {
export type DisplaySetSelector = {
id?: string;
// The image matching rule (not currently implemented) selects which image to
// display initially, only for stack views.
imageMatchingRules?: MatchingRule[];
@@ -83,19 +128,19 @@ type DisplaySetSelector = {
studyMatchingRules?: MatchingRule[];
};
type SyncGroup = {
export type SyncGroup = {
type: string;
id: string;
source?: boolean
target?: boolean
}
source?: boolean;
target?: boolean;
};
type initialImageOptions = {
export type initialImageOptions = {
index?: number;
preset? : string; // todo: type more
}
preset?: string; // todo: type more
};
type ViewportOptions = {
export type ViewportOptions = {
toolGroupId: string;
viewportType: string;
id?: string;
@@ -104,37 +149,116 @@ type ViewportOptions = {
initialImageOptions?: initialImageOptions;
syncGroups?: SyncGroup[];
customViewportProps?: Record<string, unknown>;
// Set to true to allow non-matching drag and drop or options provided
// from options.displaySetSelectorsMap
allowUnmatchedView?: boolean;
};
type DisplaySetOptions = {
// The options here includes both the display set selector and matching index
// as well as actual options to apply to the individual viewports.
export type DisplaySetOptions = {
// The id is used to choose which display set selector to apply here
id: string;
// An offset to allow display secondary series, for example
// to display the second matching series (displaySetIndex==1)
// This cannot easily be done with the matching rules directly.
displaySetIndex?: number;
/** The offset to allow display secondary series, for example
* to display the second matching series, use `matchedDisplaySetsIndex==1` */
matchedDisplaySetsIndex?: number;
// The options to apply to the display set.
options?: Record<string, unknown>;
};
type Viewport = {
export type Viewport = {
viewportOptions: ViewportOptions;
displaySets: DisplaySetOptions[];
};
type ProtocolStage = {
id: string;
/**
* disabled stages are missing display sets required in order to view them.
* enabled stages have all the requiredDisplaySets and at least preferredViewports
* filled.
* passive stages have the requiredDisplaySets and at least requiredViewports filled.
*/
export type StageStatus = 'disabled' | 'enabled' | 'passive';
/** Controls whether a stage is activated or not, at the given level, by
* controlling the status of the stage.
*/
export type StageActivation = {
// The minimum number of viewports to be NON-blank to activate this level of the stage
minViewportsMatched?: number;
// The required set of display set selectors to have at least 1 match to activate
displaySetSelectorsMatched?: string[];
};
/**
* Protocol stages are a set of different views which can be applied, for
* example, a 2x1 and a 1x1 view might be both applied (see default extension
* for this example).
*/
export type ProtocolStage = {
/** The id defaults to the name of the protocol if not otherwise specified */
id?: string;
/**
* The display name used for this stage when shown to the user. This can
* differ from the id, for example, to use the same name for different
* stages, only one of which ends up being active.
*/
name: string;
/** Indicate if the stage can be applied or not */
status?: StageStatus;
viewportStructure: ViewportStructure;
stageActivation?: {
// The enabled activation is provided for fully active stages,
// participating in automatic stage selection and navigation
enabled?: StageActivation;
// The passive activation is provided to allow stages to manually
// be activated, but not navigated to by default, or used on initial view
passive?: StageActivation;
};
/** A viewport definition used for to fill in manually selected viewports.
* This allows changing the layout definition for additional viewports without
* needing to define layouts for each of the 1x1, 2x2 etc modes.
*/
defaultViewport?: Viewport;
viewports: Viewport[];
// Unused.
createdDate?: string;
};
type Protocol = {
// Add notifications for various types of events.
export type ProtocolNotifications = {
// This set of commands is executed after the protocol is exited and the new one applied
onProtocolExit?: Command[];
// This set of commands is executed after the protocol is entered and applied
onProtocolEnter?: Command[];
// This set of commands is executed before the layout change is started.
// If it returns false, the layout change will be aborted.
// The numRows and numCols is included in the command params, so it is possible
// to apply a specific hanging protocol
onLayoutChange?: Command[];
};
/**
* A protocol is the top level definition for a hanging protocol.
* It is a set of rules about when the protocol can be applied at all,
* as well as a set of stages that represent indivividual views.
* Additionally, the display set selectors are used to choose from the existing
* display sets. The hanging protcol definition here does NOT allow
* redefining the display sets to use, but only selects the views to show.
*/
export type Protocol = {
// Mandatory
id: string;
// Selects which display sets are given a specific name.
/** Maps ids to display set selectors to choose display sets */
displaySetSelectors: Record<string, DisplaySetSelector>;
/** A default viewport to use for any stage to select new viewport layouts. */
defaultViewport?: Viewport;
stages: ProtocolStage[];
// Optional
locked?: boolean;
@@ -145,35 +269,34 @@ type Protocol = {
availableTo?: Record<string, unknown>;
editableBy?: Record<string, unknown>;
toolGroupIds?: string[];
// A set of callbacks relevant to entering and exiting the protocol
callbacks?: ProtocolNotifications;
imageLoadStrategy?: string; // Todo: this should be types specifically
protocolMatchingRules?: MatchingRule[];
/* The number of priors required for this hanging protocol.
* -1 means that NO priors are referenced, and thus this HP matches
* only the active study, whereas 0 means that an unknown number of
* priors is matched.
*/
numberOfPriorsReferenced?: number;
syncDataForViewports?: boolean;
};
type ProtocolGenerator = ({ servicesManager: any, commandsManager: any }) => {
/** Used to dynamically generate protocols.
* Try to avoid this as it is difficult to provide active/disabled settings
* to the GUI when this is used, and it can be expensive to apply.
* Alternatives include using the custom attributes where possible.
*/
export type ProtocolGenerator = ({
servicesManager: any,
commandsManager: any,
}) => {
protocol: Protocol;
};
export type {
SetProtocolOptions,
ViewportOptions,
ViewportMatchDetails,
DisplaySetMatchDetails,
HangingProtocolMatchDetails,
Protocol,
ProtocolStage,
Viewport,
DisplaySetSelector,
ViewportStructure,
ViewportLayoutOptions,
DisplaySetOptions,
MatchingRule,
SyncGroup,
initialImageOptions,
DisplaySetInfo,
GlobalProtocolOptions,
ViewportSpecificProtocolOptions,
DisplaySetAndViewportOptions,
ProtocolGenerator,
export type HPInfo = {
protocolId: string;
stageId: string;
stageIndex: number;
activeStudyUID: string;
};
+2
View File
@@ -5,6 +5,7 @@ import {
ViewportGridService,
ToolbarService,
DisplaySetService,
StateSyncService,
} from '../services';
/**
@@ -28,5 +29,6 @@ export default interface Services {
syncGroupService?: Record<string, unknown>;
cornerstoneCacheService?: Record<string, unknown>;
segmentationService?: Record<string, unknown>;
stateSyncService?: StateSyncService;
panelService?: Record<string, unknown>;
}