2023-02-10 20:30:56 +01:00
|
|
|
import MODULE_TYPES from './MODULE_TYPES';
|
|
|
|
|
import log from '../log';
|
2024-05-08 18:12:49 +02:00
|
|
|
import { PubSubService, ServiceProvidersManager } from '../services';
|
2023-02-10 20:30:56 +01:00
|
|
|
import { HotkeysManager, CommandsManager } from '../classes';
|
2024-07-05 18:33:07 +02:00
|
|
|
import type { DataSourceDefinition } from '../types';
|
|
|
|
|
import type AppTypes from '../types/AppTypes';
|
2023-02-10 20:30:56 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* This is the arguments given to create the extension.
|
|
|
|
|
*/
|
|
|
|
|
export interface ExtensionConstructor {
|
2024-05-08 18:12:49 +02:00
|
|
|
servicesManager: AppTypes.ServicesManager;
|
2024-04-02 15:35:30 +02:00
|
|
|
serviceProvidersManager: ServiceProvidersManager;
|
2023-02-10 20:30:56 +01:00
|
|
|
commandsManager: CommandsManager;
|
|
|
|
|
hotkeysManager: HotkeysManager;
|
2024-05-24 18:21:09 +02:00
|
|
|
appConfig: AppTypes.Config;
|
2023-02-10 20:30:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The configuration of an extension.
|
|
|
|
|
* This uses type as the extension manager only knows that the configuration
|
|
|
|
|
* is an object of some sort, and doesn't know anything else about it.
|
|
|
|
|
*/
|
|
|
|
|
export type ExtensionConfiguration = Record<string, unknown>;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The parameters passed to the extension.
|
|
|
|
|
*/
|
|
|
|
|
export interface ExtensionParams extends ExtensionConstructor {
|
|
|
|
|
extensionManager: ExtensionManager;
|
2024-05-08 18:12:49 +02:00
|
|
|
servicesManager: AppTypes.ServicesManager;
|
2024-04-02 15:35:30 +02:00
|
|
|
serviceProvidersManager: ServiceProvidersManager;
|
2023-02-10 20:30:56 +01:00
|
|
|
configuration?: ExtensionConfiguration;
|
2024-07-05 19:19:42 +02:00
|
|
|
peerImport: (moduleId: string) => Promise<any>;
|
2023-02-10 20:30:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The type of an actual extension instance.
|
|
|
|
|
* This is an interface as it declares possible calls, but extensions can
|
|
|
|
|
* have more values than this.
|
|
|
|
|
*/
|
|
|
|
|
export interface Extension {
|
2023-02-17 21:04:07 +01:00
|
|
|
id: string;
|
2023-03-22 22:45:28 +01:00
|
|
|
preRegistration?: (p: ExtensionParams) => Promise<void> | void;
|
|
|
|
|
getHangingProtocolModule?: (p: ExtensionParams) => unknown;
|
|
|
|
|
getCommandsModule?: (p: ExtensionParams) => CommandsModule;
|
2023-04-18 17:17:50 +02:00
|
|
|
getViewportModule?: (p: ExtensionParams) => unknown;
|
|
|
|
|
getUtilityModule?: (p: ExtensionParams) => unknown;
|
feat(RT): add dicom RT support via volume viewports (#3310)
* feat: initial RT support
* make the segmentation service work with representation data
* feat: make segmentation service work with representations
* fix rtss vis
* fix: rt hydration
* fix the rendering of rt names
* fix imports
* refactor: Modify status and click handling for hydration of RTStructures
Modify status and click handling for hydration of RTStructures by renaming `onPillClick` to `onStatusClick` in `OHIFCornerstoneRTViewport.tsx` and `_getStatusComponent.tsx` files. Also, update initial segmentation configurations in `PanelSegmentation.tsx` and simplify configuration changes and values for segmentation service in `SegmentationService.ts`. Finally, remove console debug in `CornerstoneViewportService.ts`.
* wip for highlighting contours
* refactor rt displayset code
* review code update
* update cornerstone dependencies
* refactor: Update license year, version number, and minor code cleanup
This commit updates the license year in several files, updates the version number in package.json, and contains minor code cleanup in two files.
* add bulkdataURI retrieve for RT
* fix package version
* apply review comments
* apply review comments
* apply review comments
* feat(panels): refactor and streamline segmentation configuration and inputs
Rewrote state hooks and streamlined the configuration input for `PanelSegmentation` to be more verbose and reusable. Included several new input types, including the `InputRange` component which now shows a fixed floating value based on the step provided. The `SegmentationConfig` component now works with dynamic values controlled by `initialConfig`. These changes should improve function usability and make the code more maintainable going forward.
* fix various bugs
* fix contour delete by upgrade cs3d version
* feat(viewport, inputNumber, segmentationConfig, orthanc): Implement minimum and maximum values for input number components, and useBulkDataURI for Orthanc configuration. Compare measurement view planes with absolute viewport view planes in Cornerstone viewport.
* update yarn lock
2023-04-26 18:21:08 +02:00
|
|
|
getCustomizationModule?: (p: ExtensionParams) => unknown;
|
2023-08-08 20:39:31 +02:00
|
|
|
getSopClassHandlerModule?: (p: ExtensionParams) => unknown;
|
2024-03-27 21:01:32 +01:00
|
|
|
getToolbarModule?: (p: ExtensionParams) => unknown;
|
2024-04-10 05:57:34 +02:00
|
|
|
getPanelModule?: (p: ExtensionParams) => unknown;
|
2024-07-05 18:33:07 +02:00
|
|
|
onModeEnter?: (p: AppTypes) => void;
|
2023-04-18 17:17:50 +02:00
|
|
|
onModeExit?: () => void;
|
2023-02-10 20:30:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ExtensionRegister = {
|
|
|
|
|
id: string;
|
|
|
|
|
create: (p: ExtensionParams) => Extension;
|
|
|
|
|
};
|
2019-08-13 21:36:46 +02:00
|
|
|
|
2023-03-22 22:45:28 +01:00
|
|
|
export type CommandsModule = {
|
|
|
|
|
actions: Record<string, unknown>;
|
|
|
|
|
definitions: Record<string, unknown>;
|
|
|
|
|
defaultContext?: string;
|
|
|
|
|
};
|
|
|
|
|
|
2023-07-20 17:43:05 +02:00
|
|
|
export default class ExtensionManager extends PubSubService {
|
|
|
|
|
public static readonly EVENTS = {
|
|
|
|
|
ACTIVE_DATA_SOURCE_CHANGED: 'event::activedatasourcechanged',
|
|
|
|
|
};
|
|
|
|
|
|
2024-03-27 21:01:32 +01:00
|
|
|
public static readonly MODULE_TYPES = MODULE_TYPES;
|
|
|
|
|
|
2023-02-10 20:30:56 +01:00
|
|
|
private _commandsManager: CommandsManager;
|
2024-05-08 18:12:49 +02:00
|
|
|
private _servicesManager: AppTypes.ServicesManager;
|
2023-02-10 20:30:56 +01:00
|
|
|
private _hotkeysManager: HotkeysManager;
|
2024-04-02 15:35:30 +02:00
|
|
|
private _serviceProvidersManager: ServiceProvidersManager;
|
2024-03-27 21:01:32 +01:00
|
|
|
private modulesMap: Record<string, unknown>;
|
|
|
|
|
private modules: Record<string, any[]>;
|
|
|
|
|
private registeredExtensionIds: string[];
|
|
|
|
|
private moduleTypeNames: string[];
|
|
|
|
|
private _appConfig: any;
|
|
|
|
|
private _extensionLifeCycleHooks: {
|
|
|
|
|
onModeEnter: Record<string, any>;
|
|
|
|
|
onModeExit: Record<string, any>;
|
|
|
|
|
};
|
|
|
|
|
private dataSourceMap: Record<string, any>;
|
|
|
|
|
private dataSourceDefs: Record<string, any>;
|
|
|
|
|
private defaultDataSourceName: string;
|
|
|
|
|
private activeDataSource: string;
|
2024-07-05 19:19:42 +02:00
|
|
|
private peerImport: (moduleId) => Promise<any>;
|
2023-02-10 20:30:56 +01:00
|
|
|
|
2021-05-28 20:32:48 +02:00
|
|
|
constructor({
|
|
|
|
|
commandsManager,
|
|
|
|
|
servicesManager,
|
2024-04-02 15:35:30 +02:00
|
|
|
serviceProvidersManager,
|
2021-05-28 20:32:48 +02:00
|
|
|
hotkeysManager,
|
|
|
|
|
appConfig = {},
|
2023-02-10 20:30:56 +01:00
|
|
|
}: ExtensionConstructor) {
|
2023-07-20 17:43:05 +02:00
|
|
|
super(ExtensionManager.EVENTS);
|
2019-08-13 21:36:46 +02:00
|
|
|
this.modules = {};
|
|
|
|
|
this.registeredExtensionIds = [];
|
|
|
|
|
this.moduleTypeNames = Object.values(MODULE_TYPES);
|
|
|
|
|
//
|
|
|
|
|
this._commandsManager = commandsManager;
|
2019-11-13 21:02:14 +01:00
|
|
|
this._servicesManager = servicesManager;
|
2024-04-02 15:35:30 +02:00
|
|
|
this._serviceProvidersManager = serviceProvidersManager;
|
2020-08-19 21:02:59 +02:00
|
|
|
this._hotkeysManager = hotkeysManager;
|
2019-12-16 20:07:53 +01:00
|
|
|
this._appConfig = appConfig;
|
2019-08-13 21:36:46 +02:00
|
|
|
|
2020-05-07 20:04:33 +02:00
|
|
|
this.modulesMap = {};
|
2019-08-13 21:36:46 +02:00
|
|
|
this.moduleTypeNames.forEach(moduleType => {
|
|
|
|
|
this.modules[moduleType] = [];
|
|
|
|
|
});
|
2020-08-04 11:11:23 +02:00
|
|
|
this._extensionLifeCycleHooks = { onModeEnter: {}, onModeExit: {} };
|
2020-05-07 20:04:33 +02:00
|
|
|
this.dataSourceMap = {};
|
2022-12-29 16:13:06 +01:00
|
|
|
this.dataSourceDefs = {};
|
2020-06-05 17:36:11 +02:00
|
|
|
this.defaultDataSourceName = appConfig.defaultDataSourceName;
|
2023-07-20 17:43:05 +02:00
|
|
|
this.activeDataSource = appConfig.defaultDataSourceName;
|
2024-07-05 19:19:42 +02:00
|
|
|
this.peerImport = appConfig.peerImport;
|
2020-06-05 17:36:11 +02:00
|
|
|
}
|
2020-05-13 12:06:13 +02:00
|
|
|
|
2023-07-20 17:43:05 +02:00
|
|
|
public setActiveDataSource(dataSource: string): void {
|
|
|
|
|
if (this.activeDataSource === dataSource) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.activeDataSource = dataSource;
|
|
|
|
|
|
|
|
|
|
this._broadcastEvent(
|
|
|
|
|
ExtensionManager.EVENTS.ACTIVE_DATA_SOURCE_CHANGED,
|
|
|
|
|
this.dataSourceDefs[this.activeDataSource]
|
|
|
|
|
);
|
2019-08-13 21:36:46 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-19 22:31:59 +02:00
|
|
|
public getRegisteredExtensionIds() {
|
|
|
|
|
return [...this.registeredExtensionIds];
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-28 14:58:39 +02:00
|
|
|
private getUniqueServicesList(servicesManager: AppTypes.ServicesManager) {
|
|
|
|
|
// Make sure only one service instance is returned because almost all services are
|
|
|
|
|
// registered with different keys (eg: StudyPrefetcherService and studyPrefetcherService)
|
|
|
|
|
return Array.from(new Set(Object.values(servicesManager.services)));
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-10 20:30:56 +01:00
|
|
|
/**
|
|
|
|
|
* Calls all the services and extension on mode enters.
|
|
|
|
|
* The service onModeEnter is called first
|
|
|
|
|
* Then registered extensions onModeEnter is called
|
|
|
|
|
* This is supposed to setup the extension for a standard entry.
|
|
|
|
|
*/
|
|
|
|
|
public onModeEnter(): void {
|
2020-07-03 19:18:59 +02:00
|
|
|
const {
|
|
|
|
|
registeredExtensionIds,
|
|
|
|
|
_servicesManager,
|
|
|
|
|
_commandsManager,
|
2020-08-19 21:02:59 +02:00
|
|
|
_hotkeysManager,
|
2020-07-03 19:18:59 +02:00
|
|
|
_extensionLifeCycleHooks,
|
|
|
|
|
} = this;
|
2024-06-28 14:58:39 +02:00
|
|
|
const services = this.getUniqueServicesList(_servicesManager);
|
2020-07-03 19:18:59 +02:00
|
|
|
|
2022-12-09 16:58:01 +01:00
|
|
|
// The onModeEnter of the service must occur BEFORE the extension
|
|
|
|
|
// onModeEnter in order to reset the state to a standard state
|
|
|
|
|
// before the extension restores and cached data.
|
2024-06-28 14:58:39 +02:00
|
|
|
for (const service of services) {
|
2022-11-24 22:28:10 +01:00
|
|
|
service?.onModeEnter?.();
|
|
|
|
|
}
|
2020-08-04 11:11:23 +02:00
|
|
|
|
2020-07-03 19:18:59 +02:00
|
|
|
registeredExtensionIds.forEach(extensionId => {
|
|
|
|
|
const onModeEnter = _extensionLifeCycleHooks.onModeEnter[extensionId];
|
|
|
|
|
|
|
|
|
|
if (typeof onModeEnter === 'function') {
|
|
|
|
|
onModeEnter({
|
|
|
|
|
servicesManager: _servicesManager,
|
|
|
|
|
commandsManager: _commandsManager,
|
2021-05-28 20:32:48 +02:00
|
|
|
hotkeysManager: _hotkeysManager,
|
2020-07-03 19:18:59 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-10 20:30:56 +01:00
|
|
|
public onModeExit(): void {
|
2023-09-01 22:19:39 +02:00
|
|
|
const { registeredExtensionIds, _servicesManager, _commandsManager, _extensionLifeCycleHooks } =
|
|
|
|
|
this;
|
2024-06-28 14:58:39 +02:00
|
|
|
const services = this.getUniqueServicesList(_servicesManager);
|
2020-08-04 11:11:23 +02:00
|
|
|
|
|
|
|
|
registeredExtensionIds.forEach(extensionId => {
|
|
|
|
|
const onModeExit = _extensionLifeCycleHooks.onModeExit[extensionId];
|
|
|
|
|
|
|
|
|
|
if (typeof onModeExit === 'function') {
|
|
|
|
|
onModeExit({
|
|
|
|
|
servicesManager: _servicesManager,
|
|
|
|
|
commandsManager: _commandsManager,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
2022-12-09 16:58:01 +01:00
|
|
|
|
|
|
|
|
// The service onModeExit calls must occur after the extension ones
|
|
|
|
|
// so that extension ones can store/restore data.
|
2024-06-28 14:58:39 +02:00
|
|
|
for (const service of services) {
|
2022-12-09 16:58:01 +01:00
|
|
|
try {
|
|
|
|
|
service?.onModeExit?.();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('onModeExit caught', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-08-04 11:11:23 +02:00
|
|
|
}
|
|
|
|
|
|
2019-08-13 21:36:46 +02:00
|
|
|
/**
|
|
|
|
|
* An array of extensions, or an array of arrays that contains extension
|
|
|
|
|
* configuration pairs.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object[]} extensions - Array of extensions
|
|
|
|
|
*/
|
2023-02-10 20:30:56 +01:00
|
|
|
public registerExtensions = async (
|
2023-09-01 22:19:39 +02:00
|
|
|
extensions: (ExtensionRegister | [ExtensionRegister, ExtensionConfiguration])[],
|
2023-02-10 20:30:56 +01:00
|
|
|
dataSources: unknown[] = []
|
|
|
|
|
): Promise<void> => {
|
2022-07-27 18:39:04 +02:00
|
|
|
// Todo: we ideally should be able to run registrations in parallel
|
|
|
|
|
// but currently since some extensions need to be registered before
|
|
|
|
|
// others, we need to run them sequentially. We need a postInit hook
|
|
|
|
|
// to avoid this sequential async registration
|
2023-02-17 21:04:07 +01:00
|
|
|
for (let i = 0; i < extensions.length; i++) {
|
|
|
|
|
const extension = extensions[i];
|
2019-08-13 21:36:46 +02:00
|
|
|
const hasConfiguration = Array.isArray(extension);
|
2022-07-27 18:39:04 +02:00
|
|
|
try {
|
|
|
|
|
if (hasConfiguration) {
|
2023-02-17 21:04:07 +01:00
|
|
|
// Important: for some reason in the line below the type
|
|
|
|
|
// of extension is not recognized as [ExtensionRegister,
|
|
|
|
|
// ExtensionConfiguration] by babel DON"T CHANGE IT
|
|
|
|
|
// Same for the for loop above don't use
|
|
|
|
|
// for (const extension of extensions)
|
|
|
|
|
const ohifExtension = extension[0];
|
|
|
|
|
const configuration = extension[1];
|
2023-09-01 22:19:39 +02:00
|
|
|
await this.registerExtension(ohifExtension, configuration, dataSources);
|
2022-07-27 18:39:04 +02:00
|
|
|
} else {
|
|
|
|
|
await this.registerExtension(extension, {}, dataSources);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(error);
|
2019-08-13 21:36:46 +02:00
|
|
|
}
|
2022-07-27 18:39:04 +02:00
|
|
|
}
|
2020-05-09 20:23:43 +02:00
|
|
|
};
|
2019-08-13 21:36:46 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* TODO: Id Management: SopClassHandlers currently refer to viewport module by id; setting the extension id as viewport module id is a workaround for now
|
|
|
|
|
* @param {Object} extension
|
|
|
|
|
* @param {Object} configuration
|
|
|
|
|
*/
|
2023-02-10 20:30:56 +01:00
|
|
|
public registerExtension = async (
|
|
|
|
|
extension: ExtensionRegister,
|
2022-07-27 18:39:04 +02:00
|
|
|
configuration = {},
|
|
|
|
|
dataSources = []
|
2023-02-10 20:30:56 +01:00
|
|
|
): Promise<void> => {
|
2019-08-13 21:36:46 +02:00
|
|
|
if (!extension) {
|
2021-05-28 20:32:48 +02:00
|
|
|
throw new Error('Attempting to register a null/undefined extension.');
|
2019-08-13 21:36:46 +02:00
|
|
|
}
|
|
|
|
|
|
2023-02-10 20:30:56 +01:00
|
|
|
const extensionId = extension.id;
|
2019-08-13 21:36:46 +02:00
|
|
|
|
|
|
|
|
if (!extensionId) {
|
2020-05-13 12:06:13 +02:00
|
|
|
// Note: Mode framework cannot function without IDs.
|
|
|
|
|
log.warn(extension);
|
|
|
|
|
throw new Error(`Extension ID not set`);
|
2019-08-13 21:36:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.registeredExtensionIds.includes(extensionId)) {
|
|
|
|
|
log.warn(
|
|
|
|
|
`Extension ID ${extensionId} has already been registered. Exiting before duplicating modules.`
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// preRegistrationHook
|
|
|
|
|
if (extension.preRegistration) {
|
2022-07-27 18:39:04 +02:00
|
|
|
await extension.preRegistration({
|
2019-11-14 15:50:37 +01:00
|
|
|
servicesManager: this._servicesManager,
|
2024-04-02 15:35:30 +02:00
|
|
|
serviceProvidersManager: this._serviceProvidersManager,
|
2019-12-02 17:29:04 +01:00
|
|
|
commandsManager: this._commandsManager,
|
2020-08-19 21:02:59 +02:00
|
|
|
hotkeysManager: this._hotkeysManager,
|
2022-07-27 18:39:04 +02:00
|
|
|
extensionManager: this,
|
2019-12-16 20:07:53 +01:00
|
|
|
appConfig: this._appConfig,
|
2019-11-13 21:02:14 +01:00
|
|
|
configuration,
|
|
|
|
|
});
|
2019-08-13 21:36:46 +02:00
|
|
|
}
|
|
|
|
|
|
2020-07-03 19:18:59 +02:00
|
|
|
if (extension.onModeEnter) {
|
2023-09-01 22:19:39 +02:00
|
|
|
this._extensionLifeCycleHooks.onModeEnter[extensionId] = extension.onModeEnter;
|
2020-07-03 19:18:59 +02:00
|
|
|
}
|
|
|
|
|
|
2020-08-04 11:11:23 +02:00
|
|
|
if (extension.onModeExit) {
|
2023-09-01 22:19:39 +02:00
|
|
|
this._extensionLifeCycleHooks.onModeExit[extensionId] = extension.onModeExit;
|
2020-08-04 11:11:23 +02:00
|
|
|
}
|
|
|
|
|
|
2019-08-13 21:36:46 +02:00
|
|
|
// Register Modules
|
|
|
|
|
this.moduleTypeNames.forEach(moduleType => {
|
|
|
|
|
const extensionModule = this._getExtensionModule(
|
|
|
|
|
moduleType,
|
|
|
|
|
extension,
|
2019-12-16 20:07:53 +01:00
|
|
|
extensionId,
|
|
|
|
|
configuration
|
2019-08-13 21:36:46 +02:00
|
|
|
);
|
|
|
|
|
|
2024-03-27 21:01:32 +01:00
|
|
|
if (!extensionModule) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2019-08-13 21:36:46 +02:00
|
|
|
|
2024-03-27 21:01:32 +01:00
|
|
|
switch (moduleType) {
|
|
|
|
|
case MODULE_TYPES.COMMANDS:
|
|
|
|
|
this._initCommandsModule(extensionModule);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case MODULE_TYPES.DATA_SOURCE:
|
|
|
|
|
this._initDataSourcesModule(extensionModule, extensionId, dataSources);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case MODULE_TYPES.HANGING_PROTOCOL:
|
|
|
|
|
this._initHangingProtocolsModule(extensionModule, extensionId);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case MODULE_TYPES.PANEL:
|
|
|
|
|
this._initPanelModule(extensionModule, extensionId);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case MODULE_TYPES.TOOLBAR:
|
|
|
|
|
this._initToolbarModule(extensionModule, extensionId);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case MODULE_TYPES.VIEWPORT:
|
|
|
|
|
case MODULE_TYPES.SOP_CLASS_HANDLER:
|
|
|
|
|
case MODULE_TYPES.CONTEXT:
|
|
|
|
|
case MODULE_TYPES.LAYOUT_TEMPLATE:
|
|
|
|
|
case MODULE_TYPES.CUSTOMIZATION:
|
|
|
|
|
case MODULE_TYPES.STATE_SYNC:
|
|
|
|
|
case MODULE_TYPES.UTILITY:
|
|
|
|
|
this.processExtensionModule(extensionModule, extensionId, moduleType);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new Error(`Module type invalid: ${moduleType}`);
|
2019-08-13 21:36:46 +02:00
|
|
|
}
|
2024-03-27 21:01:32 +01:00
|
|
|
|
|
|
|
|
this.modules[moduleType].push({
|
|
|
|
|
extensionId,
|
|
|
|
|
module: extensionModule,
|
|
|
|
|
});
|
2019-08-13 21:36:46 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Track extension registration
|
|
|
|
|
this.registeredExtensionIds.push(extensionId);
|
2020-05-09 20:23:43 +02:00
|
|
|
};
|
2019-08-13 21:36:46 +02:00
|
|
|
|
2024-03-27 21:01:32 +01:00
|
|
|
/**
|
|
|
|
|
* Retrieves the module entry associated with the given string entry
|
|
|
|
|
* @param stringEntry - The string entry to retrieve the module entry for which is
|
|
|
|
|
* in the format of `${extensionId}.${moduleType}.${moduleName}`
|
|
|
|
|
* @returns The module entry associated with the given string entry.
|
|
|
|
|
*/
|
2020-05-09 20:23:43 +02:00
|
|
|
getModuleEntry = stringEntry => {
|
2020-05-07 20:04:33 +02:00
|
|
|
return this.modulesMap[stringEntry];
|
2020-05-09 20:23:43 +02:00
|
|
|
};
|
2020-05-07 20:04:33 +02:00
|
|
|
|
2024-03-27 21:01:32 +01:00
|
|
|
/**
|
|
|
|
|
* Retrieves all modules of a given type for all registered extensions.
|
|
|
|
|
*
|
|
|
|
|
* @param moduleType - The type of modules to retrieve.
|
|
|
|
|
* @returns An array of modules of the specified type.
|
|
|
|
|
*/
|
|
|
|
|
getModulesByType = (moduleType: string) => {
|
|
|
|
|
return this.modules[moduleType];
|
|
|
|
|
};
|
|
|
|
|
|
2020-05-11 18:56:46 +02:00
|
|
|
getDataSources = dataSourceName => {
|
2020-06-05 17:36:11 +02:00
|
|
|
if (dataSourceName === undefined) {
|
2020-06-26 11:01:13 +02:00
|
|
|
// Default to the activeDataSource
|
2020-06-05 17:36:11 +02:00
|
|
|
dataSourceName = this.activeDataSource;
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-09 10:57:52 +02:00
|
|
|
// Note: this currently uses the data source name, which feels weird...
|
2020-05-11 18:56:46 +02:00
|
|
|
return this.dataSourceMap[dataSourceName];
|
2020-05-09 20:23:43 +02:00
|
|
|
};
|
2020-05-07 20:04:33 +02:00
|
|
|
|
2020-06-26 11:01:13 +02:00
|
|
|
getActiveDataSource = () => {
|
2020-07-03 19:18:59 +02:00
|
|
|
return this.dataSourceMap[this.activeDataSource];
|
2020-06-26 11:01:13 +02:00
|
|
|
};
|
|
|
|
|
|
2023-07-20 17:43:05 +02:00
|
|
|
/**
|
|
|
|
|
* Gets the data source definition for the given data source name.
|
|
|
|
|
* If no data source name is provided, the active data source definition is
|
|
|
|
|
* returned.
|
|
|
|
|
* @param dataSourceName the data source name
|
|
|
|
|
* @returns the data source definition
|
|
|
|
|
*/
|
2023-08-25 20:19:17 +02:00
|
|
|
getDataSourceDefinition = dataSourceName => {
|
2023-07-20 17:43:05 +02:00
|
|
|
if (dataSourceName === undefined) {
|
|
|
|
|
// Default to the activeDataSource
|
|
|
|
|
dataSourceName = this.activeDataSource;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.dataSourceDefs[dataSourceName];
|
2021-05-28 20:32:48 +02:00
|
|
|
};
|
|
|
|
|
|
2023-08-25 20:19:17 +02:00
|
|
|
/**
|
|
|
|
|
* Gets the data source definition for the active data source.
|
|
|
|
|
*/
|
|
|
|
|
getActiveDataSourceDefinition = () => {
|
|
|
|
|
return this.getDataSourceDefinition(this.activeDataSource);
|
|
|
|
|
};
|
|
|
|
|
|
2019-08-13 21:36:46 +02:00
|
|
|
/**
|
|
|
|
|
* @private
|
|
|
|
|
* @param {string} moduleType
|
|
|
|
|
* @param {Object} extension
|
|
|
|
|
* @param {string} extensionId - Used for logging warnings
|
|
|
|
|
*/
|
2020-05-09 20:23:43 +02:00
|
|
|
_getExtensionModule = (moduleType, extension, extensionId, configuration) => {
|
2019-08-13 21:36:46 +02:00
|
|
|
const getModuleFnName = 'get' + _capitalizeFirstCharacter(moduleType);
|
|
|
|
|
const getModuleFn = extension[getModuleFnName];
|
|
|
|
|
|
|
|
|
|
if (!getModuleFn) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2023-03-22 22:45:28 +01:00
|
|
|
const extensionModule = extension[getModuleFnName]({
|
2020-06-15 03:18:29 +02:00
|
|
|
appConfig: this._appConfig,
|
2019-12-02 17:29:04 +01:00
|
|
|
commandsManager: this._commandsManager,
|
2020-06-15 03:18:29 +02:00
|
|
|
servicesManager: this._servicesManager,
|
2020-08-19 21:02:59 +02:00
|
|
|
hotkeysManager: this._hotkeysManager,
|
|
|
|
|
extensionManager: this,
|
2019-12-16 20:07:53 +01:00
|
|
|
configuration,
|
2019-11-14 23:29:40 +01:00
|
|
|
});
|
2019-08-13 21:36:46 +02:00
|
|
|
|
|
|
|
|
if (!extensionModule) {
|
|
|
|
|
log.warn(
|
|
|
|
|
`Null or undefined returned when registering the ${getModuleFnName} module for the ${extensionId} extension`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return extensionModule;
|
|
|
|
|
} catch (ex) {
|
2023-03-22 22:45:28 +01:00
|
|
|
console.log(ex);
|
2021-03-01 16:52:54 +01:00
|
|
|
throw new Error(
|
2019-08-13 21:36:46 +02:00
|
|
|
`Exception thrown while trying to call ${getModuleFnName} for the ${extensionId} extension`
|
|
|
|
|
);
|
|
|
|
|
}
|
2020-05-09 20:23:43 +02:00
|
|
|
};
|
2019-08-13 21:36:46 +02:00
|
|
|
|
2022-09-21 05:40:26 +02:00
|
|
|
_initHangingProtocolsModule = (extensionModule, extensionId) => {
|
2023-02-10 22:46:09 +01:00
|
|
|
const { hangingProtocolService } = this._servicesManager.services;
|
2023-03-31 17:58:48 +02:00
|
|
|
extensionModule.forEach(({ name, protocol }) => {
|
2022-10-05 14:32:54 +02:00
|
|
|
if (protocol) {
|
|
|
|
|
// Only auto-register if protocol specified, otherwise let mode register
|
2023-03-31 17:58:48 +02:00
|
|
|
hangingProtocolService.addProtocol(name, protocol);
|
2022-10-05 14:32:54 +02:00
|
|
|
}
|
2022-09-21 05:40:26 +02:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2024-03-27 21:01:32 +01:00
|
|
|
_initPanelModule = (extensionModule, extensionId) => {
|
|
|
|
|
this.processExtensionModule(extensionModule, extensionId, MODULE_TYPES.PANEL);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_initToolbarModule = (extensionModule, extensionId) => {
|
|
|
|
|
// check if the toolbar module has a handler function for evaluation of
|
|
|
|
|
// the toolbar button state
|
|
|
|
|
const { toolbarService } = this._servicesManager.services;
|
|
|
|
|
extensionModule.forEach(toolbarButton => {
|
|
|
|
|
if (toolbarButton.evaluate) {
|
|
|
|
|
toolbarService.registerEvaluateFunction(toolbarButton.name, toolbarButton.evaluate);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Processes an extension module.
|
|
|
|
|
* @param extensionModule - The extension module to process.
|
|
|
|
|
* @param extensionId - The ID of the extension.
|
|
|
|
|
* @param moduleType - The type of the module.
|
|
|
|
|
*/
|
|
|
|
|
private processExtensionModule(extensionModule, extensionId: string, moduleType: string) {
|
|
|
|
|
extensionModule.forEach(element => {
|
|
|
|
|
if (!element.name) {
|
|
|
|
|
throw new Error(`Extension ID ${extensionId} module ${moduleType} element has no name`);
|
|
|
|
|
}
|
|
|
|
|
const id = `${extensionId}.${moduleType}.${element.name}`;
|
|
|
|
|
element.id = id;
|
|
|
|
|
this.modulesMap[id] = element;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-20 17:43:05 +02:00
|
|
|
/**
|
|
|
|
|
* Adds the given data source and optionally sets it as the active data source.
|
|
|
|
|
* The method does this by first creating the data source.
|
|
|
|
|
* @param dataSourceDef the data source definition to be added
|
|
|
|
|
* @param activate flag to indicate if the added data source should be set to the active data source
|
|
|
|
|
*/
|
2023-09-01 22:19:39 +02:00
|
|
|
addDataSource(dataSourceDef: DataSourceDefinition, options = { activate: false }) {
|
2023-07-20 17:43:05 +02:00
|
|
|
const existingDataSource = this.getDataSources(dataSourceDef.sourceName);
|
|
|
|
|
if (existingDataSource?.[0]) {
|
|
|
|
|
// The data source already exists and cannot be added.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._createDataSourceInstance(dataSourceDef);
|
|
|
|
|
|
|
|
|
|
if (options.activate) {
|
|
|
|
|
this.setActiveDataSource(dataSourceDef.sourceName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Updates the configuration of the given data source name. It first creates a new data source with
|
|
|
|
|
* the existing definition and the new configuration passed in.
|
|
|
|
|
* @param dataSourceName the name of the data source to update
|
|
|
|
|
* @param dataSourceConfiguration the new configuration to update the data source with
|
|
|
|
|
*/
|
2023-09-01 22:19:39 +02:00
|
|
|
updateDataSourceConfiguration(dataSourceName: string, dataSourceConfiguration: any) {
|
2023-07-20 17:43:05 +02:00
|
|
|
const existingDataSource = this.getDataSources(dataSourceName);
|
|
|
|
|
if (!existingDataSource?.[0]) {
|
|
|
|
|
// Cannot update a non existent data source.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dataSourceDef = this.dataSourceDefs[dataSourceName];
|
|
|
|
|
// Update the configuration.
|
|
|
|
|
dataSourceDef.configuration = dataSourceConfiguration;
|
|
|
|
|
this._createDataSourceInstance(dataSourceDef);
|
|
|
|
|
|
|
|
|
|
if (this.activeDataSource === dataSourceName) {
|
|
|
|
|
// When the active data source is changed/set, fire an event to indicate that its configuration has changed.
|
2023-09-01 22:19:39 +02:00
|
|
|
this._broadcastEvent(ExtensionManager.EVENTS.ACTIVE_DATA_SOURCE_CHANGED, dataSourceDef);
|
2023-07-20 17:43:05 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a data source instance from the given definition. The definition is
|
|
|
|
|
* added to dataSourceDefs and the created instance is added to dataSourceMap.
|
|
|
|
|
* @param dataSourceDef
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
_createDataSourceInstance(dataSourceDef: DataSourceDefinition) {
|
|
|
|
|
const module = this.getModuleEntry(dataSourceDef.namespace);
|
|
|
|
|
|
|
|
|
|
if (!module) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.dataSourceDefs[dataSourceDef.sourceName] = dataSourceDef;
|
|
|
|
|
|
|
|
|
|
const dataSourceInstance = module.createDataSource(
|
|
|
|
|
dataSourceDef.configuration,
|
2023-12-13 20:21:46 +01:00
|
|
|
this._servicesManager,
|
2023-11-24 14:46:54 +01:00
|
|
|
this
|
2023-07-20 17:43:05 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.dataSourceMap[dataSourceDef.sourceName] = [dataSourceInstance];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_initDataSourcesModule(
|
|
|
|
|
extensionModule,
|
|
|
|
|
extensionId,
|
|
|
|
|
dataSources: Array<DataSourceDefinition> = []
|
|
|
|
|
): void {
|
|
|
|
|
extensionModule.forEach(element => {
|
2023-09-01 22:19:39 +02:00
|
|
|
this.modulesMap[`${extensionId}.${MODULE_TYPES.DATA_SOURCE}.${element.name}`] = element;
|
2022-12-29 16:13:06 +01:00
|
|
|
});
|
2021-06-23 20:41:27 +02:00
|
|
|
|
2020-05-13 12:06:13 +02:00
|
|
|
extensionModule.forEach(element => {
|
|
|
|
|
const namespace = `${extensionId}.${MODULE_TYPES.DATA_SOURCE}.${element.name}`;
|
|
|
|
|
|
|
|
|
|
dataSources.forEach(dataSource => {
|
|
|
|
|
if (dataSource.namespace === namespace) {
|
2023-07-20 17:43:05 +02:00
|
|
|
this.addDataSource(dataSource);
|
2019-08-13 21:36:46 +02:00
|
|
|
}
|
2020-05-13 12:06:13 +02:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
2019-08-13 21:36:46 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @private
|
|
|
|
|
* @param {Object[]} commandDefinitions
|
|
|
|
|
*/
|
2020-05-13 12:06:13 +02:00
|
|
|
_initCommandsModule = extensionModule => {
|
|
|
|
|
let { definitions, defaultContext } = extensionModule;
|
|
|
|
|
if (!definitions || Object.keys(definitions).length === 0) {
|
|
|
|
|
log.warn('Commands Module contains no command definitions');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defaultContext = defaultContext || 'VIEWER';
|
|
|
|
|
|
2019-08-13 21:36:46 +02:00
|
|
|
if (!this._commandsManager.getContext(defaultContext)) {
|
|
|
|
|
this._commandsManager.createContext(defaultContext);
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-13 12:06:13 +02:00
|
|
|
Object.keys(definitions).forEach(commandName => {
|
|
|
|
|
const commandDefinition = definitions[commandName];
|
2019-08-13 21:36:46 +02:00
|
|
|
const commandHasContextThatDoesNotExist =
|
2023-09-01 22:19:39 +02:00
|
|
|
commandDefinition.context && !this._commandsManager.getContext(commandDefinition.context);
|
2019-08-13 21:36:46 +02:00
|
|
|
|
|
|
|
|
if (commandHasContextThatDoesNotExist) {
|
|
|
|
|
this._commandsManager.createContext(commandDefinition.context);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._commandsManager.registerCommand(
|
|
|
|
|
commandDefinition.context || defaultContext,
|
|
|
|
|
commandName,
|
|
|
|
|
commandDefinition
|
|
|
|
|
);
|
|
|
|
|
});
|
2020-05-09 20:23:43 +02:00
|
|
|
};
|
2024-07-05 19:19:42 +02:00
|
|
|
|
|
|
|
|
public get appConfig() {
|
|
|
|
|
return this._appConfig;
|
|
|
|
|
}
|
2019-08-13 21:36:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @private
|
|
|
|
|
* @param {string} lower
|
|
|
|
|
*/
|
|
|
|
|
function _capitalizeFirstCharacter(lower) {
|
2022-11-10 19:29:55 +01:00
|
|
|
return lower.charAt(0).toUpperCase() + lower.substring(1);
|
2019-08-13 21:36:46 +02:00
|
|
|
}
|