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:
1 parent
b7fff77e17
commit
803f638401
87 files changed
+3413
-1919
No files matched your search
@@ -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',
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('Top level exports', () => {
|
||||
//
|
||||
'CineService',
|
||||
'CustomizationService',
|
||||
'StateSyncService',
|
||||
'UIDialogService',
|
||||
'UIModalService',
|
||||
'UINotificationService',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [];
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -27,6 +27,27 @@ registered automatically to the HangingProtocolService.
|
||||
|
||||
All protocols are stored in the `HangingProtocolService` using their `id` as the key, and the protocol itself as the value.
|
||||
|
||||
## Protocol Definition
|
||||
Protocols are defined in a getHangingProtocolModule inside an extension. As such,
|
||||
they are defined with a module structure that starts with an id, and has field protocol
|
||||
that is the actual protocol definition. This setup allows defining more than
|
||||
one protocol within a module, each one needing it's own definition file.
|
||||
|
||||
```javascript
|
||||
import MyProtocol from './MyProtocol';
|
||||
export default function getHangingProtocolModule() {
|
||||
return [
|
||||
{
|
||||
id: MyProtocol.id,
|
||||
protocol: MyProtocol,
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Within the protocol itself, the structure is layed out as described in the HangingProtocol.ts
|
||||
type definition, starting with `Protocol`. See the type definition for more details.
|
||||
|
||||
## Events
|
||||
|
||||
There are two events that get publish in `HangingProtocolService`:
|
||||
@@ -34,31 +55,101 @@ There are two events that get publish in `HangingProtocolService`:
|
||||
| Event | Description |
|
||||
| ------------ | -------------------------------------------------------------------- |
|
||||
| NEW_LAYOUT | Fires when a new layout is requested by the `HangingProtocolService` |
|
||||
| STAGE_CHANGE | Fires when the the stage is changed in the hanging protocols |
|
||||
| PROTOCOL_CHANGED | Fires when the the protocol is changed in the hanging protocols |
|
||||
| HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT | Fires when the hanging protocol applies for a viewport (sets its displaySets) |
|
||||
| PROTOCOL_CHANGED | Fires when the the protocol is changed in the hanging protocols, or when the applied stage is changed. |
|
||||
| RESTORE_PROTOCOL | Fires when the protocol or stage is restored, for example, after turning off MPR mode |
|
||||
| STAGE_ACTIVATION | Fires when the stages are known to have stage.status set. |
|
||||
|
||||
## Stage Activation and Status
|
||||
Sometimes a hanging protocol can be applicable generally, but not all stages
|
||||
should be shown by default, or should be shown at all. This can be handled by
|
||||
using the stage activation to control whether the stage is shown by default (`enabled`),
|
||||
whether it can be navigated to (`passive`) or whether it should not be shown
|
||||
at all (`disabled`).
|
||||
|
||||
The `stage.status` is used to control this, and the status is controlled by
|
||||
the stage activate. The status values are:
|
||||
|
||||
* enabled - meaning that the stage is fully applicable
|
||||
* passive - meaning that the stage can be applied, but might be missing details
|
||||
* disabled - meaning that the study has insuffient information for this stage
|
||||
|
||||
The default values for no `stageActivation` are to assume that `enabled` has `minViewports` of 1,
|
||||
and `passive` has `minViewports=0`. That is, enable the stage if at least one
|
||||
viewport is filled, and make it passive if no viewports are filled.
|
||||
|
||||
The setting for these are controlled by the stageActivation property, for example
|
||||
the following:
|
||||
|
||||
```javascript
|
||||
stageActivation: {
|
||||
// The enabled activation specifies requirements to enable the stage, that is,
|
||||
// make it preferred.
|
||||
enabled: {
|
||||
// The default value here is 1, and indicates how many non-blank viewports
|
||||
// are required.
|
||||
minViewportsMatched: 3,
|
||||
// This enables specifying cross cutting concerns, such as having a stage
|
||||
// only apply to males or females, and is a list of display set selector ids
|
||||
displaySetSelectorsMatched: ['dsMale'],
|
||||
},
|
||||
// The passive check is performed first. If it fails, the enabled is NOT
|
||||
// checked, but the status set to disabled. The default passive check
|
||||
// should always be passed, so it is fine to just define enabled if desired.
|
||||
passive: {
|
||||
// The default is 0, which means allow the stage even if no viewports are
|
||||
// filled. This allows dragging and dropping into the viewports to
|
||||
// make matches manually, which can then be re-used for other stages.
|
||||
minViewportsMatched: 0,
|
||||
displaySetSelectorsMatched: [...],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
- `destroy`: Destroys the HP service
|
||||
|
||||
- `reset` and `onModeEnter`: Resets the HP service to not have any active
|
||||
hanging protocols
|
||||
|
||||
- `getActiveProtocol`: Returns an object of the internal state of the HP service,
|
||||
useful for storing said state, as well as for getting direct access to the
|
||||
protocol and stage objects. Users of this should count on it being not completely
|
||||
stable as to exactly what this returns, as internal details can change.
|
||||
|
||||
- `getState`: Returns the currently applied protocol ID, stage index and active study UID.
|
||||
This information is storable/useable as state information to be used elsewhere.
|
||||
|
||||
- `getDefaultProtocol`: Returns the default protocol to apply.
|
||||
|
||||
- `getMatchDetails`: returns an object which contains the details of the
|
||||
matching for the viewports, displaySets and whether the protocol is
|
||||
applied to the viewport or not yet.
|
||||
applied to the viewport or not yet. This is deprecated as it is expected
|
||||
to be communicated by events instead.
|
||||
|
||||
- `getProtocols`: Returns a list of the currently active protocols.
|
||||
|
||||
- `getProtocolById`: Gets the protocol with the given id.
|
||||
|
||||
- `addProtocol`: adds provided protocol to the list of registered protocols
|
||||
for matching
|
||||
for matching. Will replacing any protocol with the same id, allowing, for example,
|
||||
to replace the default protocol.
|
||||
|
||||
- `setActiveProtocols`: Choose the protocols which are active. Can take a
|
||||
single protocol id or a list. When a single one is provided, that one will be
|
||||
applied whether or not the required rules match. Called automatically on mode
|
||||
init.
|
||||
|
||||
- `setActiveStudyUID`: Sets the given study UID as active, which has significance
|
||||
in terms of the matching rules being able to match against the active study.
|
||||
|
||||
- `run({studies, activeStudy, displaySets }, protocolId)`: runs the HPService with the provided
|
||||
studyMetaData and optional protocolId. If protocol is not given, HP Matching
|
||||
engine will search all the registered protocols for the best matching one
|
||||
based on the constraints.
|
||||
|
||||
- `registerImageLoadStrategy`: Adds a custom image load strategy.
|
||||
|
||||
- `addCustomAttribute`: adding a custom attribute for matching. (see below)
|
||||
|
||||
- `setProtocol`: applies a protocol to the current studies, it can be used for instance to apply a
|
||||
@@ -68,6 +159,12 @@ init.
|
||||
used for the protocol. If no options are provided, all displaySets will
|
||||
be used to match the protocol.
|
||||
|
||||
- `getStageIndex`: Finds the stage index for a given set of match keys. Currently
|
||||
only works on the currently active protocol, but is supposed to be able to work
|
||||
with other protocols as well.
|
||||
|
||||
- `getMissingViewport`: Returns a viewport object to be used as the missing
|
||||
viewport instance. This is used to fill out new viewports.
|
||||
|
||||
Default initialization of the modes handles running the `HangingProtocolService`
|
||||
|
||||
@@ -78,7 +175,7 @@ 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'.
|
||||
|
||||
A hanging protocol can also be defined with a generator.
|
||||
A hanging protocol can also be defined with a generator.
|
||||
A generator is a function we can write this way:
|
||||
|
||||
```ts
|
||||
@@ -93,6 +190,33 @@ function protocolGenerator({ servicesManager, commandsManager }) {
|
||||
|
||||
See the typescript definitions for more details on the structure of protocols.
|
||||
|
||||
## Additional viewports for layout - `defaultViewport`
|
||||
Sometimes the user manually selects a layout of a given size, say `2x3`. The
|
||||
hanging protocol can define what viewport options to use for this viewport by
|
||||
defining an extra viewport option in `defaultViewport`. For example:
|
||||
|
||||
```javascript
|
||||
defaultViewport: {
|
||||
viewportOptions: {
|
||||
viewportType: 'stack',
|
||||
toolGroupId: 'default',
|
||||
allowUnmatchedView: true,
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'defaultDisplaySetId',
|
||||
matchedDisplaySetsIndex: -1,
|
||||
},
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||
This allows defining the type of additional viewports, what tool group etc they
|
||||
are allowed in, and which display set is used to fill them. In the above case,
|
||||
the display set is the same as the other viewports, but the
|
||||
`matchedDisplaySetsIndex=-1`, so that means find the next matching display set
|
||||
from the display set selector which isn't already filling a view.
|
||||
|
||||
## Custom Attribute
|
||||
In some situations, you might want to match based on a custom attribute and not the DICOM tags. For instance,
|
||||
if you have assigned a `timepointId` to each study, and you want to match based on it.
|
||||
@@ -102,7 +226,7 @@ There are various ways that you can let `HangingProtocolService` know of you
|
||||
custom attribute. We will show how to add it inside the mode configuration.
|
||||
|
||||
```js
|
||||
const deafultProtocol = {
|
||||
const defaultProtocol = {
|
||||
id: 'defaultProtocol',
|
||||
/** ... **/
|
||||
protocolMatchingRules: [
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
sidebar_position: 8
|
||||
sidebar_label: State Sync Service
|
||||
---
|
||||
|
||||
# State Sync Service
|
||||
|
||||
## Overview
|
||||
The state sync service is designed to allow short and long term memory of things such as
|
||||
annotations applied, last annotation state, hanging protocol viewport state,
|
||||
window level etc. This allows for better interaction with things like navigation
|
||||
between hanging protocols, ensuring that the previously displayed layouts
|
||||
can be redisplayed after returning to a given hanging protocol.
|
||||
|
||||
Currently, all the state sync service configurations have one of the following two
|
||||
lifetimes. See the mode description for general information on the mode lifetime.
|
||||
|
||||
* Application load - when the application is restarted, the state is lost
|
||||
* `clearOnModeExit` - which stores state until the mode onModeExit is called, and then throws away the remaining state. This is useful for mode specific information.
|
||||
|
||||
### TODO work - add more storage locations
|
||||
It is expected to add a few more storage locations, which will store to various
|
||||
locations on updates:
|
||||
|
||||
* User specific server store - to store things between application restarts at the user level
|
||||
* Browser state store - to store things in the browser local state, to recover after crashing.
|
||||
* Study specific server store - to store things relevant to a given study between application restarts, on the server.
|
||||
|
||||
## Events
|
||||
|
||||
Currently the service does not fire events.
|
||||
|
||||
## API
|
||||
|
||||
- `register`: to create a new named state storage
|
||||
- `reduce`: to apply a set of changes to several states at once
|
||||
- `getState`: to retrieve the current state
|
||||
- `onModeExit`: clears the states configured as clearOnModeExit states
|
||||
|
||||
### register
|
||||
The register call is typically added to an extension to create a new
|
||||
syncable state. A typical call is shown below, registering the viewport
|
||||
grid store state as a modal state.
|
||||
|
||||
```javascript
|
||||
stateSyncService.register('viewportGridStore', { clearOnModeExit: true });
|
||||
```
|
||||
|
||||
### getState
|
||||
The `getState` call returns an object containing all of the reigstered states,
|
||||
by id. The values can be read directly, but should not be modified.
|
||||
|
||||
### reduce
|
||||
The `reduce` call is used to apply a set of updates to various states. The
|
||||
updates are performed for every state as a simply "set" call.
|
||||
|
||||
### onModeExit
|
||||
When the Mode is exited, the onModeExit is called on the sync state, and this
|
||||
clears all states registered with `clearOnModeExit: true`.
|
||||
To avoid clearing the state, the mode definition should store any transient
|
||||
state in the mode onModeExit and recover it in the `mode.onModeEnter`.
|
||||
|
||||
## OHIF Registered State
|
||||
There are a number of defined states here. It is recommended to update this
|
||||
list as states are added:
|
||||
|
||||
* `viewportGridStore` has viewport grid restore information for returning to an earlier grid layout.
|
||||
* `reuseIdMap` has a map of names to display sets for preserving user changes to hp display set selections.
|
||||
* `hanging` has a map of the hanging protocol stage information applied (HPInfo)
|
||||
* `presentationSync` has the cornerstone presentation state information
|
||||
* `toggleHangingProtocol` has the previously applied hanging protocol, to toggle an HP off.
|
||||
* `querySync` has the previously applied query information. Not fully implemented yet.
|
||||
@@ -33,6 +33,7 @@ button is clicked by the user.
|
||||
presets.
|
||||
- `commandName`: if tool has a command attached to run
|
||||
- `commandOptions`: arguments for the command.
|
||||
- `setActive`: Sets a given tool active (not as primary but as secondary)
|
||||
|
||||
- `reset`: reset the state of the toolbarService, set the primary tool to be
|
||||
`Wwwc` and unsubscribe tools that have registered their functions.
|
||||
|
||||
@@ -19,7 +19,8 @@ We maintain the following non-ui Services:
|
||||
- [Hanging Protocol Service](../data/HangingProtocolService.md)
|
||||
- [Toolbar Service](../data/ToolBarService.md)
|
||||
- [Measurement Service](../data/MeasurementService.md)
|
||||
- [Customization Service](customization-service.md)
|
||||
- [Customization Service](../data/customization-service.md)
|
||||
- [State Sync Service](../data/StateSyncService.md)
|
||||
- [Panel Service](../data/PanelService.md)
|
||||
|
||||
## Service Architecture
|
||||
|
||||
@@ -168,9 +168,11 @@ const SplitButton = ({
|
||||
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light'
|
||||
)}
|
||||
>
|
||||
<span className="mr-4">
|
||||
<Icon name={icon} className="w-5 h-5" />
|
||||
</span>
|
||||
{icon && (
|
||||
<span className="mr-4">
|
||||
<Icon name={icon} className="w-5 h-5" />
|
||||
</span>
|
||||
)}
|
||||
<span className="mr-5">{t(label)}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Icon } from '../';
|
||||
import { StringNumber } from '../../types';
|
||||
|
||||
/**
|
||||
*
|
||||
* Display a thumbnail for a display set.
|
||||
*/
|
||||
const Thumbnail = ({
|
||||
displaySetInstanceUID,
|
||||
@@ -16,11 +16,12 @@ const Thumbnail = ({
|
||||
description,
|
||||
seriesNumber,
|
||||
numInstances,
|
||||
countIcon,
|
||||
dragData,
|
||||
isActive,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
}) => {
|
||||
}): React.ReactNode => {
|
||||
// TODO: We should wrap our thumbnail to create a "DraggableThumbnail", as
|
||||
// this will still allow for "drag", even if there is no drop target for the
|
||||
// specified item.
|
||||
@@ -73,7 +74,8 @@ const Thumbnail = ({
|
||||
{seriesNumber}
|
||||
</div>
|
||||
<div className="flex flex-row items-center flex-1">
|
||||
<Icon name="group-layers" className="w-3 mr-2" /> {numInstances}
|
||||
<Icon name={countIcon || 'group-layers'} className="w-3 mr-2" />
|
||||
{` ${numInstances}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-base text-white break-all">{description}</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ const ThumbnailList = ({
|
||||
modality,
|
||||
componentType,
|
||||
seriesDate,
|
||||
countIcon,
|
||||
viewportIdentificator,
|
||||
isTracked,
|
||||
canReject,
|
||||
@@ -33,7 +34,6 @@ const ThumbnailList = ({
|
||||
const isActive = activeDisplaySetInstanceUIDs.includes(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
switch (componentType) {
|
||||
case 'thumbnail':
|
||||
return (
|
||||
@@ -44,6 +44,7 @@ const ThumbnailList = ({
|
||||
description={description}
|
||||
seriesNumber={seriesNumber}
|
||||
numInstances={numInstances}
|
||||
countIcon={countIcon}
|
||||
imageSrc={imageSrc}
|
||||
imageAltText={imageAltText}
|
||||
viewportIdentificator={viewportIdentificator}
|
||||
@@ -63,6 +64,7 @@ const ThumbnailList = ({
|
||||
description={description}
|
||||
seriesNumber={seriesNumber}
|
||||
numInstances={numInstances}
|
||||
countIcon={countIcon}
|
||||
imageSrc={imageSrc}
|
||||
imageAltText={imageAltText}
|
||||
viewportIdentificator={viewportIdentificator}
|
||||
|
||||
@@ -13,6 +13,7 @@ const ThumbnailTracked = ({
|
||||
description,
|
||||
seriesNumber,
|
||||
numInstances,
|
||||
countIcon,
|
||||
dragData,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
@@ -117,6 +118,7 @@ const ThumbnailTracked = ({
|
||||
description={description}
|
||||
seriesNumber={seriesNumber}
|
||||
numInstances={numInstances}
|
||||
countIcon={countIcon}
|
||||
isActive={isActive}
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
|
||||
@@ -39,6 +39,11 @@ const ToolbarButton = ({
|
||||
|
||||
const activeClass = isActive ? 'active' : '';
|
||||
const shouldShowDropdown = !!isActive && !!dropdownContent;
|
||||
const iconEl = icon ? (
|
||||
<Icon name={icon} />
|
||||
) : (
|
||||
<div>{label || 'Missing icon and label'}</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={id}>
|
||||
@@ -64,7 +69,7 @@ const ToolbarButton = ({
|
||||
id={id}
|
||||
{...rest}
|
||||
>
|
||||
<Icon name={icon} />
|
||||
{iconEl}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,16 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { LegacyViewportActionBar, Notification } from '../';
|
||||
|
||||
const Viewport = ({ viewportIndex, onArrowsClick, studyData, children }) => {
|
||||
const Viewport = ({
|
||||
viewportId,
|
||||
viewportIndex,
|
||||
onArrowsClick,
|
||||
studyData,
|
||||
children,
|
||||
}) => {
|
||||
if (!viewportId) {
|
||||
viewportId = `viewport-${viewportIndex}`;
|
||||
}
|
||||
return (
|
||||
<div className="relative flex flex-col h-full">
|
||||
<div className="absolute top-0 left-0 w-full">
|
||||
@@ -39,7 +48,7 @@ const Viewport = ({ viewportIndex, onArrowsClick, studyData, children }) => {
|
||||
</div>
|
||||
|
||||
{/* STUDY IMAGE */}
|
||||
<div className="w-full h-full" id={`viewport-${viewportIndex}`}>
|
||||
<div className="w-full h-full" id={viewportId}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,17 +6,22 @@ import React, {
|
||||
useReducer,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import isEqual from 'lodash.isequal';
|
||||
import viewportLabels from '../utils/viewportLabels';
|
||||
import getPresentationId from './getPresentationId';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
numRows: null,
|
||||
numCols: null,
|
||||
layoutType: 'grid',
|
||||
activeViewportIndex: 0,
|
||||
layout: {
|
||||
numRows: 0,
|
||||
numCols: 0,
|
||||
layoutType: 'grid',
|
||||
},
|
||||
viewports: [
|
||||
{
|
||||
displaySetInstanceUIDs: [],
|
||||
viewportOptions: {},
|
||||
displaySetSelectors: [],
|
||||
displaySetOptions: [{}],
|
||||
x: 0, // left
|
||||
y: 0, // top
|
||||
@@ -25,20 +30,58 @@ const DEFAULT_STATE = {
|
||||
viewportLabel: null,
|
||||
},
|
||||
],
|
||||
activeViewportIndex: 0,
|
||||
cachedLayout: {},
|
||||
};
|
||||
|
||||
export const ViewportGridContext = createContext(DEFAULT_STATE);
|
||||
|
||||
/**
|
||||
* Given the flatten index, and rows and column, it returns the
|
||||
* row and column index
|
||||
* Find a viewport to re-use, and then set the viewportId
|
||||
*
|
||||
* @param idSet
|
||||
* @param viewport
|
||||
* @param stateViewports
|
||||
* @returns
|
||||
*/
|
||||
const unravelIndex = (index, numRows, numCols) => {
|
||||
const row = Math.floor(index / numCols);
|
||||
const col = index % numCols;
|
||||
return { row, col };
|
||||
const reuseViewport = (idSet, viewport, stateViewports) => {
|
||||
const oldIds = {};
|
||||
for (const oldViewport of stateViewports) {
|
||||
const { viewportId: oldId } = oldViewport;
|
||||
oldIds[oldId] = true;
|
||||
if (!oldId || idSet[oldId]) continue;
|
||||
if (
|
||||
!isEqual(
|
||||
oldViewport.displaySetInstanceUIDs,
|
||||
viewport.displaySetInstanceUIDs
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
idSet[oldId] = true;
|
||||
// TODO re-use viewports once the flickering/wrong size redraw is fixed
|
||||
// return {
|
||||
// ...oldViewport,
|
||||
// ...viewport,
|
||||
// viewportOptions: {
|
||||
// ...oldViewport.viewportOptions,
|
||||
|
||||
// viewportId: oldViewport.viewportId,
|
||||
// },
|
||||
// };
|
||||
}
|
||||
// Find a viewport instance number different from earlier viewports having
|
||||
// the same presentationId as this one would - will be less than 10k
|
||||
// viewports hopefully :-)
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
const viewportId = 'viewport-' + i;
|
||||
if (idSet[viewportId] || oldIds[viewportId]) continue;
|
||||
idSet[viewportId] = true;
|
||||
return {
|
||||
...viewport,
|
||||
viewportId,
|
||||
viewportOptions: { ...viewport.viewportOptions, viewportId },
|
||||
};
|
||||
}
|
||||
throw new Error('No ID found');
|
||||
};
|
||||
|
||||
export function ViewportGridProvider({ children, service }) {
|
||||
@@ -59,26 +102,40 @@ export function ViewportGridProvider({ children, service }) {
|
||||
// which might have been a PDF Viewport. The viewport itself
|
||||
// will deal with inheritance if required. Here is just a simple
|
||||
// provider.
|
||||
const viewportOptions = payload.viewportOptions || {};
|
||||
const displaySetOptions = payload.displaySetOptions || [{}];
|
||||
const viewport = state.viewports[viewportIndex] || {};
|
||||
const viewportOptions = { ...payload.viewportOptions };
|
||||
|
||||
const displaySetOptions = payload.displaySetOptions || [];
|
||||
if (displaySetOptions.length === 0) {
|
||||
// Only copy index 0, as that is all that is currently supported by this
|
||||
// method call.
|
||||
displaySetOptions.push({ ...viewport.displaySetOptions?.[0] });
|
||||
}
|
||||
|
||||
const viewports = state.viewports.slice();
|
||||
|
||||
if (!viewportOptions.viewportId) {
|
||||
viewportOptions.viewportId = `viewport-${viewportIndex}`;
|
||||
}
|
||||
|
||||
// merge the displaySetOptions and viewportOptions and displaySetInstanceUIDs
|
||||
// into the viewport object at the given index
|
||||
viewports[viewportIndex] = {
|
||||
...viewports[viewportIndex],
|
||||
let newView = {
|
||||
...viewport,
|
||||
displaySetInstanceUIDs,
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
viewportLabel: viewportLabels[viewportIndex],
|
||||
};
|
||||
viewportOptions.presentationId = getPresentationId(newView, viewports);
|
||||
|
||||
return { ...state, ...{ viewports } };
|
||||
// Make sure we assign a viewport id
|
||||
newView = reuseViewport({}, newView, state.viewports);
|
||||
console.log(
|
||||
'Creating new viewport',
|
||||
viewportIndex,
|
||||
newView.viewportOptions.viewportId,
|
||||
displaySetInstanceUIDs,
|
||||
displaySetOptions
|
||||
);
|
||||
|
||||
viewports[viewportIndex] = newView;
|
||||
|
||||
return { ...state, viewports };
|
||||
}
|
||||
case 'SET_LAYOUT': {
|
||||
const {
|
||||
@@ -86,108 +143,96 @@ export function ViewportGridProvider({ children, service }) {
|
||||
numRows,
|
||||
layoutOptions,
|
||||
layoutType = 'grid',
|
||||
keepExtraViewports = false,
|
||||
findOrCreateViewport,
|
||||
} = action.payload;
|
||||
|
||||
// If empty viewportOptions, we use numRow and numCols to calculate number of viewports
|
||||
const numPanes = layoutOptions.length || numRows * numCols;
|
||||
const viewports = state.viewports.slice();
|
||||
const activeViewportIndex =
|
||||
state.activeViewportIndex >= numPanes ? 0 : state.activeViewportIndex;
|
||||
const hasOptions = layoutOptions?.length;
|
||||
const viewports = [];
|
||||
|
||||
while (viewports.length < numPanes) {
|
||||
viewports.push({});
|
||||
}
|
||||
// Options is a temporary state store which can be used by the
|
||||
// findOrCreate to store state about already found viewports. Typically,
|
||||
// it will be used to store the display set UID's which are already
|
||||
// in view so that the find or create can decide which display sets
|
||||
// haven't been viewed yet, and add them in the appropriate order.
|
||||
const options = {};
|
||||
|
||||
// Extra viewports are kept when the grid layout is changed in the UI
|
||||
// because the user populated those viewports and if the viewports were to
|
||||
// return on screen their contents should be maintained.
|
||||
if (!keepExtraViewports) {
|
||||
while (viewports.length > numPanes) {
|
||||
viewports.pop();
|
||||
let activeViewportIndex;
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
const pos = col + row * numCols;
|
||||
const layoutOption = layoutOptions[pos];
|
||||
const positionId = layoutOption?.positionId || `${col}-${row}`;
|
||||
if (hasOptions && pos >= layoutOptions.length) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!activeViewportIndex ||
|
||||
state.viewports[pos]?.positionId === positionId
|
||||
) {
|
||||
activeViewportIndex = pos;
|
||||
}
|
||||
const viewport = findOrCreateViewport(pos, positionId, options);
|
||||
if (!viewport) continue;
|
||||
viewport.positionId = positionId;
|
||||
// Create a new viewport object as it is getting updated here
|
||||
// and it is part of the read only state
|
||||
viewports.push(viewport);
|
||||
let xPos, yPos, w, h;
|
||||
|
||||
if (layoutOptions && layoutOptions[pos]) {
|
||||
({ x: xPos, y: yPos, width: w, height: h } = layoutOptions[pos]);
|
||||
} else {
|
||||
w = 1 / numCols;
|
||||
h = 1 / numRows;
|
||||
xPos = col * w;
|
||||
yPos = row * h;
|
||||
}
|
||||
|
||||
viewport.width = w;
|
||||
viewport.height = h;
|
||||
viewport.x = xPos;
|
||||
viewport.y = yPos;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < numPanes; i++) {
|
||||
let xPos, yPos, w, h;
|
||||
|
||||
if (layoutOptions && layoutOptions[i]) {
|
||||
({ x: xPos, y: yPos, width: w, height: h } = layoutOptions[i]);
|
||||
} else {
|
||||
const { row, col } = unravelIndex(i, numRows, numCols);
|
||||
w = 1 / numCols;
|
||||
h = 1 / numRows;
|
||||
xPos = col * w;
|
||||
yPos = row * h;
|
||||
const viewportIdSet = {};
|
||||
for (
|
||||
let viewportIndex = 0;
|
||||
viewportIndex < viewports.length;
|
||||
viewportIndex++
|
||||
) {
|
||||
const viewport = reuseViewport(
|
||||
viewportIdSet,
|
||||
viewports[viewportIndex],
|
||||
state.viewports
|
||||
);
|
||||
if (!viewport.viewportOptions.presentationId) {
|
||||
viewport.viewportOptions.presentationId = getPresentationId(
|
||||
viewport,
|
||||
viewports
|
||||
);
|
||||
}
|
||||
|
||||
viewports[i].width = w;
|
||||
viewports[i].height = h;
|
||||
viewports[i].x = xPos;
|
||||
viewports[i].y = yPos;
|
||||
viewport.viewportIndex = viewportIndex;
|
||||
viewport.viewportLabel = viewportLabels[viewportIndex];
|
||||
viewports[viewportIndex] = viewport;
|
||||
}
|
||||
|
||||
return {
|
||||
const ret = {
|
||||
...state,
|
||||
...{
|
||||
activeViewportIndex,
|
||||
activeViewportIndex,
|
||||
layout: {
|
||||
...state.layout,
|
||||
numCols,
|
||||
numRows,
|
||||
layoutType,
|
||||
viewports,
|
||||
},
|
||||
viewports,
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
case 'RESET': {
|
||||
return {
|
||||
numCols: null,
|
||||
numRows: null,
|
||||
layoutType: 'grid',
|
||||
activeViewportIndex: 0,
|
||||
viewports: [
|
||||
{
|
||||
displaySetInstanceUIDs: [],
|
||||
displaySetOptions: [],
|
||||
viewportOptions: {},
|
||||
x: 0, // left
|
||||
y: 0, // top
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
],
|
||||
cachedLayout: {},
|
||||
};
|
||||
}
|
||||
|
||||
// The SET_CACHE_LAYOUT action can be used for caching a layout
|
||||
// for instance double clicking a viewport to maximize it.
|
||||
// and then restoring the previous layout when the viewport is
|
||||
// double clicked again.
|
||||
case 'SET_CACHED_LAYOUT': {
|
||||
const { cacheId, cachedLayout } = action.payload;
|
||||
|
||||
// deep copy the cachedLayout into the state
|
||||
return {
|
||||
...state,
|
||||
cachedLayout: {
|
||||
...state.cachedLayout,
|
||||
[cacheId]: JSON.parse(JSON.stringify(cachedLayout)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'RESTORE_CACHED_LAYOUT': {
|
||||
const cacheId = action.payload;
|
||||
|
||||
if (!state.cachedLayout[cacheId]) {
|
||||
console.warn(
|
||||
`No cached layout found for cacheId: ${cacheId}. Ignoring...`
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
const cachedLayout = state.cachedLayout;
|
||||
return { ...state.cachedLayout[cacheId], cachedLayout };
|
||||
return DEFAULT_STATE;
|
||||
}
|
||||
|
||||
case 'SET': {
|
||||
@@ -221,6 +266,7 @@ export function ViewportGridProvider({ children, service }) {
|
||||
viewportIndex,
|
||||
displaySetInstanceUIDs,
|
||||
viewportOptions,
|
||||
displaySetSelectors,
|
||||
displaySetOptions,
|
||||
}) =>
|
||||
dispatch({
|
||||
@@ -229,6 +275,7 @@ export function ViewportGridProvider({ children, service }) {
|
||||
viewportIndex,
|
||||
displaySetInstanceUIDs,
|
||||
viewportOptions,
|
||||
displaySetSelectors,
|
||||
displaySetOptions,
|
||||
},
|
||||
}),
|
||||
@@ -250,7 +297,7 @@ export function ViewportGridProvider({ children, service }) {
|
||||
numRows,
|
||||
numCols,
|
||||
layoutOptions = [],
|
||||
keepExtraViewports = false,
|
||||
findOrCreateViewport,
|
||||
}) =>
|
||||
dispatch({
|
||||
type: 'SET_LAYOUT',
|
||||
@@ -259,7 +306,7 @@ export function ViewportGridProvider({ children, service }) {
|
||||
numRows,
|
||||
numCols,
|
||||
layoutOptions,
|
||||
keepExtraViewports,
|
||||
findOrCreateViewport,
|
||||
},
|
||||
}),
|
||||
[dispatch]
|
||||
@@ -274,25 +321,6 @@ export function ViewportGridProvider({ children, service }) {
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setCachedLayout = useCallback(
|
||||
payload =>
|
||||
dispatch({
|
||||
type: 'SET_CACHED_LAYOUT',
|
||||
payload,
|
||||
}),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const restoreCachedLayout = useCallback(
|
||||
cacheId => {
|
||||
dispatch({
|
||||
type: 'RESTORE_CACHED_LAYOUT',
|
||||
payload: cacheId,
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const set = useCallback(
|
||||
payload =>
|
||||
dispatch({
|
||||
@@ -303,7 +331,8 @@ export function ViewportGridProvider({ children, service }) {
|
||||
);
|
||||
|
||||
const getNumViewportPanes = useCallback(() => {
|
||||
const { numCols, numRows, viewports } = viewportGridState;
|
||||
const { layout, viewports } = viewportGridState;
|
||||
const { numRows, numCols } = layout;
|
||||
return Math.min(viewports.length, numCols * numRows);
|
||||
}, [viewportGridState]);
|
||||
|
||||
@@ -322,8 +351,6 @@ export function ViewportGridProvider({ children, service }) {
|
||||
setLayout,
|
||||
reset,
|
||||
onModeExit: reset,
|
||||
setCachedLayout,
|
||||
restoreCachedLayout,
|
||||
set,
|
||||
getNumViewportPanes,
|
||||
});
|
||||
@@ -336,8 +363,6 @@ export function ViewportGridProvider({ children, service }) {
|
||||
setDisplaySetsForViewports,
|
||||
setLayout,
|
||||
reset,
|
||||
setCachedLayout,
|
||||
restoreCachedLayout,
|
||||
set,
|
||||
getNumViewportPanes,
|
||||
]);
|
||||
@@ -348,8 +373,6 @@ export function ViewportGridProvider({ children, service }) {
|
||||
setDisplaySetsForViewport,
|
||||
setDisplaySetsForViewports,
|
||||
setLayout,
|
||||
setCachedLayout,
|
||||
restoreCachedLayout,
|
||||
reset,
|
||||
set,
|
||||
getNumViewportPanes,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Selects a presentation ID to use for this viewport.
|
||||
* This is done to allow the same display set to be displayed more than once
|
||||
* on screen, with different attributes such as window level and initial position.
|
||||
* Then, when redisplaying that, the nearest/most common attribute is re-used.
|
||||
*
|
||||
* For example, for display set <displaySetUID>, in a viewport of type volume,
|
||||
* the generated presentationID might be
|
||||
* `volume:axial:<displaySetUID>`. This can then be used to store and retrieve
|
||||
* presentation information in state sync service 'presentationSync' state.
|
||||
*
|
||||
* The generated value attempts to generate a unique value for every type
|
||||
* of viewport which should have it's own presentation information. Thus, the
|
||||
* following values are used for presentation ID:
|
||||
*
|
||||
* 1. viewportType - since the presentation information for a volume is different than for a stack
|
||||
* 2. orientation - since the camera is different for different orientations
|
||||
* 3. display set instance UID - since different display sets should get displayed differently
|
||||
* 4. instance count - since displaying the same series twice should allow applying different window level etc
|
||||
*
|
||||
* @param viewport requiring a presentation Id
|
||||
* @param viewports is the list of viewports being shown. Any presentation ID's
|
||||
* among them must not be re-used in order to have each viewport have it's own presentation ID.
|
||||
* @returns Presentation ID id, or undefined if nothing displayed
|
||||
*/
|
||||
const getPresentationId = (viewport, viewports): string => {
|
||||
if (!viewport) return;
|
||||
const { viewportOptions, displaySetInstanceUIDs } = viewport;
|
||||
if (!viewportOptions || !displaySetInstanceUIDs?.length) {
|
||||
console.log('No viewport type or display sets in', viewport);
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportType = viewportOptions.viewportType || 'stack';
|
||||
const idArr = [viewportType, 0, ...displaySetInstanceUIDs];
|
||||
if (viewportOptions.orientation) {
|
||||
idArr.splice(2, 0, viewportOptions.orientation);
|
||||
}
|
||||
|
||||
// Allow setting a custom presentation prefix in the hanging protocol
|
||||
// This allows defining new
|
||||
// presentation groups to be set automatically when one knows that the
|
||||
// same display set will be displayed in different ways.
|
||||
// This is the recommended way to manage a hanging protocol which displays
|
||||
// multiple views of a single display set, eg to display brain, bone, soft
|
||||
// tissue views in different viewports.
|
||||
if (viewportOptions.presentationPrefix) {
|
||||
idArr.push(viewportOptions.presentationPrefix);
|
||||
}
|
||||
if (!viewports) {
|
||||
console.log('viewports not defined', idArr.join(','));
|
||||
return idArr.join('&');
|
||||
}
|
||||
|
||||
// This code finds the first unique index to add to the presentation id so that
|
||||
// two viewports containing the same display set in the same type of viewport
|
||||
// can have different presentation information. This allows comparison of
|
||||
// a single display set in two or more viewports, when the user has simply
|
||||
// dragged and dropped the view in twice. For example, it allows displaying
|
||||
// bone, brain and soft tissue views of a single display set, and to still
|
||||
// remember the specific changes to each viewport.
|
||||
for (let displayInstance = 0; displayInstance < 128; displayInstance++) {
|
||||
idArr[1] = displayInstance;
|
||||
const testId = idArr.join('&');
|
||||
if (!viewports.find(it => it.viewportOptions?.presentationId === testId)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const id = idArr.join('&');
|
||||
return id;
|
||||
};
|
||||
|
||||
export default getPresentationId;
|
||||
@@ -0,0 +1,31 @@
|
||||
describe('OHIF HP', () => {
|
||||
beforeEach(() => {
|
||||
cy.checkStudyRouteInViewer(
|
||||
'1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1',
|
||||
'&hangingProtocolId=@ohif/hp-extension.mn'
|
||||
);
|
||||
cy.expectMinimumThumbnails(3);
|
||||
cy.initCornerstoneToolsAliases();
|
||||
cy.initCommonElementsAliases();
|
||||
});
|
||||
|
||||
it('Should display 3 up', () => {
|
||||
cy.get('[data-cy="viewport-pane"]')
|
||||
.its('length')
|
||||
.should('be.eq', 3);
|
||||
});
|
||||
|
||||
it('Should navigate next/previous stage', () => {
|
||||
cy.get('body').type(',');
|
||||
cy.wait(250);
|
||||
cy.get('[data-cy="viewport-pane"]')
|
||||
.its('length')
|
||||
.should('be.eq', 4);
|
||||
|
||||
cy.get('body').type('..');
|
||||
cy.wait(250);
|
||||
cy.get('[data-cy="viewport-pane"]')
|
||||
.its('length')
|
||||
.should('be.eq', 2);
|
||||
});
|
||||
});
|
||||
@@ -17,8 +17,21 @@ describe('OHIF Study Viewer Page', function() {
|
||||
});
|
||||
|
||||
it('drags and drop a series thumbnail into viewport', function() {
|
||||
cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)') //element to be dragged
|
||||
.drag('.cornerstone-canvas'); //dropzone element
|
||||
// Can't use the native drag version as the element should be rerendered
|
||||
// cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)') //element to be dragged
|
||||
// .drag('.cornerstone-canvas'); //dropzone element
|
||||
|
||||
const dataTransfer = new DataTransfer();
|
||||
|
||||
cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)')
|
||||
.first()
|
||||
.trigger('mousedown', { which: 1, button: 0 })
|
||||
.trigger('dragstart', { dataTransfer })
|
||||
.trigger('drag', {});
|
||||
cy.get('.cornerstone-canvas')
|
||||
.trigger('mousemove', 'center')
|
||||
.trigger('dragover', { dataTransfer, force: true })
|
||||
.trigger('drop', { dataTransfer, force: true });
|
||||
|
||||
//const expectedText =
|
||||
// 'Ser: 2Img: 1 1/13512 x 512Loc: -17.60 mm Thick: 3.00 mm';
|
||||
|
||||
@@ -54,20 +54,29 @@ Cypress.Commands.add('openStudy', PatientName => {
|
||||
.click({ force: true });
|
||||
});
|
||||
|
||||
Cypress.Commands.add('checkStudyRouteInViewer', StudyInstanceUID => {
|
||||
cy.location('pathname').then($url => {
|
||||
cy.log($url);
|
||||
if ($url == 'blank' || !$url.includes(`/basic-test/${StudyInstanceUID}`)) {
|
||||
cy.openStudyInViewer(StudyInstanceUID);
|
||||
cy.waitDicomImage();
|
||||
cy.wait(2000);
|
||||
}
|
||||
});
|
||||
});
|
||||
Cypress.Commands.add(
|
||||
'checkStudyRouteInViewer',
|
||||
(StudyInstanceUID, otherParams = '') => {
|
||||
cy.location('pathname').then($url => {
|
||||
cy.log($url);
|
||||
if (
|
||||
$url == 'blank' ||
|
||||
!$url.includes(`/basic-test/${StudyInstanceUID}${otherParams}`)
|
||||
) {
|
||||
cy.openStudyInViewer(StudyInstanceUID, otherParams);
|
||||
cy.waitDicomImage();
|
||||
cy.wait(2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
Cypress.Commands.add('openStudyInViewer', StudyInstanceUID => {
|
||||
cy.visit(`/basic-test?StudyInstanceUIDs=${StudyInstanceUID}`);
|
||||
});
|
||||
Cypress.Commands.add(
|
||||
'openStudyInViewer',
|
||||
(StudyInstanceUID, otherParams = '') => {
|
||||
cy.visit(`/basic-test?StudyInstanceUIDs=${StudyInstanceUID}${otherParams}`);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Command to search for a Modality and open the study.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
window.config = {
|
||||
// Activate the new HP mode....
|
||||
isNewHP: true,
|
||||
|
||||
routerBasename: '/',
|
||||
customizationService: [
|
||||
'@ohif/extension-default.customizationModule.datasources',
|
||||
@@ -59,25 +62,6 @@ window.config = {
|
||||
singlepart: 'bulkdata,video,pdf',
|
||||
},
|
||||
},
|
||||
{
|
||||
friendlyName: 'dcmjs DICOMWeb Server',
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'shared',
|
||||
configuration: {
|
||||
name: 'shared',
|
||||
qidoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
|
||||
wadoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb',
|
||||
qidoSupportsIncludeField: false,
|
||||
supportsReject: false,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
supportsFuzzyMatching: false,
|
||||
supportsWildcard: true,
|
||||
staticWado: true,
|
||||
singlepart: 'bulkdata,video,pdf',
|
||||
},
|
||||
},
|
||||
{
|
||||
friendlyName: 'E2E Test Data',
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
@@ -122,92 +106,7 @@ window.config = {
|
||||
console.warn('test, navigate to https://ohif.org/');
|
||||
},
|
||||
defaultDataSourceName: 'default',
|
||||
hotkeys: [
|
||||
{
|
||||
commandName: 'incrementActiveViewport',
|
||||
label: 'Next Viewport',
|
||||
keys: ['right'],
|
||||
},
|
||||
{
|
||||
commandName: 'decrementActiveViewport',
|
||||
label: 'Previous Viewport',
|
||||
keys: ['left'],
|
||||
},
|
||||
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
|
||||
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
|
||||
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
|
||||
{
|
||||
commandName: 'flipViewportHorizontal',
|
||||
label: 'Flip Horizontally',
|
||||
keys: ['h'],
|
||||
},
|
||||
{
|
||||
commandName: 'flipViewportVertical',
|
||||
label: 'Flip Vertically',
|
||||
keys: ['v'],
|
||||
},
|
||||
{ commandName: 'scaleUpViewport', label: 'Zoom In', keys: ['+'] },
|
||||
{ commandName: 'scaleDownViewport', label: 'Zoom Out', keys: ['-'] },
|
||||
{ commandName: 'fitViewportToWindow', label: 'Zoom to Fit', keys: ['='] },
|
||||
{ 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: 'setZoomTool', label: 'Zoom', keys: ['z'] },
|
||||
// ~ Window level presets
|
||||
{
|
||||
commandName: 'windowLevelPreset1',
|
||||
label: 'W/L Preset 1',
|
||||
keys: ['1'],
|
||||
},
|
||||
{
|
||||
commandName: 'windowLevelPreset2',
|
||||
label: 'W/L Preset 2',
|
||||
keys: ['2'],
|
||||
},
|
||||
{
|
||||
commandName: 'windowLevelPreset3',
|
||||
label: 'W/L Preset 3',
|
||||
keys: ['3'],
|
||||
},
|
||||
{
|
||||
commandName: 'windowLevelPreset4',
|
||||
label: 'W/L Preset 4',
|
||||
keys: ['4'],
|
||||
},
|
||||
{
|
||||
commandName: 'windowLevelPreset5',
|
||||
label: 'W/L Preset 5',
|
||||
keys: ['5'],
|
||||
},
|
||||
{
|
||||
commandName: 'windowLevelPreset6',
|
||||
label: 'W/L Preset 6',
|
||||
keys: ['6'],
|
||||
},
|
||||
{
|
||||
commandName: 'windowLevelPreset7',
|
||||
label: 'W/L Preset 7',
|
||||
keys: ['7'],
|
||||
},
|
||||
{
|
||||
commandName: 'windowLevelPreset8',
|
||||
label: 'W/L Preset 8',
|
||||
keys: ['8'],
|
||||
},
|
||||
{
|
||||
commandName: 'windowLevelPreset9',
|
||||
label: 'W/L Preset 9',
|
||||
keys: ['9'],
|
||||
},
|
||||
],
|
||||
|
||||
// Only list the unique hotkeys
|
||||
hotkeys: [],
|
||||
};
|
||||
+20
-12
@@ -5,7 +5,12 @@ import i18n from '@ohif/i18n';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import Compose from './routes/Mode/Compose';
|
||||
|
||||
import {
|
||||
ServicesManager,
|
||||
ExtensionManager,
|
||||
CommandsManager,
|
||||
HotkeysManager,
|
||||
} from '@ohif/core';
|
||||
import {
|
||||
DialogProvider,
|
||||
Modal,
|
||||
@@ -24,7 +29,10 @@ import createRoutes from './routes';
|
||||
import appInit from './appInit.js';
|
||||
import OpenIdConnectRoutes from './utils/OpenIdConnectRoutes';
|
||||
|
||||
let commandsManager, extensionManager, servicesManager, hotkeysManager;
|
||||
let commandsManager: CommandsManager,
|
||||
extensionManager: ExtensionManager,
|
||||
servicesManager: ServicesManager,
|
||||
hotkeysManager: HotkeysManager;
|
||||
|
||||
function App({ config, defaultExtensions, defaultModes }) {
|
||||
const [init, setInit] = useState(null);
|
||||
@@ -59,12 +67,12 @@ function App({ config, defaultExtensions, defaultModes }) {
|
||||
} = appConfigState;
|
||||
|
||||
const {
|
||||
UIDialogService,
|
||||
uiDialogService,
|
||||
uiModalService,
|
||||
UINotificationService,
|
||||
UIViewportDialogService,
|
||||
ViewportGridService,
|
||||
CineService,
|
||||
uiNotificationService,
|
||||
uiViewportDialogService,
|
||||
viewportGridService,
|
||||
cineService,
|
||||
userAuthenticationService,
|
||||
customizationService,
|
||||
} = servicesManager.services;
|
||||
@@ -74,11 +82,11 @@ function App({ config, defaultExtensions, defaultModes }) {
|
||||
[UserAuthenticationProvider, { service: userAuthenticationService }],
|
||||
[I18nextProvider, { i18n }],
|
||||
[ThemeWrapper],
|
||||
[ViewportGridProvider, { service: ViewportGridService }],
|
||||
[ViewportDialogProvider, { service: UIViewportDialogService }],
|
||||
[CineProvider, { service: CineService }],
|
||||
[SnackbarProvider, { service: UINotificationService }],
|
||||
[DialogProvider, { service: UIDialogService }],
|
||||
[ViewportGridProvider, { service: viewportGridService }],
|
||||
[ViewportDialogProvider, { service: uiViewportDialogService }],
|
||||
[CineProvider, { service: cineService }],
|
||||
[SnackbarProvider, { service: uiNotificationService }],
|
||||
[DialogProvider, { service: uiDialogService }],
|
||||
[ModalProvider, { service: uiModalService, modal: Modal }],
|
||||
];
|
||||
const CombinedProviders = ({ children }) =>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
UIDialogService,
|
||||
UIViewportDialogService,
|
||||
MeasurementService,
|
||||
StateSyncService,
|
||||
DisplaySetService,
|
||||
ToolbarService,
|
||||
ViewportGridService,
|
||||
@@ -60,6 +61,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
|
||||
CineService.REGISTRATION,
|
||||
UserAuthenticationService.REGISTRATION,
|
||||
PanelService.REGISTRATION,
|
||||
StateSyncService.REGISTRATION,
|
||||
]);
|
||||
|
||||
errorHandler.getHTTPErrorHandler = () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ServicesManager } from '@ohif/core';
|
||||
import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui';
|
||||
import { utils } from '@ohif/core';
|
||||
import EmptyViewport from './EmptyViewport';
|
||||
@@ -22,11 +23,28 @@ const ORIENTATION_MAP = {
|
||||
},
|
||||
};
|
||||
|
||||
const createHpInfo = (protocol, stage, activeStudyUID) => {
|
||||
return {
|
||||
hangingProtocolId: protocol.id,
|
||||
stageId: stage.stageId,
|
||||
stageIdx: protocol.stages.findIndex(it => it === stage),
|
||||
activeStudyUID,
|
||||
};
|
||||
};
|
||||
|
||||
const compareViewportOptions = (opts1, opts2) => {
|
||||
if ((opts1.viewportType || 'stack') != opts2.viewportType) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
function ViewerViewportGrid(props) {
|
||||
const { servicesManager, viewportComponents, dataSource } = props;
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
|
||||
const { numCols, numRows, activeViewportIndex, viewports } = viewportGrid;
|
||||
const { layout, activeViewportIndex, viewports } = viewportGrid;
|
||||
const { numCols, numRows } = layout;
|
||||
|
||||
// TODO -> Need some way of selecting which displaySets hit the viewports.
|
||||
const {
|
||||
@@ -34,124 +52,77 @@ function ViewerViewportGrid(props) {
|
||||
measurementService,
|
||||
hangingProtocolService,
|
||||
uiNotificationService,
|
||||
} = servicesManager.services;
|
||||
} = (servicesManager as ServicesManager).services;
|
||||
|
||||
/**
|
||||
* This callback runs only after displaySets have changed (created and added or modified)
|
||||
* This callback runs after the viewports structure has changed in any way.
|
||||
* On initial display, that means if it has changed by applying a HangingProtocol,
|
||||
* while subsequently it may mean by changing the stage or by manually adjusting
|
||||
* the layout.
|
||||
|
||||
*/
|
||||
const updateDisplaySetsForViewports = useCallback(
|
||||
availableDisplaySets => {
|
||||
if (!availableDisplaySets.length) {
|
||||
const updateDisplaySetsFromProtocol = (
|
||||
protocol,
|
||||
stage,
|
||||
activeStudyUID,
|
||||
viewportMatchDetails
|
||||
) => {
|
||||
const availableDisplaySets = displaySetService.getActiveDisplaySets();
|
||||
|
||||
if (!availableDisplaySets.length) {
|
||||
console.log('No available display sets', availableDisplaySets);
|
||||
return;
|
||||
}
|
||||
|
||||
// Match each viewport individually
|
||||
const { layoutType } = stage.viewportStructure;
|
||||
const stageProps = stage.viewportStructure.properties;
|
||||
const { columns: numCols, rows: numRows, layoutOptions = [] } = stageProps;
|
||||
|
||||
/**
|
||||
* This find or create viewport uses the hanging protocol results to
|
||||
* specify the viewport match details, which specifies the size and
|
||||
* setup of the various viewports.
|
||||
*/
|
||||
const findOrCreateViewport = viewportIndex => {
|
||||
const details = viewportMatchDetails.get(viewportIndex);
|
||||
if (!details) {
|
||||
console.log('No match details for viewport', viewportIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
viewportMatchDetails,
|
||||
hpAlreadyApplied,
|
||||
} = hangingProtocolService.getMatchDetails();
|
||||
|
||||
if (!viewportMatchDetails.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gridDisplaySetUIDs = [];
|
||||
const blankViewportIndices = [];
|
||||
|
||||
// Match each viewport individually.
|
||||
const numViewports = viewportGridService.getNumViewportPanes();
|
||||
|
||||
for (
|
||||
let viewportIndex = 0;
|
||||
viewportIndex < numViewports;
|
||||
viewportIndex++
|
||||
) {
|
||||
const viewportDisplaySetUIDs =
|
||||
viewports[viewportIndex]?.displaySetInstanceUIDs ?? [];
|
||||
|
||||
if (hpAlreadyApplied.get(viewportIndex)) {
|
||||
gridDisplaySetUIDs.push(...viewportDisplaySetUIDs);
|
||||
continue;
|
||||
}
|
||||
|
||||
// if current viewport doesn't have a match
|
||||
if (viewportMatchDetails.get(viewportIndex) === undefined) {
|
||||
// if the current viewport is empty/blank
|
||||
if (viewportDisplaySetUIDs.length === 0) {
|
||||
blankViewportIndices.push(viewportIndex);
|
||||
} else {
|
||||
gridDisplaySetUIDs.push(...viewportDisplaySetUIDs);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const { displaySetsInfo, viewportOptions } = viewportMatchDetails.get(
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
const displaySetUIDsToHang = [];
|
||||
const displaySetUIDsToHangOptions = [];
|
||||
displaySetsInfo.forEach(
|
||||
({ displaySetInstanceUID, displaySetOptions }) => {
|
||||
if (!displaySetInstanceUID) {
|
||||
return;
|
||||
}
|
||||
const { displaySetsInfo, viewportOptions } = details;
|
||||
const displaySetUIDsToHang = [];
|
||||
const displaySetUIDsToHangOptions = [];
|
||||
|
||||
displaySetsInfo.forEach(
|
||||
({ displaySetInstanceUID, displaySetOptions }) => {
|
||||
if (displaySetInstanceUID) {
|
||||
displaySetUIDsToHang.push(displaySetInstanceUID);
|
||||
displaySetUIDsToHangOptions.push(displaySetOptions);
|
||||
}
|
||||
);
|
||||
|
||||
gridDisplaySetUIDs.push(...displaySetUIDsToHang);
|
||||
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportIndex: viewportIndex,
|
||||
displaySetInstanceUIDs: displaySetUIDsToHang,
|
||||
viewportOptions,
|
||||
displaySetOptions: displaySetUIDsToHangOptions,
|
||||
});
|
||||
|
||||
// During setting displaySets for viewport, we need to update the hanging protocol
|
||||
// but some viewports contain more than one display set (fusion), and their displaySet
|
||||
// will not be available at the time of setting displaySets for viewport. So we need to
|
||||
// update the hanging protocol after making sure all the matched display sets are available
|
||||
// and set on the viewport
|
||||
if (displaySetUIDsToHang.length === displaySetsInfo.length) {
|
||||
// The following will set the viewportsDisplaySetsMatched state
|
||||
|
||||
const suppressEvent = true;
|
||||
const applied = true;
|
||||
hangingProtocolService.setHangingProtocolAppliedForViewport(
|
||||
viewportIndex,
|
||||
applied,
|
||||
suppressEvent
|
||||
);
|
||||
displaySetUIDsToHangOptions.push(displaySetOptions);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
blankViewportIndices.forEach((blankVPIndex: number) => {
|
||||
// try to fill the empty viewport with a display set not already in the grid
|
||||
const displaySetsNotInGrid = availableDisplaySets.filter(
|
||||
displaySet =>
|
||||
gridDisplaySetUIDs.indexOf(displaySet.displaySetInstanceUID) ===
|
||||
-1 &&
|
||||
['SEG', 'SR', 'RTSTRUCT'].indexOf(displaySet.Modality) === -1
|
||||
);
|
||||
return {
|
||||
displaySetInstanceUIDs: displaySetUIDsToHang,
|
||||
displaySetOptions: displaySetUIDsToHangOptions,
|
||||
viewportOptions: {
|
||||
...viewportOptions,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
if (displaySetsNotInGrid.length > 0) {
|
||||
const displaySetUIDToAdd =
|
||||
displaySetsNotInGrid[0].displaySetInstanceUID;
|
||||
gridDisplaySetUIDs.push(displaySetUIDToAdd);
|
||||
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportIndex: blankVPIndex,
|
||||
displaySetInstanceUIDs: [displaySetUIDToAdd],
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
[viewportGrid, numRows, numCols]
|
||||
);
|
||||
viewportGridService.setLayout({
|
||||
numRows,
|
||||
numCols,
|
||||
layoutType,
|
||||
layoutOptions,
|
||||
hpInfo: createHpInfo(protocol, stage, activeStudyUID),
|
||||
findOrCreateViewport,
|
||||
});
|
||||
};
|
||||
|
||||
const _getUpdatedViewports = useCallback(
|
||||
(viewportIndex, displaySetInstanceUID) => {
|
||||
@@ -177,22 +148,17 @@ function ViewerViewportGrid(props) {
|
||||
[hangingProtocolService, uiNotificationService]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const displaySets = displaySetService.getActiveDisplaySets();
|
||||
updateDisplaySetsForViewports(displaySets);
|
||||
}, [numRows, numCols]);
|
||||
|
||||
// Layout change based on hanging protocols
|
||||
// Using Hanging protocol engine to match the displaySets
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = hangingProtocolService.subscribe(
|
||||
hangingProtocolService.EVENTS.NEW_LAYOUT,
|
||||
({ layoutType, numRows, numCols, layoutOptions }) => {
|
||||
viewportGridService.setLayout({
|
||||
numRows,
|
||||
numCols,
|
||||
layoutType,
|
||||
layoutOptions,
|
||||
});
|
||||
hangingProtocolService.EVENTS.PROTOCOL_CHANGED,
|
||||
({ protocol, stage, activeStudyUID, viewportMatchDetails }) => {
|
||||
updateDisplaySetsFromProtocol(
|
||||
protocol,
|
||||
stage,
|
||||
activeStudyUID,
|
||||
viewportMatchDetails
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -201,35 +167,6 @@ function ViewerViewportGrid(props) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Using Hanging protocol engine to match the displaySets
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = hangingProtocolService.subscribe(
|
||||
hangingProtocolService.EVENTS.PROTOCOL_CHANGED,
|
||||
() => {
|
||||
const displaySets = displaySetService.getActiveDisplaySets();
|
||||
updateDisplaySetsForViewports(displaySets);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [viewports]);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = hangingProtocolService.subscribe(
|
||||
hangingProtocolService.EVENTS.STAGE_CHANGE,
|
||||
() => {
|
||||
const displaySets = DisplaySetService.getActiveDisplaySets();
|
||||
updateDisplaySetsForViewports(displaySets);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [viewports]);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = measurementService.subscribe(
|
||||
measurementService.EVENTS.JUMP_TO_MEASUREMENT,
|
||||
@@ -366,11 +303,15 @@ function ViewerViewportGrid(props) {
|
||||
const getViewportPanes = useCallback(() => {
|
||||
const viewportPanes = [];
|
||||
|
||||
const numViewports = viewportGridService.getNumViewportPanes();
|
||||
for (let i = 0; i < numViewports; i++) {
|
||||
const numViewportPanes = viewportGridService.getNumViewportPanes();
|
||||
for (let i = 0; i < numViewportPanes; i++) {
|
||||
const viewportIndex = i;
|
||||
const isActive = activeViewportIndex === viewportIndex;
|
||||
const paneMetadata = viewports[i] || {};
|
||||
const viewportId = paneMetadata.viewportId || `viewport-${i}`;
|
||||
if (!paneMetadata.viewportId) {
|
||||
paneMetadata.viewportId = viewportId;
|
||||
}
|
||||
const {
|
||||
displaySetInstanceUIDs,
|
||||
viewportOptions,
|
||||
@@ -420,7 +361,7 @@ function ViewerViewportGrid(props) {
|
||||
|
||||
viewportPanes[i] = (
|
||||
<ViewportPane
|
||||
key={viewportIndex}
|
||||
key={viewportId}
|
||||
acceptDropsFor="displayset"
|
||||
onDrop={onDropHandler.bind(null, viewportIndex)}
|
||||
onInteraction={onInteractionHandler}
|
||||
@@ -434,6 +375,7 @@ function ViewerViewportGrid(props) {
|
||||
isActive={isActive}
|
||||
>
|
||||
<div
|
||||
data-cy="viewport-pane"
|
||||
className={classNames('h-full w-full flex flex-col', {
|
||||
'pointer-events-none': !isActive,
|
||||
})}
|
||||
@@ -441,7 +383,7 @@ function ViewerViewportGrid(props) {
|
||||
<ViewportComponent
|
||||
displaySets={displaySets}
|
||||
viewportIndex={viewportIndex}
|
||||
viewportLabel={numViewports > 1 ? viewportLabel : ''}
|
||||
viewportLabel={viewports.length > 1 ? viewportLabel : ''}
|
||||
dataSource={dataSource}
|
||||
viewportOptions={viewportOptions}
|
||||
displaySetOptions={displaySetOptions}
|
||||
@@ -456,7 +398,7 @@ function ViewerViewportGrid(props) {
|
||||
}, [viewports, activeViewportIndex, viewportComponents, dataSource]);
|
||||
|
||||
/**
|
||||
* Loading indicator until numCols and numRows are gotten from the hangingProtocolService
|
||||
* Loading indicator until numCols and numRows are gotten from the HangingProtocolService
|
||||
*/
|
||||
if (!numRows || !numCols) {
|
||||
return null;
|
||||
@@ -472,6 +414,7 @@ function ViewerViewportGrid(props) {
|
||||
|
||||
ViewerViewportGrid.propTypes = {
|
||||
viewportComponents: PropTypes.array.isRequired,
|
||||
servicesManager: PropTypes.instanceOf(ServicesManager),
|
||||
};
|
||||
|
||||
ViewerViewportGrid.defaultProps = {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui';
|
||||
import { useQuery, useSearchParams } from '@hooks';
|
||||
import ViewportGrid from '@components/ViewportGrid';
|
||||
import Compose from './Compose';
|
||||
import getStudies from './studiesList';
|
||||
|
||||
/**
|
||||
* Initialize the route.
|
||||
@@ -64,21 +65,9 @@ function defaultRouteInit(
|
||||
return;
|
||||
}
|
||||
|
||||
const studyMap = {};
|
||||
// Gets the studies list to use
|
||||
const studies = getStudies(studyInstanceUIDs, displaySets);
|
||||
|
||||
// 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];
|
||||
|
||||
@@ -133,10 +122,8 @@ export default function ModeRoute({
|
||||
|
||||
extensionManager.setActiveDataSource(dataSourceName);
|
||||
|
||||
const dataSources = extensionManager.getActiveDataSource();
|
||||
const dataSource = extensionManager.getActiveDataSource()[0];
|
||||
|
||||
// Only handling one instance of the datasource type (E.g. one DICOMWeb server)
|
||||
const dataSource = dataSources[0];
|
||||
// Only handling one route per mode for now
|
||||
const route = mode.routes[0];
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { DicomMetadataStore, Types } from '@ohif/core';
|
||||
|
||||
type StudyMetadata = Types.StudyMetadata;
|
||||
|
||||
/**
|
||||
* Compare function for sorting
|
||||
*
|
||||
* @param a - some simple value (string, number, timestamp)
|
||||
* @param b - some simple value
|
||||
* @param defaultCompare - default return value as a fallback when a===b
|
||||
* @returns - compare a and b, returning 1 if a<b -1 if a>b and defaultCompare otherwise
|
||||
*/
|
||||
const compare = (a, b, defaultCompare = 0): number => {
|
||||
if (a === b) return defaultCompare;
|
||||
if (a < b) return 1;
|
||||
return -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* The studies from display sets gets the studies in study date
|
||||
* order or in study instance UID order - not very useful, but
|
||||
* if not specifically specified then at least making it consistent is useful.
|
||||
*/
|
||||
const getStudiesfromDisplaySets = (displaysets): StudyMetadata[] => {
|
||||
const studyMap = {};
|
||||
|
||||
const ret = displaySets.reduce((prev, curr) => {
|
||||
const { StudyInstanceUID } = curr;
|
||||
if (!studyMap[StudyInstanceUID]) {
|
||||
const study = DicomMetadataStore.getStudy(StudyInstanceUID);
|
||||
studyMap[StudyInstanceUID] = study;
|
||||
prev.push(study);
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
// Return the sorted studies, first on study date and second on study instance UID
|
||||
ret.sort((a, b) => {
|
||||
return compare(
|
||||
a.StudyDate,
|
||||
b.StudyDate,
|
||||
compare(a.StudyInstanceUID, b.StudyInstanceUID)
|
||||
);
|
||||
});
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
* The studies retrieve from the Uids is faster and gets the studies
|
||||
* in the original order, as specified.
|
||||
*/
|
||||
const getStudiesFromUIDs = (studyUids: string[]): StudyMetadata[] => {
|
||||
if (!studyUids?.length) return;
|
||||
return studyUids.map(uid => DicomMetadataStore.getStudy(uid));
|
||||
};
|
||||
|
||||
/** Gets the array of studies */
|
||||
const getStudies = (studyUids?: string[], displaySets): StudyMetadata[] => {
|
||||
return (
|
||||
getStudiesFromUIDs(studyUids) || getStudiesfromDisplaySets(displaySets)
|
||||
);
|
||||
};
|
||||
|
||||
export default getStudies;
|
||||
|
||||
export { getStudies, getStudiesFromUIDs, getStudiesfromDisplaySets, compare };
|
||||
@@ -501,11 +501,9 @@ const defaultFilterValues = {
|
||||
|
||||
function _tryParseInt(str, defaultValue) {
|
||||
let retValue = defaultValue;
|
||||
if (str != null) {
|
||||
if (str.length > 0) {
|
||||
if (!isNaN(str)) {
|
||||
retValue = parseInt(str);
|
||||
}
|
||||
if (str && str.length > 0) {
|
||||
if (!isNaN(str)) {
|
||||
retValue = parseInt(str);
|
||||
}
|
||||
}
|
||||
return retValue;
|
||||
|
||||
Reference in new issue
Block a user