import { Types, DicomMetadataStore, utils } from '@ohif/core'; import { datasetToDicomBlob, setNonEnumerableInstanceProperty } from './utils/dicomWriter'; import { registerNaturalizedDatasetsForLocalWadouri } from './utils/registerNaturalizedDatasetForLocalWadouri'; const { downloadBlob } = utils; import { ContextMenuController } from './CustomizableContextMenu'; import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser'; import reuseCachedLayouts from './utils/reuseCachedLayouts'; import { configureViewportForLayerAddition, configureViewportForLayerRemoval, canAddDisplaySetToViewport, DERIVED_OVERLAY_MODALITIES, } from './utils/layerConfigurationUtils'; import findViewportsByPosition, { findOrCreateViewport as layoutFindOrCreate, } from './findViewportsByPosition'; import { ContextMenuProps } from './CustomizableContextMenu/types'; import { NavigateHistory } from './types/commandModuleTypes'; import { history } from '@ohif/app'; import { useViewportGridStore } from './stores/useViewportGridStore'; import { useDisplaySetSelectorStore } from './stores/useDisplaySetSelectorStore'; import { useHangingProtocolStageIndexStore } from './stores/useHangingProtocolStageIndexStore'; import { useToggleHangingProtocolStore } from './stores/useToggleHangingProtocolStore'; import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore'; import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore'; import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy'; import promptSaveReport from './utils/promptSaveReport'; export type HangingProtocolParams = { protocolId?: string; stageIndex?: number; activeStudyUID?: string; stageId?: string; reset?: false; }; export type UpdateViewportDisplaySetParams = { direction: number; excludeNonImageModalities?: boolean; }; const commandsModule = ({ servicesManager, commandsManager, extensionManager, }: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => { const { customizationService, measurementService, hangingProtocolService, uiNotificationService, viewportGridService, displaySetService, multiMonitorService, } = servicesManager.services; // Define a context menu controller for use with any context menus const contextMenuController = new ContextMenuController(servicesManager, commandsManager); const actions = { /** * Adds a display set as a layer to the specified viewport * * @param options.viewportId - The ID of the viewport to add the layer to * @param options.displaySetInstanceUID - The UID of the display set to add as a layer * @param options.removeFirst - Optional flag to remove the display set first if it's already added */ addDisplaySetAsLayer: ({ viewportId, displaySetInstanceUID, removeFirst = false }) => { if (!viewportId) { const { activeViewportId } = servicesManager.services.viewportGridService.getState(); viewportId = activeViewportId; } if (!viewportId || !displaySetInstanceUID) { console.warn('Missing required parameters for addDisplaySetAsLayer command'); return; } const { displaySetService, viewportGridService, hangingProtocolService } = servicesManager.services; // Get the display set const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); if (!displaySet) { return; } // Get current display sets for the viewport const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); // Check if we can add this display set to the viewport const canAdd = canAddDisplaySetToViewport({ viewportId, displaySetInstanceUID, servicesManager, }); if (!canAdd) { return; } // Add the display set to the viewport const updatedViewports = hangingProtocolService.getViewportsRequireUpdate( viewportId, displaySetInstanceUID ); // Configure each viewport for layer addition updatedViewports.forEach(viewport => { configureViewportForLayerAddition({ viewport, displaySetInstanceUID, currentDisplaySetUIDs, servicesManager, }); }); // Update position presentation commandsManager.runCommand('updateStoredPositionPresentation', { viewportId, displaySetInstanceUIDs: updatedViewports[0].displaySetInstanceUIDs, }); // Run command to update viewports commandsManager.run('setDisplaySetsForViewports', { viewportsToUpdate: updatedViewports, }); }, /** * Removes a display set layer from the specified viewport * * @param options.viewportId - The ID of the viewport to remove the layer from * @param options.displaySetInstanceUID - The UID of the display set to remove */ removeDisplaySetLayer: ({ viewportId, displaySetInstanceUID }) => { if (!viewportId || !displaySetInstanceUID) { console.warn('Missing required parameters for removeDisplaySetLayer command'); return; } const { displaySetService, viewportGridService, hangingProtocolService, segmentationService, } = servicesManager.services; // Get the display set const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); if (!displaySet) { return; } // Check if it's a segmentation and handle accordingly. // Note that for the sake of hydrated segmentations, we remove the // segmentation before checking if the display set is indeed in the viewport. // This is because hydrated segmentations are not in the viewport per se // {i.e. they are not layered) but are simply referenced by the display // set in the viewport. const isSegmentation = DERIVED_OVERLAY_MODALITIES.includes(displaySet.Modality); if (isSegmentation) { segmentationService.removeRepresentationsFromViewport(viewportId, { segmentationId: displaySetInstanceUID, }); } // Get current display sets for the viewport const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); // If the display set is not in the viewport, no need to remove it if (!currentDisplaySetUIDs.includes(displaySetInstanceUID)) { return; } const updatedViewports = hangingProtocolService.getViewportsRequireUpdate( viewportId, displaySetInstanceUID ); // Configure each viewport for layer removal updatedViewports.forEach(viewport => { configureViewportForLayerRemoval({ viewport, displaySetInstanceUID, currentDisplaySetUIDs, servicesManager, }); }); // Update position presentation commandsManager.runCommand('updateStoredPositionPresentation', { viewportId, displaySetInstanceUIDs: updatedViewports[0].displaySetInstanceUIDs, }); // Update the viewports commandsManager.run('setDisplaySetsForViewports', { viewportsToUpdate: updatedViewports, }); }, /** * Runs a command in multi-monitor mode. No-op if not multi-monitor. */ multimonitor: async options => { const { screenDelta, StudyInstanceUID, commands, hashParams } = options; if (multiMonitorService.numberOfScreens < 2) { return options.fallback?.(options); } const newWindow = await multiMonitorService.launchWindow( StudyInstanceUID, screenDelta, hashParams ); // Only run commands if we successfully got a window with a commands manager if (newWindow && commands) { // Todo: fix this properly, but it takes time for the new window to load // and then the commandsManager is available for it setTimeout(() => { multiMonitorService.run(screenDelta, commands, options); }, 1000); } }, /** Displays a prompt and then save the report if relevant */ promptSaveReport: props => { const { StudyInstanceUID } = props; promptSaveReport({ servicesManager, commandsManager, extensionManager }, props, { data: { StudyInstanceUID }, }); }, /** * Ensures that the specified study is available for display * Then, if commands is specified, runs the given commands list/instance */ loadStudy: async options => { const { StudyInstanceUID } = options; const displaySets = displaySetService.getActiveDisplaySets(); const isActive = displaySets.find(ds => ds.StudyInstanceUID === StudyInstanceUID); if (isActive) { return; } const [dataSource] = extensionManager.getActiveDataSource(); await requestDisplaySetCreationForStudy(dataSource, displaySetService, StudyInstanceUID); const study = DicomMetadataStore.getStudy(StudyInstanceUID); hangingProtocolService.addStudy(study); }, /** * Show the context menu. * @param options.menuId defines the menu name to lookup, from customizationService * @param options.defaultMenu contains the default menu set to use * @param options.element is the element to show the menu within * @param options.event is the event that caused the context menu * @param options.selectorProps is the set of selection properties to use */ showContextMenu: (options: ContextMenuProps) => { const { menuCustomizationId, element, event, selectorProps, defaultPointsPosition = [], } = options; const optionsToUse = { ...options }; if (menuCustomizationId) { Object.assign(optionsToUse, customizationService.getCustomization(menuCustomizationId)); } // TODO - make the selectorProps richer by including the study metadata and display set. const { protocol, stage } = hangingProtocolService.getActiveProtocol(); optionsToUse.selectorProps = { event, protocol, stage, ...selectorProps, }; contextMenuController.showContextMenu(optionsToUse, element, defaultPointsPosition); }, /** Close a context menu currently displayed */ closeContextMenu: () => { contextMenuController.closeContextMenu(); }, displayNotification: ({ text, title, type }) => { uiNotificationService.show({ title: title, message: text, type: type, }); }, clearMeasurements: options => { measurementService.clearMeasurements(options.measurementFilter); }, /** * Sets the specified protocol * 1. Records any existing state using the viewport grid service * 2. Finds the destination state - this can be one of: * a. The specified protocol stage * b. An alternate (toggled or restored) protocol stage * c. A restored custom layout * 3. Finds the parameters for the specified state * a. Gets the displaySetSelectorMap * b. Gets the map by position * c. Gets any toggle mapping to map position to/from current view * 4. If restore, then sets layout * a. Maps viewport position by currently displayed viewport map id * b. Uses toggle information to map display set id * 5. Else applies the hanging protocol * a. HP Service is provided displaySetSelectorMap * b. HP Service will throw an exception if it isn't applicable * @param options - contains information on the HP to apply * @param options.activeStudyUID - the updated study to apply the HP to * @param options.protocolId - the protocol ID to change to * @param options.stageId - the stageId to apply * @param options.stageIndex - the index of the stage to go to. * @param options.reset - flag to indicate if the HP should be reset to its original and not restored to a previous state * * commandsManager.run('setHangingProtocol', { * activeStudyUID: '1.2.3', * protocolId: 'myProtocol', * stageId: 'myStage', * stageIndex: 0, * reset: false, * }); */ setHangingProtocol: ({ activeStudyUID = '', StudyInstanceUID = '', protocolId, stageId, stageIndex, reset = false, }: HangingProtocolParams): boolean => { const toUseStudyInstanceUID = activeStudyUID || StudyInstanceUID; try { // Stores in the state the display set selector id to displaySetUID mapping // Pass in viewportId for the active viewport. This item will get set as // the activeViewportId const state = viewportGridService.getState(); const hpInfo = hangingProtocolService.getState(); reuseCachedLayouts(state, hangingProtocolService); const { hangingProtocolStageIndexMap } = useHangingProtocolStageIndexStore.getState(); const { displaySetSelectorMap } = useDisplaySetSelectorStore.getState(); if (!protocolId) { // Reuse the previous protocol id, and optionally stage protocolId = hpInfo.protocolId; if (stageId === undefined && stageIndex === undefined) { stageIndex = hpInfo.stageIndex; } } else if (stageIndex === undefined && stageId === undefined) { // Re-set the same stage as was previously used const hangingId = `${toUseStudyInstanceUID || hpInfo.activeStudyUID}:${protocolId}`; stageIndex = hangingProtocolStageIndexMap[hangingId]?.stageIndex; } const useStageIdx = stageIndex ?? hangingProtocolService.getStageIndex(protocolId, { stageId, stageIndex, }); const activeStudyChanged = hangingProtocolService.setActiveStudyUID(toUseStudyInstanceUID); const storedHanging = `${toUseStudyInstanceUID || hangingProtocolService.getState().activeStudyUID}:${protocolId}:${ useStageIdx || 0 }`; const { viewportGridState } = useViewportGridStore.getState(); const restoreProtocol = !reset && viewportGridState[storedHanging]; if ( reset || (activeStudyChanged && !viewportGridState[storedHanging] && stageIndex === undefined && stageId === undefined) ) { // Run the hanging protocol fresh, re-using the existing study data // This is done on reset or when the study changes and we haven't yet // applied it, and don't specify exact stage to use. const displaySets = displaySetService.getActiveDisplaySets(); const activeStudy = { StudyInstanceUID: toUseStudyInstanceUID, displaySets, }; hangingProtocolService.run(activeStudy, protocolId); } else if ( protocolId === hpInfo.protocolId && useStageIdx === hpInfo.stageIndex && !toUseStudyInstanceUID ) { // Clear the HP setting to reset them hangingProtocolService.setProtocol(protocolId, { stageId, stageIndex: useStageIdx, displaySetSelectorMap, }); } else { hangingProtocolService.setProtocol(protocolId, { displaySetSelectorMap, stageId, stageIndex: useStageIdx, restoreProtocol, }); if (restoreProtocol) { viewportGridService.set(viewportGridState[storedHanging]); } } // Do this after successfully applying the update const { setDisplaySetSelector } = useDisplaySetSelectorStore.getState(); setDisplaySetSelector( `${toUseStudyInstanceUID || hpInfo.activeStudyUID}:activeDisplaySet:0`, null ); return true; } catch (e) { console.error(e); uiNotificationService.show({ title: 'Apply Hanging Protocol', message: 'The hanging protocol could not be applied.', type: 'error', duration: 3000, }); return false; } }, toggleHangingProtocol: ({ protocolId, stageIndex }: HangingProtocolParams): boolean => { const { protocol, stageIndex: desiredStageIndex, activeStudy, } = hangingProtocolService.getActiveProtocol(); const { toggleHangingProtocol, setToggleHangingProtocol } = useToggleHangingProtocolStore.getState(); const storedHanging = `${activeStudy.StudyInstanceUID}:${protocolId}:${stageIndex | 0}`; if ( protocol.id === protocolId && (stageIndex === undefined || stageIndex === desiredStageIndex) ) { // Toggling off - restore to previous state const previousState = toggleHangingProtocol[storedHanging] || { protocolId: 'default', }; return actions.setHangingProtocol(previousState); } else { setToggleHangingProtocol(storedHanging, { protocolId: protocol.id, stageIndex: desiredStageIndex, }); return actions.setHangingProtocol({ protocolId, stageIndex, reset: true, }); } }, deltaStage: ({ direction }) => { const { protocolId, stageIndex: oldStageIndex } = hangingProtocolService.getState(); const { protocol } = hangingProtocolService.getActiveProtocol(); for ( let stageIndex = oldStageIndex + direction; stageIndex >= 0 && stageIndex < protocol.stages.length; stageIndex += direction ) { if (protocol.stages[stageIndex].status !== 'disabled') { return actions.setHangingProtocol({ protocolId, stageIndex, }); } } uiNotificationService.show({ title: 'Change Stage', message: 'The hanging protocol has no more applicable stages', type: 'info', duration: 3000, }); }, /** * Changes the viewport grid layout in terms of the MxN layout. */ setViewportGridLayout: ({ numRows, numCols, isHangingProtocolLayout = false }) => { const { protocol } = hangingProtocolService.getActiveProtocol(); const onLayoutChange = protocol.callbacks?.onLayoutChange; if (commandsManager.run(onLayoutChange, { numRows, numCols }) === false) { // Don't apply the layout if the run command returns false return; } const completeLayout = () => { const state = viewportGridService.getState(); findViewportsByPosition(state, { numRows, numCols }); const { viewportsByPosition, initialInDisplay } = useViewportsByPositionStore.getState(); const findOrCreateViewport = layoutFindOrCreate.bind( null, hangingProtocolService, isHangingProtocolLayout, { ...viewportsByPosition, initialInDisplay } ); viewportGridService.setLayout({ numRows, numCols, findOrCreateViewport, isHangingProtocolLayout, }); }; // Need to finish any work in the callback window.setTimeout(completeLayout, 0); }, toggleOneUp() { const viewportGridState = viewportGridService.getState(); const { activeViewportId, viewports, layout, isHangingProtocolLayout } = viewportGridState; const { displaySetInstanceUIDs, displaySetOptions, viewportOptions } = viewports.get(activeViewportId); if (layout.numCols === 1 && layout.numRows === 1) { // The viewer is in one-up. Check if there is a state to restore/toggle back to. const { toggleOneUpViewportGridStore } = useToggleOneUpViewportGridStore.getState(); if (!toggleOneUpViewportGridStore) { return; } // There is a state to toggle back to. The viewport that was // originally toggled to one up was the former active viewport. const viewportIdToUpdate = toggleOneUpViewportGridStore.activeViewportId; // We are restoring the previous layout but taking into the account that // the current one up viewport might have a new displaySet dragged and dropped on it. // updatedViewportsViaHP below contains the viewports applicable to the HP that existed // prior to the toggle to one-up - including the updated viewports if a display // set swap were to have occurred. const updatedViewportsViaHP = displaySetInstanceUIDs.length > 1 ? [] : displaySetInstanceUIDs .map(displaySetInstanceUID => hangingProtocolService.getViewportsRequireUpdate( viewportIdToUpdate, displaySetInstanceUID, isHangingProtocolLayout ) ) .flat(); // findOrCreateViewport returns either one of the updatedViewportsViaHP // returned from the HP service OR if there is not one from the HP service then // simply returns what was in the previous state for a given position in the layout. const findOrCreateViewport = (position: number, positionId: string) => { // Find the viewport for the given position prior to the toggle to one-up. const preOneUpViewport = Array.from(toggleOneUpViewportGridStore.viewports.values()).find( viewport => viewport.positionId === positionId ); // Use the viewport id from before the toggle to one-up to find any updates to the viewport. const viewport = updatedViewportsViaHP.find( viewport => viewport.viewportId === preOneUpViewport.viewportId ); return viewport ? // Use the applicable viewport from the HP updated viewports { viewportOptions, displaySetOptions, ...viewport } : // Use the previous viewport for the given position preOneUpViewport; }; const layoutOptions = viewportGridService.getLayoutOptionsFromState( toggleOneUpViewportGridStore ); // Restore the previous layout including the active viewport. viewportGridService.setLayout({ numRows: toggleOneUpViewportGridStore.layout.numRows, numCols: toggleOneUpViewportGridStore.layout.numCols, activeViewportId: viewportIdToUpdate, layoutOptions, findOrCreateViewport, isHangingProtocolLayout: true, }); // Reset crosshairs after restoring the layout setTimeout(() => { commandsManager.runCommand('resetCrosshairs'); }, 0); } else { // We are not in one-up, so toggle to one up. // Store the current viewport grid state so we can toggle it back later. const { setToggleOneUpViewportGridStore } = useToggleOneUpViewportGridStore.getState(); setToggleOneUpViewportGridStore(viewportGridState); // one being toggled to one up. const findOrCreateViewport = () => { return { displaySetInstanceUIDs, displaySetOptions, viewportOptions, }; }; // Set the layout to be 1x1/one-up. viewportGridService.setLayout({ numRows: 1, numCols: 1, findOrCreateViewport, isHangingProtocolLayout: true, }); } }, /** * Exposes the browser history navigation used by OHIF. This command can be used to either replace or * push a new entry into the browser history. For example, the following will replace the current * browser history entry with the specified relative URL which changes the study displayed to the * study with study instance UID 1.2.3. Note that as a result of using `options.replace = true`, the * page prior to invoking this command cannot be returned to via the browser back button. * * navigateHistory({ * to: 'viewer?StudyInstanceUIDs=1.2.3', * options: { replace: true }, * }); * * @param historyArgs - arguments for the history function; * the `to` property is the URL; * the `options.replace` is a boolean indicating if the current browser history entry * should be replaced or a new entry pushed onto the history (stack); the default value * for `replace` is false */ navigateHistory(historyArgs: NavigateHistory) { history.navigate(historyArgs.to, historyArgs.options); }, openDICOMTagViewer({ displaySetInstanceUID }: { displaySetInstanceUID?: string }) { const { activeViewportId, viewports } = viewportGridService.getState(); const activeViewportSpecificData = viewports.get(activeViewportId); const { displaySetInstanceUIDs } = activeViewportSpecificData; const displaySets = displaySetService.activeDisplaySets; const { UIModalService } = servicesManager.services; const defaultDisplaySetInstanceUID = displaySetInstanceUID || displaySetInstanceUIDs[0] || displaySets[0]?.displaySetInstanceUID; UIModalService.show({ content: DicomTagBrowser, contentProps: { displaySets, displaySetInstanceUID: defaultDisplaySetInstanceUID, }, title: 'DICOM Tag Browser', containerClassName: 'max-w-3xl', }); }, /** * Toggle viewport overlay (the information panel shown on the four corners * of the viewport) * @see ViewportOverlay and CustomizableViewportOverlay components */ toggleOverlays: () => { const overlays = document.getElementsByClassName('viewport-overlay'); for (let i = 0; i < overlays.length; i++) { overlays.item(i).classList.toggle('hidden'); } }, scrollActiveThumbnailIntoView: () => { const { activeViewportId, viewports } = viewportGridService.getState(); const activeViewport = viewports.get(activeViewportId); const activeDisplaySetInstanceUID = activeViewport?.displaySetInstanceUIDs?.[0]; if (!activeDisplaySetInstanceUID) { return; } const thumbnailList = document.querySelector('#ohif-thumbnail-list'); if (!thumbnailList) { return; } const thumbnail = document.querySelector(`#thumbnail-${activeDisplaySetInstanceUID}`); if (!thumbnail) { return; } thumbnail.scrollIntoView({ behavior: 'smooth' }); }, updateViewportDisplaySet: ({ direction, excludeNonImageModalities, }: UpdateViewportDisplaySetParams) => { const nonImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE']; const currentDisplaySets = [...displaySetService.activeDisplaySets]; const { activeViewportId, viewports, isHangingProtocolLayout } = viewportGridService.getState(); const { displaySetInstanceUIDs } = viewports.get(activeViewportId); const activeDisplaySetIndex = currentDisplaySets.findIndex(displaySet => displaySetInstanceUIDs.includes(displaySet.displaySetInstanceUID) ); let displaySetIndexToShow: number; for ( displaySetIndexToShow = activeDisplaySetIndex + direction; displaySetIndexToShow > -1 && displaySetIndexToShow < currentDisplaySets.length; displaySetIndexToShow += direction ) { const nextDisplaySet = currentDisplaySets[displaySetIndexToShow]; if (nextDisplaySet.madeInClient) { continue; } if (!excludeNonImageModalities || !nonImageModalities.includes(nextDisplaySet.Modality)) { break; } } if (displaySetIndexToShow < 0 || displaySetIndexToShow >= currentDisplaySets.length) { return; } const { displaySetInstanceUID } = currentDisplaySets[displaySetIndexToShow]; let updatedViewports = []; try { updatedViewports = hangingProtocolService.getViewportsRequireUpdate( activeViewportId, displaySetInstanceUID, isHangingProtocolLayout ); } catch (error) { console.warn(error); uiNotificationService.show({ title: 'Navigate Viewport Display Set', message: 'The requested display sets could not be added to the viewport due to a mismatch in the Hanging Protocol rules.', type: 'info', duration: 3000, }); } commandsManager.run('setDisplaySetsForViewports', { viewportsToUpdate: updatedViewports }); setTimeout(() => actions.scrollActiveThumbnailIntoView(), 0); }, /** * Creates a store function based on the data source type. * @param dataSource - 'download', 'copyToClipboard', or a named data source * @param defaultFileName - Default filename for download/clipboard * @param defaultContentType - Default content type for clipboard * @returns A store function, or null if no valid store exists */ createStoreFunction: ({ dataSource, defaultFileName, defaultContentType }) => { if (dataSource === 'download') { return async dicom => { const instances = Array.isArray(dicom) ? dicom : [dicom]; registerNaturalizedDatasetsForLocalWadouri(instances); DicomMetadataStore.addInstances(instances, true); if (instances.length !== 1) { throw new Error('Download only supports a single DICOM instance'); } const reportBlob = datasetToDicomBlob(instances[0]); downloadBlob(reportBlob, { filename: defaultFileName || 'dicom.dcm' }); }; } if (dataSource === 'copyToClipboard') { return async dicom => { const instances = Array.isArray(dicom) ? dicom : [dicom]; registerNaturalizedDatasetsForLocalWadouri(instances); DicomMetadataStore.addInstances(instances, true); if (instances.length !== 1) { throw new Error('Copy to clipboard only supports a single DICOM instance'); } const reportBlob = datasetToDicomBlob(instances[0]); const type = defaultContentType || 'application/dicom'; await navigator.clipboard.write([ new ClipboardItem({ [type]: reportBlob }), ]); }; } // DICOM STOW path — resolve the named data source const dataSources = extensionManager.getDataSources(dataSource); const resolvedDataSource = dataSources?.[0]; if (!resolvedDataSource?.store?.dicom) { return null; } return async (dicom, { dicomDict } = {}) => { const instances = Array.isArray(dicom) ? dicom : [dicom]; // Always keep an in-memory wadouri copy so DICOM can be read without re-fetching. registerNaturalizedDatasetsForLocalWadouri(instances); if (dataSource !== 'dicomlocal') { const config = resolvedDataSource.getConfig?.(); if (config?.wadoRoot) { instances.forEach(instance => { setNonEnumerableInstanceProperty(instance, 'wadoRoot', config.wadoRoot); }); } } DicomMetadataStore.addInstances(instances, true); for (const instance of instances) { await resolvedDataSource.store.dicom(instance, null, dicomDict); } const studyUIDs = new Set(instances.map(i => i.StudyInstanceUID).filter(Boolean)); for (const uid of studyUIDs) { resolvedDataSource.deleteStudyMetadataPromise(uid); } }; }, /** * Launches a workflow (mode) for a study from the worklist. This is the * default `workList.onStudyDoubleClick` command. * * @param study - the StudyRow the action applies to * @param workflows - the workflows applicable to the study, in menu order; * each has `id`, `displayName`, `isDefault` and `launchWithStudy(study)` * @param defaultWorkflow - the user's default workflow when it applies to * the study * @param workflowId - command option to force a specific workflow (mode id) * instead of the default/first applicable one */ launchDefaultMode: ({ study, workflows = [], defaultWorkflow, workflowId }) => { const workflow = workflowId ? workflows.find(w => w.id === workflowId) : (defaultWorkflow ?? workflows[0]); if (!workflow) { console.warn( workflowId ? `launchDefaultMode: workflow '${workflowId}' is not applicable to the study` : 'launchDefaultMode: no workflow is applicable to the study' ); return; } workflow.launchWithStudy(study); }, }; const definitions = { multimonitor: actions.multimonitor, promptSaveReport: actions.promptSaveReport, loadStudy: actions.loadStudy, showContextMenu: actions.showContextMenu, closeContextMenu: actions.closeContextMenu, clearMeasurements: actions.clearMeasurements, displayNotification: actions.displayNotification, setHangingProtocol: actions.setHangingProtocol, toggleHangingProtocol: actions.toggleHangingProtocol, navigateHistory: actions.navigateHistory, nextStage: { commandFn: actions.deltaStage, options: { direction: 1 }, }, previousStage: { commandFn: actions.deltaStage, options: { direction: -1 }, }, setViewportGridLayout: actions.setViewportGridLayout, toggleOneUp: actions.toggleOneUp, openDICOMTagViewer: actions.openDICOMTagViewer, updateViewportDisplaySet: actions.updateViewportDisplaySet, scrollActiveThumbnailIntoView: actions.scrollActiveThumbnailIntoView, addDisplaySetAsLayer: actions.addDisplaySetAsLayer, removeDisplaySetLayer: actions.removeDisplaySetLayer, createStoreFunction: actions.createStoreFunction, launchDefaultMode: { commandFn: actions.launchDefaultMode, context: 'WORKLIST', }, }; return { actions, definitions, defaultContext: 'DEFAULT', }; }; export default commandsModule;