OHIF with Cornerstone3D 2.0 (#4347)
This commit is contained in:
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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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 = {
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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;
|
||||
}>;
|
||||
}>;
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ describe('Top level exports', () => {
|
||||
'resolveObjectPath',
|
||||
'hierarchicalListUtils',
|
||||
'progressTrackingUtils',
|
||||
'subscribeToNextViewportGridChange',
|
||||
'uuidv4',
|
||||
'addAccessors',
|
||||
].sort();
|
||||
|
||||
@@ -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,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 };
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user