OHIF with Cornerstone3D 2.0 (#4347)

This commit is contained in:
Alireza authored and GitHub committed 2024-11-06 07:15:27 -05:00
1 parent 8981a61b8c
commit 7ce7127d7c
803 files changed
+32760 -15893

No files matched your search

@@ -549,7 +549,7 @@ const WADO_IMAGE_LOADER = {
frameOfReferenceUID: instance.FrameOfReferenceUID,
rows: toNumber(instance.Rows),
columns: toNumber(instance.Columns),
imageOrientationPatient: toNumber(ImageOrientationPatient),
imageOrientationPatient: toNumber(ImageOrientationPatient) || [0, 1, 0, 0, 0, -1],
rowCosines: toNumber(rowCosines || [0, 1, 0]),
isDefaultValueSetForRowCosine: toNumber(rowCosines) ? false : true,
columnCosines: toNumber(columnCosines || [0, 0, -1]),
@@ -51,7 +51,7 @@ export interface Extension {
getToolbarModule?: (p: ExtensionParams) => unknown;
getPanelModule?: (p: ExtensionParams) => unknown;
onModeEnter?: (p: AppTypes) => void;
onModeExit?: () => void;
onModeExit?: (p: AppTypes) => void;
}
export type ExtensionRegister = {
@@ -440,7 +440,7 @@ export default class ExtensionManager extends PubSubService {
return extensionModule;
} catch (ex) {
console.log(ex);
console.error(ex);
throw new Error(
`Exception thrown while trying to call ${getModuleFnName} for the ${extensionId} extension`
);
@@ -0,0 +1,60 @@
import { useEffect, useState, useCallback } from 'react';
import { DisplaySet } from '../types';
/**
* Hook that listens for changes in the active viewport and its display sets.
* It returns the display sets associated with the active viewport.
*
* @param servicesManager - Services manager instance
* @returns Array of display sets for the active viewport
*/
const useActiveViewportDisplaySets = ({ servicesManager }): DisplaySet[] => {
const [displaySets, setDisplaySets] = useState<DisplaySet[]>([]);
const { displaySetService, viewportGridService } = servicesManager.services;
// Move this function outside useEffect and memoize it
const getDisplaySetsForViewport = useCallback(
(viewportId: string) => {
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId) || [];
return displaySetUIDs.map(uid => displaySetService.getDisplaySetByUID(uid)).filter(Boolean);
},
[displaySetService, viewportGridService]
);
useEffect(() => {
// Get initial state
const viewportId = viewportGridService.getActiveViewportId();
setDisplaySets(getDisplaySetsForViewport(viewportId));
const handleViewportChange = ({ viewportId }) => {
setDisplaySets(getDisplaySetsForViewport(viewportId));
};
const handleGridStateChange = ({ state }) => {
const activeViewportId = state.activeViewportId;
if (activeViewportId) {
setDisplaySets(getDisplaySetsForViewport(activeViewportId));
}
};
// Subscribe to viewport changes
const subscriptions = [
viewportGridService.subscribe(
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
handleViewportChange
),
viewportGridService.subscribe(
viewportGridService.EVENTS.GRID_STATE_CHANGED,
handleGridStateChange
),
];
return () => {
subscriptions.forEach(subscription => subscription.unsubscribe());
};
}, [viewportGridService, getDisplaySetsForViewport]); // Only depend on stable references
return displaySets;
};
export default useActiveViewportDisplaySets;
-58
View File
@@ -1,58 +0,0 @@
import * as OHIF from './index';
describe('Top level exports', () => {
test('have not changed', () => {
const expectedExports = [
'MODULE_TYPES',
//
'CommandsManager',
'ExtensionManager',
'HotkeysManager',
'ServicesManager',
'ServiceProvidersManager',
//
'defaults',
'utils',
'hotkeys',
'classes',
'default', //
'errorHandler',
'string',
'user',
'object',
'log',
'DICOMWeb',
'OHIF',
//
'CineService',
'CustomizationService',
'Enums',
'StateSyncService',
'UIDialogService',
'UIModalService',
'UINotificationService',
'UIViewportDialogService',
'DisplaySetService',
'MeasurementService',
'ToolbarService',
'Types',
'ViewportGridService',
'HangingProtocolService',
'UserAuthenticationService',
'IWebApiDataSource',
'DicomMetadataStore',
'DisplaySetMessage',
'DisplaySetMessageList',
'pubSubServiceInterface',
'PubSubService',
'PanelService',
'WorkflowStepsService',
'StudyPrefetcherService',
'useToolbar',
].sort();
const exports = Object.keys(OHIF).sort();
expect(exports).toEqual(expectedExports);
});
});
+5 -4
View File
@@ -30,7 +30,6 @@ import {
PubSubService,
UserAuthenticationService,
CustomizationService,
StateSyncService,
PanelService,
WorkflowStepsService,
StudyPrefetcherService,
@@ -39,6 +38,7 @@ import {
import { DisplaySetMessage, DisplaySetMessageList } from './services/DisplaySetService';
import IWebApiDataSource from './DataSources/IWebApiDataSource';
import useActiveViewportDisplaySets from './hooks/useActiveViewportDisplaySets';
const hotkeys = {
...utils.hotkeys,
@@ -68,7 +68,6 @@ const OHIF = {
//
CineService,
CustomizationService,
StateSyncService,
UIDialogService,
UIModalService,
UINotificationService,
@@ -85,6 +84,7 @@ const OHIF = {
PubSubService,
PanelService,
useToolbar,
useActiveViewportDisplaySets,
WorkflowStepsService,
StudyPrefetcherService,
};
@@ -111,7 +111,6 @@ export {
//
CineService,
CustomizationService,
StateSyncService,
UIDialogService,
UIModalService,
UINotificationService,
@@ -129,13 +128,15 @@ export {
pubSubServiceInterface,
PubSubService,
Enums,
Types,
PanelService,
WorkflowStepsService,
StudyPrefetcherService,
useToolbar,
useActiveViewportDisplaySets,
};
export { OHIF };
export type { Types };
export default OHIF;
@@ -15,6 +15,7 @@ class CineService extends PubSubService {
serviceImplementation = {};
startedClips = new Map();
closedViewports = new Set();
constructor() {
super(CineService.EVENTS);
@@ -34,9 +35,15 @@ class CineService extends PubSubService {
// Todo: for some reason i need to do this setTimeout since the
// reducer state does not get updated right away and if we publish the
// event and we use the cineService.getState() it will return the old state
setTimeout(() => {
if (isCineEnabled) {
this.closedViewports.forEach(viewportId => {
this.clearViewportCineClosed(viewportId);
});
}
queueMicrotask(() => {
this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, { isCineEnabled });
}, 0);
});
}
public playClip(element, playClipOptions) {
@@ -68,6 +75,19 @@ class CineService extends PubSubService {
return this.serviceImplementation._getSyncedViewports(viewportId);
}
public setViewportCineClosed(viewportId) {
this.closedViewports.add(viewportId);
}
public isViewportCineClosed(viewportId) {
// Todo: we should move towards per viewport cine closed in next release
return this.closedViewports.size > 0;
}
public clearViewportCineClosed(viewportId) {
this.closedViewports.delete(viewportId);
}
public setServiceImplementation({
getState: getStateImplementation,
setCine: setCineImplementation,
@@ -281,6 +281,30 @@ export default class CustomizationService extends PubSubService {
return result.transform?.(this) || result;
}
/**
* Helper method to easily add and retrieve customizations
* @param id The unique identifier for the customization
* @param defaultComponent The default component to use if no customization is set
* @param customComponent Optional custom component to set
* @returns The custom component if set, otherwise the default component
*/
public getCustomComponent(
id: string,
defaultComponent: React.ComponentType<any>,
customComponent?: React.ComponentType<any>
) {
const customization = this.getCustomization(id, {
id: `default-${id}`,
content: defaultComponent,
});
if (customComponent) {
this.setModeCustomization(id, { content: customComponent });
}
return customization.content;
}
public addModeCustomizations(modeCustomizations): void {
if (!modeCustomizations) {
return;
@@ -1,4 +1,4 @@
import Command from '../../types/Command';
import { Command } from '../../types/Command';
import { ComponentType } from 'react';
export type Obj = Record<string, unknown>;
@@ -1,12 +0,0 @@
type DisplaySet {
displaySetInstanceUID: string;
StudyInstanceUID: string;
SeriesInstanceUID?: string;
SeriesNumber?: string;
unsupported?: boolean;
viewportType?: string;
instances: any[];
instance?: any;
}
export default DisplaySet;
@@ -204,7 +204,7 @@ export default class DisplaySetService extends PubSubService {
}
// If array of instances => One instance.
const displaySetsAdded = [];
const displaySetsAdded = new Array<DisplaySet>();
if (batch) {
for (let i = 0; i < input.length; i++) {
@@ -50,16 +50,23 @@ const match = (metadataInstance, rules = [], customAttributeRetrievalCallbacks,
readValues[attribute] = fromSrc[from]?.[attribute] ?? instance?.[attribute];
}
// handle cases where the constraint is also a custom attribute
const resolvedConstraint = resolveConstraintAttributes(
readValues,
rule.constraint,
customAttributeRetrievalCallbacks,
fromSrc
);
// Format the constraint as required by Validate.js
const testConstraint = {
[attribute]: rule.constraint,
[attribute]: resolvedConstraint,
};
// Create a single attribute object to be validated, since metadataInstance is an
// instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata)
let attributeValue = readValues[attribute];
const attributeMap = {
[attribute]: attributeValue,
[attribute]: readValues[attribute],
};
// Use Validate.js to evaluate the constraints on the specified metadataInstance
@@ -118,6 +125,47 @@ const match = (metadataInstance, rules = [], customAttributeRetrievalCallbacks,
};
};
// New helper function to resolve constraint attributes
const resolveConstraintAttributes = (
readValues,
constraint,
customAttributeRetrievalCallbacks,
fromSrc
) => {
if (typeof constraint !== 'object' || constraint === null) {
return constraint;
}
const resolvedConstraint = {};
Object.entries(constraint).forEach(([key, value]) => {
if (typeof value === 'object' && Object.keys(value).length > 0 && 'attribute' in value) {
const attributeName = value.attribute;
const attributeFrom = value.from ?? 'metadataInstance';
if (customAttributeRetrievalCallbacks.hasOwnProperty(attributeName)) {
const value = customAttributeRetrievalCallbacks[attributeName].callback.call(
null,
fromSrc[attributeFrom],
fromSrc.options
);
resolvedConstraint[key] = {
value,
};
} else {
resolvedConstraint[key] = {
value:
fromSrc[attributeFrom]?.[attributeName] ?? fromSrc.metadataInstance?.[attributeName],
};
}
} else {
resolvedConstraint[key] = value;
}
}, {});
return resolvedConstraint;
};
const HPMatcher = {
match,
};
@@ -267,7 +267,6 @@ export default class HangingProtocolService extends PubSubService {
// and the protocol or a function that returns a protocol as the value
const protocols = [];
const keys = this.activeProtocolIds || this.protocols.keys();
// @ts-ignore
for (const protocolId of keys) {
const protocol = this.getProtocolById(protocolId);
if (protocol) {
@@ -513,12 +512,6 @@ export default class HangingProtocolService extends PubSubService {
_validateProtocol(protocol: HangingProtocol.Protocol): HangingProtocol.Protocol {
protocol.id = protocol.id || protocol.name;
const defaultViewportOptions = {
toolGroupId: 'default',
viewportType: 'stack',
};
// Automatically compute some number of attributes if they
// aren't present. Makes defining new HPs easier.
protocol.name = protocol.name || protocol.id;
const { stages } = protocol;
@@ -545,10 +538,10 @@ export default class HangingProtocolService extends PubSubService {
const { rows, columns } = stage.viewportStructure.properties;
for (let i = 0; i < rows * columns; i++) {
const defaultViewport = stage.defaultViewport || protocol.defaultViewport || {};
stage.viewports.push({
viewportOptions: {
...defaultViewportOptions,
// Use 'default' for the first viewport, and UUIDs for the rest.
...defaultViewport.viewportOptions,
viewportId: i === 0 ? 'default' : uuidv4(),
},
displaySets: [],
@@ -557,14 +550,14 @@ export default class HangingProtocolService extends PubSubService {
} else {
// Clone each viewport to ensure independent objects
stage.viewports = stage.viewports.map((viewport, index) => {
const defaultViewport = stage.defaultViewport || protocol.defaultViewport || {};
const existingViewportId = viewport.viewportOptions?.viewportId;
return {
...viewport,
viewportOptions: {
...(viewport.viewportOptions || defaultViewportOptions),
// use provided viewportId when available, otherwise use default for first viewport
// and uuid for the rest
...defaultViewport.viewportOptions,
...viewport.viewportOptions,
viewportId: existingViewportId
? existingViewportId
: index === 0
@@ -766,6 +759,10 @@ export default class HangingProtocolService extends PubSubService {
return viewportsToUpdate;
}
public runMatchingRules(metadataArray, matchingRules, options) {
return this.protocolEngine.findMatch(metadataArray, matchingRules, options);
}
private _updateDisplaySetInstanceUIDs(
viewport: HangingProtocol.Viewport,
displaySetSelectorId: string,
@@ -1111,7 +1108,7 @@ export default class HangingProtocolService extends PubSubService {
this.stageIndex = 0;
}
const protocol = this.protocol;
const stage = protocol.stages[stageIdx];
const stage = protocol.stages[stageIdx] ?? protocol.stages[this.stageIndex];
const defaultViewport = stage.defaultViewport || protocol.defaultViewport;
if (!defaultViewport) {
return;
@@ -1407,8 +1404,8 @@ export default class HangingProtocolService extends PubSubService {
}
public areRequiredSelectorsValid(
displaySetSelectors: HangingProtocol.DisplaySetSelector,
displaySet: any
displaySetSelectors: HangingProtocol.DisplaySetSelector[],
displaySet: DisplaySet
): boolean {
let pass = true;
for (const displaySetSelector of displaySetSelectors) {
@@ -1424,7 +1421,7 @@ export default class HangingProtocolService extends PubSubService {
private _validateRequiredSelectors(
displaySetSelector: HangingProtocol.DisplaySetSelector,
displaySet: any
displaySet: DisplaySet
) {
const { seriesMatchingRules } = displaySetSelector;
@@ -32,6 +32,7 @@ import { PubSubService } from '../_shared/pubSubServiceInterface';
/* Measurement schema keys for object validation. */
const MEASUREMENT_SCHEMA_KEYS = [
'uid',
'color',
'data',
'getReport',
'displayText',
@@ -42,6 +43,8 @@ const MEASUREMENT_SCHEMA_KEYS = [
'frameNumber',
'displaySetInstanceUID',
'label',
'isLocked',
'isVisible',
'description',
'type',
'unit',
@@ -58,7 +61,7 @@ const MEASUREMENT_SCHEMA_KEYS = [
'shortestDiameter',
'longestDiameter',
'cachedStats',
'selected',
'isSelected',
'textBox',
'referencedImageId',
];
@@ -185,7 +188,7 @@ class MeasurementService extends PubSubService {
return;
}
measurement.selected = selected;
measurement.isSelected = selected;
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
source: measurement.source,
@@ -484,6 +487,10 @@ class MeasurementService extends PubSubService {
/* Convert measurement */
measurement = toMeasurementSchema(sourceAnnotationDetail);
if (!measurement) {
return;
}
measurement.source = source;
} catch (error) {
// Todo: handle other
@@ -555,23 +562,23 @@ class MeasurementService extends PubSubService {
* Removes a measurement and broadcasts the removed event.
*
* @param {string} measurementUID The measurement uid
* @param {MeasurementSource} source The measurement source instance
*/
remove(measurementUID, source, eventDetails) {
if (
!measurementUID ||
(!this.measurements.has(measurementUID) && !this.unmappedMeasurements.has(measurementUID))
) {
log.warn(`No uid provided, or unable to find measurement by uid.`);
remove(measurementUID: string): void {
const measurement =
this.measurements.get(measurementUID) || this.unmappedMeasurements.get(measurementUID);
if (!measurementUID || !measurement) {
console.debug(`No uid provided, or unable to find measurement by uid.`);
return;
}
const source = measurement.source;
this.unmappedMeasurements.delete(measurementUID);
this.measurements.delete(measurementUID);
this._broadcastEvent(this.EVENTS.MEASUREMENT_REMOVED, {
source,
measurement: measurementUID,
...eventDetails,
});
}
@@ -728,6 +735,57 @@ class MeasurementService extends PubSubService {
_arrayOfObjects = obj => {
return Object.entries(obj).map(e => ({ [e[0]]: e[1] }));
};
public toggleLockMeasurement(measurementUID: string): void {
const measurement = this.measurements.get(measurementUID);
if (!measurement) {
console.debug(`No measurement found for uid: ${measurementUID}`);
return;
}
measurement.isLocked = !measurement.isLocked;
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
source: measurement.source,
measurement,
notYetUpdatedAtSource: true,
});
}
public toggleVisibilityMeasurement(measurementUID: string): void {
const measurement = this.measurements.get(measurementUID);
if (!measurement) {
console.debug(`No measurement found for uid: ${measurementUID}`);
return;
}
measurement.isVisible = !measurement.isVisible;
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
source: measurement.source,
measurement,
notYetUpdatedAtSource: true,
});
}
public updateColorMeasurement(measurementUID: string, color: number[]): void {
const measurement = this.measurements.get(measurementUID);
if (!measurement) {
console.debug(`No measurement found for uid: ${measurementUID}`);
return;
}
measurement.color = color;
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
source: measurement.source,
measurement,
notYetUpdatedAtSource: true,
});
}
}
export default MeasurementService;
@@ -1,31 +0,0 @@
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);
});
});
});
@@ -1,86 +0,0 @@
import { PubSubService } from '../_shared/pubSubServiceInterface';
import { ExtensionManager } from '../../extensions';
const EVENTS = {};
type Obj = Record<string, unknown>;
type StateConfig = {
/** clearOnModeExit defines state configuration 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 {}
/** Registers a new sync store called `id`. The state
* defines how the state is stored, and any default clearing of the
* state.
* A default store has the lifetime of the application.
* The other available store is cleared `onModeExit`
*/
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);
}
}
@@ -1,3 +0,0 @@
import StateSyncService from './StateSyncService';
export default StateSyncService;
@@ -4,7 +4,16 @@ import ServicesManager from '../ServicesManager';
import ViewportGridService from '../ViewportGridService';
import { DisplaySet } from '../../types';
const IMAGE_REQUEST_TYPE = 'prefetch';
enum RequestType {
/** Highest priority for loading*/
Interaction = 'interaction',
/** Second highest priority for loading*/
Thumbnail = 'thumbnail',
/** Third highest priority for loading, usually used for image loading in the background*/
Prefetch = 'prefetch',
/** Lower priority, often used for background computations in the worker */
Compute = 'compute',
}
export const EVENTS = {
SERVICE_STARTED: 'event::studyPrefetcherService:started',
@@ -67,7 +76,7 @@ interface ICache {
}
interface IImageLoadPoolManager {
addRequest (
addRequest(
requestFn: () => Promise<any>,
type: string,
additionalDetails: Record<string, unknown>,
@@ -81,7 +90,7 @@ interface IImageLoader {
}
type EventSubscription = {
unsubscribe: () => void
unsubscribe: () => void;
};
interface IImageLoadEventsManager {
@@ -122,7 +131,7 @@ class StudyPrefetcherService extends PubSubService {
};
// Properties set by Cornerstone extension (initStudyPrefetcherService)
public requestType: string = IMAGE_REQUEST_TYPE;
public requestType: string = RequestType.Prefetch;
public cache: ICache;
public imageLoadPoolManager: IImageLoadPoolManager;
public imageLoader: IImageLoader;
@@ -181,22 +190,22 @@ class StudyPrefetcherService extends PubSubService {
if (!this._inflightRequests.get(imageId)) {
this._sendNextRequests();
}
}
};
const fnImageLoadedEventListener = (evt) => {
const fnImageLoadedEventListener = evt => {
const { image } = evt.detail;
const { imageId } = image;
this._moveImageIdToLoadedSet(imageId);
fnOnImageLoadCompleted(imageId);
}
};
const fnImageLoadFailedEventListener = (evt) => {
const fnImageLoadFailedEventListener = evt => {
const { imageId } = evt.detail;
this._moveImageIdToFailedSet(imageId);
fnOnImageLoadCompleted(imageId);
}
};
return this.imageLoadEventsManager.addEventListeners(
fnImageLoadedEventListener,
@@ -246,7 +255,7 @@ class StudyPrefetcherService extends PubSubService {
viewportGridActiveViewportIdSubscription,
viewportGridLayoutChangedSubscription,
viewportGridStateChangedSubscription,
viewportGridViewportreadySubscription
viewportGridViewportreadySubscription,
];
}
@@ -263,15 +272,13 @@ class StudyPrefetcherService extends PubSubService {
this._subscriptions = [];
}
private _syncWithActiveViewport(
{
activeViewportId,
forceRestart
}:
{
activeViewportId?: string,
forceRestart?: boolean
} = {}) {
private _syncWithActiveViewport({
activeViewportId,
forceRestart,
}: {
activeViewportId?: string;
forceRestart?: boolean;
} = {}) {
const { viewportGridService } = this._servicesManager.services;
const viewportGridServiceState = viewportGridService.getState();
const { viewports } = viewportGridServiceState;
@@ -311,8 +318,13 @@ class StudyPrefetcherService extends PubSubService {
private _areActiveDisplaySetsLoaded() {
const { _activeDisplaySetsInstanceUIDs: displaySetsInstanceUIDs } = this;
return displaySetsInstanceUIDs.length && displaySetsInstanceUIDs.every(displaySetsInstanceUID =>
this._displaySetLoadingStates.get(displaySetsInstanceUID).loadingProgress >= 1);
return (
displaySetsInstanceUIDs.length &&
displaySetsInstanceUIDs.every(
displaySetsInstanceUID =>
this._displaySetLoadingStates.get(displaySetsInstanceUID).loadingProgress >= 1
)
);
}
private _getClosestDisplaySets(displaySets: DisplaySet[], activeDisplaySetIndex: number) {
@@ -363,7 +375,9 @@ class StudyPrefetcherService extends PubSubService {
const { displaySetsCount } = this.config;
const activeDisplaySetsInstanceUIDs = this._activeDisplaySetsInstanceUIDs;
const [activeDisplaySetUID] = activeDisplaySetsInstanceUIDs;
const activeDisplaySetIndex = displaySets.findIndex(ds => ds.displaySetInstanceUID === activeDisplaySetUID);
const activeDisplaySetIndex = displaySets.findIndex(
ds => ds.displaySetInstanceUID === activeDisplaySetUID
);
const getDisplaySetsFunctionsMap = {
[StudyPrefetchOrder.closest]: this._getClosestDisplaySets,
[StudyPrefetchOrder.downward]: this._getDownwardDisplaySets,
@@ -381,23 +395,21 @@ class StudyPrefetcherService extends PubSubService {
// Remove any active displaySet that may still be in the activeDisplaySetsInstanceUIDs.
// That may happen when activeDisplaySetsInstanceUIDs has more than one element.
return fnGetDisplaySets.call(this, displaySets, activeDisplaySetIndex).filter(
ds => !uidsSet.has(ds.displaySetInstanceUID)
).slice(0, displaySetsCount);
return fnGetDisplaySets
.call(this, displaySets, activeDisplaySetIndex)
.filter(ds => !uidsSet.has(ds.displaySetInstanceUID))
.slice(0, displaySetsCount);
}
private _getDisplaySets() {
const { displaySetService } = this._servicesManager.services;
const displaySets = [...displaySetService.getActiveDisplaySets()];
let displaySetsToPrefetch = this._getSortedDisplaySetsToPrefetch(displaySets);
const displaySetsToPrefetch = this._getSortedDisplaySetsToPrefetch(displaySets);
return { displaySets, displaySetsToPrefetch };
}
private _updateImageIdsDisplaySetMap(
displaySetInstanceUID: string,
imageIds: string[]
): void {
private _updateImageIdsDisplaySetMap(displaySetInstanceUID: string, imageIds: string[]): void {
for (const imageId of imageIds) {
let displaySetsInstanceUIDsMap = this._imageIdsToDisplaySetsMap.get(imageId);
@@ -451,12 +463,12 @@ class StudyPrefetcherService extends PubSubService {
pendingImageIds,
loadedImageIds,
failedImageIds: new Set(),
loadingProgress: 0
loadingProgress: 0,
};
this._updateDisplaySetLoadingProgress(displaySetLoadingState);
this._displaySetLoadingStates.set(displaySetInstanceUID, displaySetLoadingState);
this._updateImageIdsDisplaySetMap(displaySetInstanceUID, imageIds)
this._updateImageIdsDisplaySetMap(displaySetInstanceUID, imageIds);
// Notify the UI that something is already loaded (eg: update StudyBrowser)
if (loadedImageIds.size) {
@@ -116,8 +116,10 @@ export default class ToolbarService extends PubSubService {
* Removes buttons from the toolbar.
* @param buttonId - The button to be removed.
*/
public removeButton(buttonId : string ){
if(this.state.buttons[buttonId]) delete this.state.buttons[buttonId]
public removeButton(buttonId: string) {
if (this.state.buttons[buttonId]) {
delete this.state.buttons[buttonId];
}
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {
...this.state,
});
@@ -555,6 +557,11 @@ export default class ToolbarService extends PubSubService {
};
getButtonComponentForUIType(uiType: string) {
return uiType ? this._getButtonUITypes()[uiType]?.defaultComponent ?? null : null;
return uiType ? (this._getButtonUITypes()[uiType]?.defaultComponent ?? null) : null;
}
clearButtonSection(buttonSection: string) {
this.state.buttonSections[buttonSection] = [];
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, { ...this.state });
}
}
@@ -1,12 +1,22 @@
import type { RunCommand } from '../../types/Command';
export type EvaluatePublic = string | EvaluateFunction | string[];
export type EvaluatePublic =
| string
| EvaluateFunction
| EvaluateObject
| (string | EvaluateFunction | EvaluateObject)[];
export type EvaluateFunction = (props: Record<string, unknown>) => {
disabled: boolean;
className: string;
};
export type EvaluateObject = {
name: string;
// Allow any additional properties
[key: string]: unknown;
};
export type ButtonProps = {
id: string;
icon: string;
@@ -1,26 +1,13 @@
/**
* A UI Notification
*
* @typedef {Object} Notification
* @property {string} title -
* @property {string} message -
* @property {number} [duration=5000] - in ms
* @property {string} [position="bottomRight"] -"topLeft" | "topCenter | "topRight" | "bottomLeft" | "bottomCenter" | "bottomRight"
* @property {string} [type="info"] - "info" | "error" | "warning" | "success"
* @property {boolean} [autoClose=true]
*/
const serviceShowRequestQueue = [];
const serviceImplementation = {
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
_hide: () => console.debug('hide() NOT IMPLEMENTED'),
_show: showArguments => {
serviceShowRequestQueue.push(showArguments);
console.warn('show() NOT IMPLEMENTED');
console.debug('show() NOT IMPLEMENTED');
return null;
},
};
type ToastType = 'success' | 'error' | 'info' | 'warning' | 'loading';
class UINotificationService {
static REGISTRATION = {
name: 'uiNotificationService',
@@ -44,11 +31,6 @@ class UINotificationService {
}
if (showImplementation) {
serviceImplementation._show = showImplementation;
while (serviceShowRequestQueue.length > 0) {
const showArguments = serviceShowRequestQueue.pop();
serviceImplementation._show(showArguments);
}
}
}
@@ -59,24 +41,103 @@ class UINotificationService {
* @returns undefined
*/
public hide(id: string) {
return serviceImplementation._hide({ id });
return serviceImplementation._hide(id);
}
/**
* Create and show a new UI notification; returns the
* ID of the created notification.
* ID of the created notification. Can also handle promises for loading states.
*
* @param {Notification} notification { title, message, duration, position, type, autoClose}
* @returns {number} id
* @param {object} notification - The notification object
* @param {string} notification.title - The title of the notification
* @param {string | function} notification.message - The message content of the notification or a function that returns a message
* @param {number} [notification.duration=5000] - The duration to show the notification (in milliseconds)
* @param {'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center'} [notification.position='bottom-right'] - The position of the notification
* @param {ToastType} [notification.type='info'] - The type of the notification
* @param {boolean} [notification.autoClose=true] - Whether the notification should auto-close
* @param {Promise} [notification.promise] - A promise to track for loading, success, and error states
* @param {object} [notification.promiseMessages] - Custom messages for promise states
* @param {string} [notification.promiseMessages.loading] - Message to show while promise is pending
* @param {string | function} [notification.promiseMessages.success] - Message to show when promise resolves
* @param {string | function} [notification.promiseMessages.error] - Message to show when promise rejects
* @returns {string} id - The ID of the created notification
*/
show({
title,
message,
duration = 5000,
position = 'bottomRight',
position = 'bottom-right',
type = 'info',
autoClose = true,
}) {
promise,
promiseMessages,
}: {
title: string;
message: string | ((data?: any) => string);
duration?: number;
position?:
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right'
| 'top-center'
| 'bottom-center';
type?: ToastType;
autoClose?: boolean;
promise?: Promise<any>;
promiseMessages?: {
loading?: string;
success?: string | ((data: any) => string);
error?: string | ((error: any) => string);
};
}): string {
if (promise && promiseMessages) {
const loadingId = serviceImplementation._show({
title,
message: promiseMessages.loading || 'Loading...',
type: 'loading',
autoClose: false,
position,
});
promise.then(
data => {
const successMessage =
typeof promiseMessages.success === 'function'
? promiseMessages.success(data)
: promiseMessages.success || 'Success';
serviceImplementation._show({
title,
message: successMessage,
type: 'success',
duration,
position,
autoClose,
});
this.hide(loadingId);
},
error => {
const errorMessage =
typeof promiseMessages.error === 'function'
? promiseMessages.error(error)
: promiseMessages.error || 'Error';
serviceImplementation._show({
title,
message: errorMessage,
type: 'error',
duration,
position,
autoClose,
});
this.hide(loadingId);
}
);
return loadingId;
}
return serviceImplementation._show({
title,
message,
@@ -1,5 +1,4 @@
import { PubSubService } from '../_shared/pubSubServiceInterface';
import { getPresentationIds, PresentationIds } from './getPresentationIds';
class ViewportGridService extends PubSubService {
public static readonly EVENTS = {
@@ -13,18 +12,75 @@ class ViewportGridService extends PubSubService {
public static REGISTRATION = {
name: 'viewportGridService',
altName: 'ViewportGridService',
create: ({ configuration = {} }) => {
return new ViewportGridService();
create: ({ configuration = {}, servicesManager }) => {
return new ViewportGridService({ servicesManager });
},
};
public static getPresentationIds = getPresentationIds;
serviceImplementation = {};
servicesManager: AppTypes.ServicesManager;
presentationIdProviders: Map<
string,
(id: string, { viewport, viewports, isUpdatingSameViewport, servicesManager }) => unknown
>;
constructor() {
constructor({ servicesManager }) {
super(ViewportGridService.EVENTS);
this.servicesManager = servicesManager;
this.serviceImplementation = {};
this.presentationIdProviders = new Map();
}
public addPresentationIdProvider(
id: string,
provider: (id: string, { viewport, viewports, isUpdatingSameViewport }) => unknown
): void {
this.presentationIdProviders.set(id, provider);
}
public getPresentationId(id: string, viewportId: string): string | null {
const state = this.getState();
const viewport = state.viewports.get(viewportId);
return this._getPresentationId(id, {
viewport,
viewports: state.viewports,
});
}
private _getPresentationId(id, { viewport, viewports }) {
const isUpdatingSameViewport = [...viewports.values()].some(
v =>
v.displaySetInstanceUIDs?.toString() === viewport.displaySetInstanceUIDs?.toString() &&
v.viewportId === viewport.viewportId
);
const provider = this.presentationIdProviders.get(id);
if (provider) {
const result = provider(id, {
viewport,
viewports,
isUpdatingSameViewport,
servicesManager: this.servicesManager,
});
return result;
}
return null;
}
public getPresentationIds({ viewport, viewports }) {
// Use the keys of the Map to get all registered provider IDs
const registeredPresentationProviders = Array.from(this.presentationIdProviders.keys());
return registeredPresentationProviders.reduce((acc, id) => {
const value = this._getPresentationId(id, {
viewport,
viewports,
});
if (value !== null) {
acc[id] = value;
}
return acc;
}, {});
}
public setServiceImplementation({
@@ -75,15 +131,24 @@ class ViewportGridService extends PubSubService {
public setActiveViewportId(id: string) {
this.serviceImplementation._setActiveViewport(id);
this._broadcastEvent(this.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, {
viewportId: id,
});
// Use queueMicrotask to delay the event broadcast
setTimeout(() => {
this._broadcastEvent(this.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, {
viewportId: id,
});
}, 0);
}
public getState() {
public getState(): AppTypes.ViewportGrid.State {
return this.serviceImplementation._getState();
}
public getViewportState(viewportId: string) {
const state = this.getState();
return state.viewports.get(viewportId);
}
public setViewportIsReady(viewportId, callback) {
this.serviceImplementation._setViewportIsReady(viewportId, callback);
}
@@ -108,19 +173,34 @@ class ViewportGridService extends PubSubService {
public async setDisplaySetsForViewports(props) {
await this.serviceImplementation._setDisplaySetsForViewports(props);
const state = this.getState();
const viewports = [];
const updatedViewports = [];
const removedViewportIds = [];
for (const viewport of props) {
const updatedViewport = state.viewports.get(viewport.viewportId);
if (updatedViewport) {
viewports.push(updatedViewport);
updatedViewports.push(updatedViewport);
const updatedDisplaySetUIDs = updatedViewport.displaySetInstanceUIDs || [];
const isCleared = updatedDisplaySetUIDs.length === 0;
if (isCleared) {
removedViewportIds.push(viewport.viewportId);
}
} else {
console.warn("ViewportGridService::Didn't find updated viewport", viewport);
removedViewportIds.push(viewport.viewportId);
}
}
this._broadcastEvent(ViewportGridService.EVENTS.GRID_STATE_CHANGED, {
state,
viewports,
setTimeout(() => {
this._broadcastEvent(ViewportGridService.EVENTS.GRID_STATE_CHANGED, {
state,
viewports: updatedViewports,
removedViewportIds,
});
});
}
@@ -143,7 +223,7 @@ class ViewportGridService extends PubSubService {
* 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({
public async setLayout({
numCols,
numRows,
layoutOptions,
@@ -152,7 +232,11 @@ class ViewportGridService extends PubSubService {
findOrCreateViewport = undefined,
isHangingProtocolLayout = false,
}) {
this.serviceImplementation._setLayout({
// Get the previous state before the layout change
const prevState = this.getState();
const prevViewportIds = new Set(prevState.viewports.keys());
await this.serviceImplementation._setLayout({
numCols,
numRows,
layoutOptions,
@@ -161,10 +245,26 @@ class ViewportGridService extends PubSubService {
findOrCreateViewport,
isHangingProtocolLayout,
});
this._broadcastEvent(this.EVENTS.LAYOUT_CHANGED, {
numCols,
numRows,
});
// Use queueMicrotask to ensure the layout changed event is published after
setTimeout(() => {
// Get the new state after the layout change
const state = this.getState();
const currentViewportIds = new Set(state.viewports.keys());
// Determine which viewport IDs have been removed
const removedViewportIds = [...prevViewportIds].filter(id => !currentViewportIds.has(id));
this._broadcastEvent(this.EVENTS.LAYOUT_CHANGED, {
numCols,
numRows,
});
this._broadcastEvent(this.EVENTS.GRID_STATE_CHANGED, {
state,
removedViewportIds,
});
}, 0);
}
public reset() {
@@ -181,11 +281,23 @@ class ViewportGridService extends PubSubService {
this.serviceImplementation._onModeExit();
}
public set(state) {
this.serviceImplementation._set(state);
this._broadcastEvent(this.EVENTS.GRID_STATE_CHANGED, {
state,
});
public set(newState) {
const prevState = this.getState();
const prevViewportIds = new Set(prevState.viewports.keys());
this.serviceImplementation._set(newState);
const state = this.getState();
const currentViewportIds = new Set(state.viewports.keys());
const removedViewportIds = [...prevViewportIds].filter(id => !currentViewportIds.has(id));
setTimeout(() => {
this._broadcastEvent(this.EVENTS.GRID_STATE_CHANGED, {
state,
removedViewportIds,
});
}, 0);
}
public getNumViewportPanes() {
@@ -207,5 +319,3 @@ class ViewportGridService extends PubSubService {
}
export default ViewportGridService;
export type { PresentationIds };
@@ -1,153 +0,0 @@
const JOIN_STR = '&';
// The default lut presentation id if none defined
const DEFAULT = 'default';
// 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.
const addUniqueIndex = (arr, key, viewports, isUpdatingSameViewport) => {
arr.push(0);
// If we are updating the viewport, we should not increment the index
if (isUpdatingSameViewport) {
return;
}
// The 128 is just a value that is larger than how many viewports we
// display at once, used as an upper bound on how many unique presentation
// ID's might exist for a single display set at once.
for (let displayInstance = 0; displayInstance < 128; displayInstance++) {
arr[arr.length - 1] = displayInstance;
const testId = arr.join(JOIN_STR);
if (
!Array.from(viewports.values()).find(
viewport => viewport.viewportOptions?.presentationIds?.[key] === testId
)
) {
break;
}
}
};
const getLutId = (ds): string => {
if (!ds || !ds.options) {
return DEFAULT;
}
if (ds.options.id) {
return ds.options.id;
}
const arr = Object.entries(ds.options).map(([key, val]) => `${key}=${val}`);
if (!arr.length) {
return DEFAULT;
}
return arr.join(JOIN_STR);
};
export type PresentationIds = {
positionPresentationId?: string;
lutPresentationId?: string;
};
/**
* Gets a set of presentation IDs for a viewport. The presentation IDs are
* used to remember the presentation state of the viewport when it is navigated
* to different layouts.
*
* The design of this is setup to allow preserving the view information in the
* following cases:
*
*
* * If a set of display sets was previously displayed in the same initial
* position as it is currently being asked to be displayed,
* then remember the camera position as previously displayed
*
* * If a set of display sets was previously displayed with the same initial
* LUT conditions, then remember the last LUT displayed for that display set
* and re-apply it.
*
* * Otherwise, apply the initial hanging protocol specified LUT and camera
* position to new display sets.
*
* This means generating two presentationId keys:
*
* `positionPresentationId`
*
* Used for getting the camera/initial position state sync values.
* This is a combination of:
* * `viewportOptions.id`
* * `viewportOptions.orientation`
* * display set UID's - as displayed for this viewport, excluding seg
* * a unique index number if the previous key is already displayed
*
* `lutPresentationId`
*
* Used for getting the voi LUT information. Generated from:
*
* * `displaySetOption[0].options` - including the id if present
* * displaySetUID's
* * a unique index number if the previously generated key is already
* displayed.
*
* @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 PresentationIds
*/
const getPresentationIds = (viewport, viewports): PresentationIds => {
if (!viewport) {
return;
}
const { viewportOptions, displaySetInstanceUIDs, displaySetOptions } = viewport;
if (!viewportOptions || !displaySetInstanceUIDs?.length) {
return;
}
const { id, orientation } = viewportOptions;
const lutId = getLutId(displaySetOptions[0]);
const lutPresentationArr = [lutId];
const positionPresentationArr = [orientation || 'acquisition'];
if (id) {
positionPresentationArr.push(id);
}
if (displaySetOptions.some(ds => ds.options?.blendMode || ds.options?.displayPreset)) {
positionPresentationArr.push(`custom`);
}
for (const uid of displaySetInstanceUIDs) {
positionPresentationArr.push(uid);
lutPresentationArr.push(uid);
}
// only add unique index if the viewport is getting inserted and not updated
const isUpdatingSameViewport = Array.from(viewports.values()).some(v => {
return (
v.displaySetInstanceUIDs?.toString() === viewport.displaySetInstanceUIDs?.toString() &&
v.viewportId === viewport.viewportId
);
});
// if it is updating the viewport we should not increment the index since
// it might be a layer on the fusion or a SEG layer that is added on
// top of the original display set
addUniqueIndex(
positionPresentationArr,
'positionPresentationId',
viewports,
isUpdatingSameViewport
);
addUniqueIndex(lutPresentationArr, 'lutPresentationId', viewports, isUpdatingSameViewport);
const lutPresentationId = lutPresentationArr.join(JOIN_STR);
const positionPresentationId = positionPresentationArr.join(JOIN_STR);
return { lutPresentationId, positionPresentationId };
};
export default getPresentationIds;
export { getPresentationIds };
@@ -1,5 +1,3 @@
import ViewportGridService from './ViewportGridService';
import type { PresentationIds } from './ViewportGridService';
export default ViewportGridService;
export type { PresentationIds };
@@ -135,6 +135,7 @@ class WorkflowStepsService extends PubSubService {
const toUse = Array.isArray(toolbarButtons) ? toolbarButtons : [toolbarButtons];
toUse.forEach(({ buttonSection, buttons }) => {
toolbarService.clearButtonSection(buttonSection);
toolbarService.createButtonSection(buttonSection, buttons);
});
}
-2
View File
@@ -14,7 +14,6 @@ import HangingProtocolService from './HangingProtocolService';
import pubSubServiceInterface, { PubSubService } from './_shared/pubSubServiceInterface';
import UserAuthenticationService from './UserAuthenticationService';
import CustomizationService from './CustomizationService';
import StateSyncService from './StateSyncService';
import PanelService from './PanelService';
import WorkflowStepsService from './WorkflowStepsService';
import StudyPrefetcherService from './StudyPrefetcherService';
@@ -27,7 +26,6 @@ export {
ServicesManager,
ServiceProvidersManager,
CustomizationService,
StateSyncService,
UIDialogService,
UIModalService,
UINotificationService,
+122 -15
View File
@@ -5,7 +5,6 @@ import MeasurementServiceType from '../services/MeasurementService';
import ViewportGridServiceType from '../services/ViewportGridService';
import ToolbarServiceType from '../services/ToolBarService';
import DisplaySetServiceType from '../services/DisplaySetService';
import StateSyncServiceType from '../services/StateSyncService';
import UINotificationServiceType from '../services/UINotificationService';
import UIModalServiceType from '../services/UIModalService';
import WorkflowStepsServiceType from '../services/WorkflowStepsService';
@@ -22,6 +21,18 @@ import ExtensionManagerType from '../extensions/ExtensionManager';
import Hotkey from '../classes/Hotkey';
import * as CommandTypes from './Command';
import * as ColorTypes from './Color';
import * as ConsumerTypes from './Consumer';
import * as DataSourceTypes from './DataSource';
import * as DataSourceConfigurationAPITypes from './DataSourceConfigurationAPI';
import * as DisplaySetTypes from './DisplaySet';
import * as HangingProtocolTypes from './HangingProtocol';
import * as IPubSubTypes from './IPubSub';
import * as PanelModuleTypes from './PanelModule';
import * as StudyMetadataTypes from './StudyMetadata';
import * as ViewportGridTypes from './ViewportGridType';
import { StepOptions, TourOptions } from 'shepherd.js';
declare global {
@@ -37,7 +48,6 @@ declare global {
export type ViewportGridService = ViewportGridServiceType;
export type UIModalService = UIModalServiceType;
export type UINotificationService = UINotificationServiceType;
export type StateSyncService = StateSyncServiceType;
export type WorkflowStepsService = WorkflowStepsServiceType;
export type CineService = CineServiceType;
export type UserAuthenticationService = UserAuthenticationServiceType;
@@ -61,7 +71,6 @@ declare global {
viewportGridService?: ViewportGridServiceType;
uiModalService?: UIModalServiceType;
uiNotificationService?: UINotificationServiceType;
stateSyncService?: StateSyncServiceType;
workflowStepsService?: WorkflowStepsServiceType;
cineService?: CineServiceType;
userAuthenticationService?: UserAuthenticationServiceType;
@@ -70,15 +79,15 @@ declare global {
panelService?: PanelServiceType;
studyPrefetcherService?: StudyPrefetcherServiceType;
}
export interface Config {
routerBasename?: string;
customizationService?: any;
customizationService?: CustomizationServiceType;
extensions?: string[];
modes?: string[];
experimentalStudyBrowserSort?: boolean;
defaultDataSourceName?: string;
hotkeys?: Record<string, Hotkey> | Hotkey[];
useSharedArrayBuffer?: 'AUTO' | 'FALSE' | 'TRUE';
preferSizeOverAccuracy?: boolean;
useNorm16Texture?: boolean;
useCPURendering?: boolean;
@@ -92,8 +101,8 @@ declare global {
interaction?: number;
prefetch?: number;
thumbnail?: number;
compute?: number;
};
disableEditing?: boolean;
maxNumberOfWebWorkers?: number;
acceptHeader?: string[];
investigationalUseDialog?: {
@@ -114,18 +123,21 @@ declare global {
activateViewportBeforeInteraction?: boolean;
autoPlayCine?: boolean;
showStudyList?: boolean;
whiteLabeling?: any;
httpErrorHandler?: any;
whiteLabeling?: Record<string, unknown>;
httpErrorHandler?: (error: Error) => void;
addWindowLevelActionMenu?: boolean;
dangerouslyUseDynamicConfig?: {
enabled: boolean;
regex: RegExp;
};
onConfiguration?: (dicomWebConfig: any, options: any) => any;
dataSources?: any;
oidc?: any;
peerImport?: (moduleId: string) => Promise<any>;
studyPrefetcher: {
onConfiguration?: (
dicomWebConfig: Record<string, unknown>,
options: Record<string, unknown>
) => Record<string, unknown>;
dataSources?: Record<string, unknown>;
oidc?: Record<string, unknown>;
peerImport?: (moduleId: string) => Promise<Record<string, unknown>>;
studyPrefetcher?: {
enabled: boolean;
displaySetsCount: number;
maxNumPrefetchRequests: number;
@@ -145,13 +157,108 @@ declare global {
extensionManager?: ExtensionManager;
config?: Config;
}
// Add Command namespace to AppTypes
export namespace Commands {
export type SimpleCommand = CommandTypes.SimpleCommand;
export type ComplexCommand = CommandTypes.ComplexCommand;
export type Command = CommandTypes.Command;
export type RunCommand = CommandTypes.RunCommand;
export interface Commands extends CommandTypes.Commands {}
}
// Color types
export type RGB = ColorTypes.RGB;
// Consumer types
export type Consumer = ConsumerTypes.Consumer;
// DataSource types
export type DataSourceDefinition = DataSourceTypes.DataSourceDefinition;
// DataSourceConfigurationAPI types
export namespace DataSourceConfiguration {
export type BaseDataSourceConfigurationAPIItem =
DataSourceConfigurationAPITypes.BaseDataSourceConfigurationAPIItem;
export type BaseDataSourceConfigurationAPI =
DataSourceConfigurationAPITypes.BaseDataSourceConfigurationAPI;
}
// DisplaySet types
export type DisplaySet = DisplaySetTypes.DisplaySet;
export type DisplaySetSeriesMetadataInvalidatedEvent =
DisplaySetTypes.DisplaySetSeriesMetadataInvalidatedEvent;
// HangingProtocol types
export namespace HangingProtocol {
export type DisplaySetInfo = HangingProtocolTypes.DisplaySetInfo;
export type ViewportMatchDetails = HangingProtocolTypes.ViewportMatchDetails;
export type DisplaySetMatchDetails = HangingProtocolTypes.DisplaySetMatchDetails;
export type DisplaySetAndViewportOptions = HangingProtocolTypes.DisplaySetAndViewportOptions;
export type DisplayArea = HangingProtocolTypes.DisplayArea;
export type SetProtocolOptions = HangingProtocolTypes.SetProtocolOptions;
export type HangingProtocolMatchDetails = HangingProtocolTypes.HangingProtocolMatchDetails;
export type ConstraintValue = HangingProtocolTypes.ConstraintValue;
export type Constraint = HangingProtocolTypes.Constraint;
export type MatchingRule = HangingProtocolTypes.MatchingRule;
export type ViewportLayoutOptions = HangingProtocolTypes.ViewportLayoutOptions;
export type ViewportStructure = HangingProtocolTypes.ViewportStructure;
export type DisplaySetSelector = HangingProtocolTypes.DisplaySetSelector;
export type SyncGroup = HangingProtocolTypes.SyncGroup;
export type CustomOptionAttribute<T> = HangingProtocolTypes.CustomOptionAttribute<T>;
export type CustomOption<T> = HangingProtocolTypes.CustomOption<T>;
export type initialImageOptions = HangingProtocolTypes.initialImageOptions;
export type ViewportOptions = HangingProtocolTypes.ViewportOptions;
export type DisplaySetOptions = HangingProtocolTypes.DisplaySetOptions;
export type Viewport = HangingProtocolTypes.Viewport;
export type StageStatus = HangingProtocolTypes.StageStatus;
export type StageActivation = HangingProtocolTypes.StageActivation;
export type ProtocolStage = HangingProtocolTypes.ProtocolStage;
export type ProtocolNotifications = HangingProtocolTypes.ProtocolNotifications;
export type Protocol = HangingProtocolTypes.Protocol;
export type ProtocolGenerator = HangingProtocolTypes.ProtocolGenerator;
export type HPInfo = HangingProtocolTypes.HPInfo;
}
// IPubSub types
export namespace PubSub {
export type IPubSub = IPubSubTypes.default;
export type Subscription = IPubSubTypes.Subscription;
}
// PanelModule types
export namespace PanelModule {
export type Panel = PanelModuleTypes.Panel;
export type ActivatePanelTriggers = PanelModuleTypes.ActivatePanelTriggers;
export type PanelEvent = PanelModuleTypes.PanelEvent;
export type ActivatePanelEvent = PanelModuleTypes.ActivatePanelEvent;
}
// StudyMetadata types
export namespace StudyMetadata {
export type PatientMetadata = StudyMetadataTypes.PatientMetadata;
export type StudyMetadata = StudyMetadataTypes.StudyMetadata;
export type SeriesMetadata = StudyMetadataTypes.SeriesMetadata;
export type InstanceMetadata = StudyMetadataTypes.InstanceMetadata;
}
// ViewportGrid types
export namespace ViewportGrid {
export type Viewport = ViewportGridTypes.GridViewport;
export type Layout = ViewportGridTypes.Layout;
export type State = ViewportGridTypes.ViewportGridState;
export type Viewports = ViewportGridTypes.GridViewports;
export type GridViewportOptions = ViewportGridTypes.GridViewportOptions;
}
}
export interface PresentationIds {}
export type withAppTypes<T = object> = T &
AppTypes.Services &
AppTypes.Managers & {
[key: string]: any;
};
[key: string]: unknown;
} & AppTypes.Config;
export type withTestTypes<T = object> = T & AppTypes.Test;
}
+5
View File
@@ -5,8 +5,13 @@ export type DisplaySet = {
instances: InstanceMetadata[];
StudyInstanceUID: string;
SeriesInstanceUID?: string;
SeriesNumber?: number;
SeriesDescription?: string;
numImages?: number;
unsupported?: boolean;
Modality?: string;
imageIds?: string[];
images?: unknown[];
};
export type DisplaySetSeriesMetadataInvalidatedEvent = {
+20 -2
View File
@@ -81,6 +81,7 @@ export type ConstraintValue =
| number
| boolean
| []
| string[]
| {
value: string | number | boolean | [];
};
@@ -92,6 +93,7 @@ export type Constraint = {
// A caseless contains
containsI?: string;
contains?: ConstraintValue;
doesNotContain?: ConstraintValue;
greaterThan?: ConstraintValue;
};
@@ -101,7 +103,7 @@ export type MatchingRule = {
// Defaults to 1
weight?: number;
attribute: string;
constraint: Constraint;
constraint?: Constraint;
// Not required by default
required?: boolean;
};
@@ -140,6 +142,11 @@ export type DisplaySetSelector = {
studyMatchingRules?: MatchingRule[];
};
export type OverlaySelector = {
id?: string;
matchingRules: MatchingRule[];
};
export type SyncGroup = {
type: string;
id: string;
@@ -166,6 +173,7 @@ export type ViewportOptions = {
viewportType?: CustomOption<string>;
id?: string;
orientation?: CustomOption<string>;
background?: CustomOption<[number, number, number]>;
viewportId?: string;
displayArea?: DisplayArea;
initialImageOptions?: CustomOption<initialImageOptions>;
@@ -189,9 +197,17 @@ export type DisplaySetOptions = {
options?: Record<string, unknown>;
};
// some options for overlays
// such as segmentation options
export type OverlayOptions = {
id?: string;
options?: Record<string, unknown>;
};
export type Viewport = {
viewportOptions: ViewportOptions;
displaySets: DisplaySetOptions[];
overlays?: OverlayOptions[];
};
/**
@@ -271,7 +287,7 @@ export type ProtocolNotifications = {
/**
* 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.
* as well as a set of stages that represent individual views.
* Additionally, the display set selectors are used to choose from the existing
* display sets. The hanging protocol definition here does NOT allow
* redefining the display sets to use, but only selects the views to show.
@@ -283,6 +299,8 @@ export type Protocol = {
description?: string;
/** Maps ids to display set selectors to choose display sets */
displaySetSelectors: Record<string, DisplaySetSelector>;
/** overlay selectors that decide whether an overlay such as segmentation should be shown or not */
overlaySelectors?: Record<string, OverlaySelector>;
/** A default viewport to use for any stage to select new viewport layouts. */
defaultViewport?: Viewport;
stages: ProtocolStage[];
-2
View File
@@ -5,7 +5,6 @@ import {
ViewportGridService,
ToolbarService,
DisplaySetService,
StateSyncService,
UINotificationService,
UIModalService,
WorkflowStepsService,
@@ -29,7 +28,6 @@ interface Services {
viewportGridService?: ViewportGridService;
uiModalService?: UIModalService;
uiNotificationService?: UINotificationService;
stateSyncService?: StateSyncService;
workflowStepsService: WorkflowStepsService;
cineService?: CineService;
userAuthenticationService?: UserAuthenticationService;
@@ -0,0 +1,70 @@
export interface GridViewportOptions {
id?: string;
viewportId?: string;
viewportType?: string;
toolGroupId?: string;
presentationIds?: AppTypes.PresentationIds;
flipHorizontal?: boolean;
//
orientation?: string;
allowUnmatchedView?: boolean;
needsRerendering?: boolean;
background?: [number, number, number];
syncGroups?: unknown[];
rotation?: number;
initialImageOptions?: unknown;
customViewportProps?: Record<string, unknown>;
//
displayArea?: unknown;
viewReference?: unknown;
}
export interface GridViewport {
viewportId: string;
displaySetInstanceUIDs: string[];
viewportOptions: GridViewportOptions;
displaySetSelectors: unknown[];
displaySetOptions: unknown[];
x: number;
y: number;
width: number;
height: number;
viewportLabel: string | null;
isReady: boolean;
}
export interface Layout {
numRows: number;
numCols: number;
layoutType: string;
}
export type GridViewports = Map<string, GridViewport>;
export interface ViewportGridState {
activeViewportId: string | null;
layout: Layout;
isHangingProtocolLayout: boolean;
viewports: GridViewports;
}
export type SetDisplaySetsForViewportsProps = Array<{
viewportId: string;
displaySetInstanceUIDs: string[];
viewportOptions?: AppTypes.ViewportGrid.GridViewportOptions;
displaySetOptions?: Array<{
id?: string;
voi?: {
windowWidth: number;
windowCenter: number;
};
voiInverted?: boolean;
blendMode?: string;
slabThickness?: number;
colormap?: {
name: string;
opacity?: number;
};
displayPreset?: string;
}>;
}>;
-2
View File
@@ -7,7 +7,6 @@ import type {
BaseDataSourceConfigurationAPI,
BaseDataSourceConfigurationAPIItem,
} from './DataSourceConfigurationAPI';
import type DisplaySet from '../services/DisplaySetService/DisplaySet';
export type * from '../services/ViewportGridService';
export type * from '../services/CustomizationService/types';
@@ -32,5 +31,4 @@ export {
DataSourceDefinition,
BaseDataSourceConfigurationAPI,
BaseDataSourceConfigurationAPIItem,
DisplaySet,
};
@@ -50,7 +50,10 @@ const combineFrameInstance = (frame, instance) => {
// Todo: we should cache this combined instance somewhere, maybe add it
// back to the dicomMetaStore so we don't have to do this again.
return newInstance;
return {
...newInstance,
ImagePositionPatient: newInstance.ImagePositionPatient ?? [0, 0, frameNumber],
};
} else {
return instance;
}
@@ -12,7 +12,12 @@
* @returns tabs - The prop object expected by the StudyBrowser component
*/
export function createStudyBrowserTabs(primaryStudyInstanceUIDs, studyDisplayList, displaySets, recentTimeframeMS = 31536000000) {
export function createStudyBrowserTabs(
primaryStudyInstanceUIDs,
studyDisplayList,
displaySets,
recentTimeframeMS = 31536000000
) {
const primaryStudies = [];
const allStudies = [];
+4
View File
@@ -24,6 +24,10 @@ export default function getImageId(instance, frame, thumbnail = false) {
return;
}
if (instance.imageId && frame === undefined) {
return instance.imageId;
}
if (typeof instance.getImageId === 'function') {
return instance.getImageId();
}
-1
View File
@@ -41,7 +41,6 @@ describe('Top level exports', () => {
'resolveObjectPath',
'hierarchicalListUtils',
'progressTrackingUtils',
'subscribeToNextViewportGridChange',
'uuidv4',
'addAccessors',
].sort();
-2
View File
@@ -36,7 +36,6 @@ import {
sortingCriteria,
seriesSortCriteria,
} from './sortStudy';
import { subscribeToNextViewportGridChange } from './subscribeToNextViewportGridChange';
import { splitComma, getSplitParam } from './splitComma';
import { createStudyBrowserTabs } from './createStudyBrowserTabs';
import { sopClassDictionary } from './sopClassDictionary';
@@ -81,7 +80,6 @@ const utils = {
debounce,
roundNumber,
downloadCSVReport,
subscribeToNextViewportGridChange,
splitComma,
getSplitParam,
generateAcceptHeader,
+13 -13
View File
@@ -13,10 +13,10 @@
function roundNumber(value, precision = 2) {
if (Array.isArray(value)) {
return value.map((v) => roundNumber(v, precision)).join(", ");
return value.map(v => roundNumber(v, precision)).join(', ');
}
if (value === undefined || value === null || value === "") {
return "NaN";
if (value === undefined || value === null || value === '') {
return 'NaN';
}
value = Number(value);
const absValue = Math.abs(value);
@@ -27,16 +27,16 @@ function roundNumber(value, precision = 2) {
absValue >= 100
? precision - 2
: absValue >= 10
? precision - 1
: absValue >= 1
? precision
: absValue >= 0.1
? precision + 1
: absValue >= 0.01
? precision + 2
: absValue >= 0.001
? precision + 3
: precision + 4;
? precision - 1
: absValue >= 1
? precision
: absValue >= 0.1
? precision + 1
: absValue >= 0.01
? precision + 2
: absValue >= 0.001
? precision + 3
: precision + 4;
return value.toFixed(fixedPrecision);
}
@@ -1,30 +0,0 @@
import { ViewportGridService } from '../services';
/**
* Subscribes to the very next LAYOUT_CHANGED event that
* is not currently on the event queue. The subscriptions are made on a 'zero'
* timeout so as to avoid responding to any of those events currently on the event queue.
* The subscription persists only for a single invocation of either event.
* Once either event is fired, the subscriptions are unsubscribed.
* @param viewportGridService the viewport grid service to subscribe to
* @param gridChangeCallback the callback
*/
function subscribeToNextViewportGridChange(
viewportGridService: ViewportGridService,
gridChangeCallback: (arg: unknown) => void
): void {
const subscriber = () => {
const callback = (callbackProps: unknown) => {
subscriptions.forEach(subscription => subscription.unsubscribe());
gridChangeCallback(callbackProps);
};
const subscriptions = [
viewportGridService.subscribe(viewportGridService.EVENTS.LAYOUT_CHANGED, callback),
];
};
window.setTimeout(subscriber, 0);
}
export { subscribeToNextViewportGridChange };
+1 -1
View File
@@ -6,7 +6,7 @@
*/
export default function toNumber(val) {
if (Array.isArray(val)) {
return val.map(v => (v !== undefined ? Number(v) : v));
return [...val].map(v => (v !== undefined ? Number(v) : v));
} else {
return val !== undefined ? Number(val) : val;
}