diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts index 238cd37ff..230a82943 100644 --- a/extensions/default/src/commandsModule.ts +++ b/extensions/default/src/commandsModule.ts @@ -23,6 +23,11 @@ export type HangingProtocolParams = { stageId?: string; }; +export type UpdateViewportDisplaySetParams = { + direction: number; + excludeNonImageModalities?: boolean; +}; + /** * Determine if a command is a hanging protocol one. * For now, just use the two hanging protocol commands that are in this @@ -541,6 +546,130 @@ const commandsModule = ({ overlays.item(i).classList.toggle('hidden'); } }, + + scrollActiveThumbnailIntoView: () => { + const { activeViewportIndex, viewports } = viewportGridService.getState(); + + if ( + !viewports || + activeViewportIndex < 0 || + activeViewportIndex > viewports.length - 1 + ) { + return; + } + + const activeViewport = viewports[activeViewportIndex]; + const activeDisplaySetInstanceUID = + activeViewport.displaySetInstanceUIDs[0]; + + const thumbnailList = document.querySelector('#ohif-thumbnail-list'); + + if (!thumbnailList) { + return; + } + + const thumbnailListBounds = thumbnailList.getBoundingClientRect(); + + const thumbnail = document.querySelector( + `#thumbnail-${activeDisplaySetInstanceUID}` + ); + + if (!thumbnail) { + return; + } + + const thumbnailBounds = thumbnail.getBoundingClientRect(); + + // This only handles a vertical thumbnail list. + if ( + thumbnailBounds.top >= thumbnailListBounds.top && + thumbnailBounds.top <= thumbnailListBounds.bottom + ) { + return; + } + + thumbnail.scrollIntoView({ behavior: 'smooth' }); + }, + + updateViewportDisplaySet: ({ + direction, + excludeNonImageModalities, + }: UpdateViewportDisplaySetParams) => { + const nonImageModalities = [ + 'SR', + 'SEG', + 'SM', + 'RTSTRUCT', + 'RTPLAN', + 'RTDOSE', + ]; + + // Sort the display sets as per the hanging protocol service viewport/display set scoring system. + // The thumbnail list uses the same sorting. + const dsSortFn = hangingProtocolService.getDisplaySetSortFunction(); + const currentDisplaySets = [...displaySetService.activeDisplaySets]; + + currentDisplaySets.sort(dsSortFn); + + const { activeViewportIndex, viewports } = viewportGridService.getState(); + + const { displaySetInstanceUIDs } = viewports[activeViewportIndex]; + + const activeDisplaySetIndex = currentDisplaySets.findIndex(displaySet => + displaySetInstanceUIDs.includes(displaySet.displaySetInstanceUID) + ); + + let displaySetIndexToShow: number; + + for ( + displaySetIndexToShow = activeDisplaySetIndex + direction; + displaySetIndexToShow > -1 && + displaySetIndexToShow < currentDisplaySets.length; + displaySetIndexToShow += direction + ) { + if ( + !excludeNonImageModalities || + !nonImageModalities.includes( + currentDisplaySets[displaySetIndexToShow].Modality + ) + ) { + break; + } + } + + if ( + displaySetIndexToShow < 0 || + displaySetIndexToShow >= currentDisplaySets.length + ) { + return; + } + + const { displaySetInstanceUID } = currentDisplaySets[ + displaySetIndexToShow + ]; + + let updatedViewports = []; + + try { + updatedViewports = hangingProtocolService.getViewportsRequireUpdate( + activeViewportIndex, + displaySetInstanceUID + ); + } 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, + }); + } + + viewportGridService.setDisplaySetsForViewports(updatedViewports); + + setTimeout(() => actions.scrollActiveThumbnailIntoView(), 0); + }, }; const definitions = { @@ -598,6 +727,11 @@ const commandsModule = ({ openDICOMTagViewer: { commandFn: actions.openDICOMTagViewer, }, + updateViewportDisplaySet: { + commandFn: actions.updateViewportDisplaySet, + storeContexts: [], + options: {}, + }, }; return { diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx index 9cc9ab2a7..99eaa6a5e 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { utils } from '@ohif/core'; +import { HangingProtocolService, utils } from '@ohif/core'; import { StudyBrowser, useImageViewer, @@ -292,7 +292,8 @@ function PanelStudyBrowserTracking({ const tabs = _createStudyBrowserTabs( StudyInstanceUIDs, studyDisplayList, - displaySets + displaySets, + hangingProtocolService ); // TODO: Should not fire this on "close" @@ -601,7 +602,8 @@ function _getComponentType(Modality) { function _createStudyBrowserTabs( primaryStudyInstanceUIDs, studyDisplayList, - displaySets + displaySets, + hangingProtocolService ) { const primaryStudies = []; const recentStudies = []; @@ -615,9 +617,8 @@ function _createStudyBrowserTabs( ); // Sort them - const sortedDisplaySetsForStudy = utils.sortBySeriesDate( - displaySetsForStudy - ); + const dsSortFn = hangingProtocolService.getDisplaySetSortFunction(); + displaySetsForStudy.sort(dsSortFn); /* Sort by series number, then by series date displaySetsForStudy.sort((a, b) => { diff --git a/platform/core/src/defaults/hotkeyBindings.js b/platform/core/src/defaults/hotkeyBindings.js index 75023247c..4c12e2179 100644 --- a/platform/core/src/defaults/hotkeyBindings.js +++ b/platform/core/src/defaults/hotkeyBindings.js @@ -76,18 +76,24 @@ const bindings = [ keys: ['left'], isEditable: true, }, - // { - // commandName: 'nextViewportDisplaySet', - // label: 'Next Series', - // keys: ['pageup'], - // isEditable: true, - // }, - // { - // commandName: 'previousViewportDisplaySet', - // label: 'Previous Series', - // keys: ['pagedown'], - // isEditable: true, - // }, + { + commandName: 'updateViewportDisplaySet', + commandOptions: { + direction: -1, + }, + label: 'Previous Series', + keys: ['pageup'], + isEditable: true, + }, + { + commandName: 'updateViewportDisplaySet', + commandOptions: { + direction: 1, + }, + label: 'Next Series', + keys: ['pagedown'], + isEditable: true, + }, { commandName: 'nextStage', context: 'DEFAULT', diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts index 3b6c540c2..4d3f72df0 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts @@ -1033,6 +1033,23 @@ export default class HangingProtocolService extends PubSubService { return this._matchViewport(useViewport, options); } + /** + * Gets a sort function that is consistent with the display set sorting performed + * to match display sets to viewports. + * @returns a display set sort function + */ + public getDisplaySetSortFunction(): ( + displaySetA: IDisplaySet, + displaySetB: IDisplaySet + ) => number { + return (displaySetA, displaySetB) => { + const seriesA = this._getSeriesSortInfoForDisplaySetSort(displaySetA); + const seriesB = this._getSeriesSortInfoForDisplaySetSort(displaySetB); + + return sortBy(this._getSeriesFieldForDisplaySetSort())(seriesA, seriesB); + }; + } + /** * Updates the viewports with the selected protocol stage. */ @@ -1461,7 +1478,7 @@ export default class HangingProtocolService extends PubSubService { sortingInfo: { score: totalMatchScore, study: study.StudyInstanceUID, - series: parseInt(displaySet.SeriesNumber), + ...this._getSeriesSortInfoForDisplaySetSort(displaySet), }, }; @@ -1484,9 +1501,7 @@ export default class HangingProtocolService extends PubSubService { name: 'study', reverse: true, }, - { - name: 'series', - } + this._getSeriesFieldForDisplaySetSort() ); matchingScores.sort((a, b) => sortingFunction(a.sortingInfo, b.sortingInfo) @@ -1506,6 +1521,19 @@ export default class HangingProtocolService extends PubSubService { }; } + private _getSeriesSortInfoForDisplaySetSort(displaySet) { + return { + [this._getSeriesFieldForDisplaySetSort().name]: + displaySet.SeriesNumber != null + ? parseInt(displaySet.SeriesNumber) + : parseInt(displaySet.seriesNumber), + }; + } + + private _getSeriesFieldForDisplaySetSort() { + return { name: 'series' }; + } + /** * Check if the next stage is available * @return {Boolean} True if next stage is available or false otherwise diff --git a/platform/ui/src/components/ThumbnailList/ThumbnailList.tsx b/platform/ui/src/components/ThumbnailList/ThumbnailList.tsx index df2047cad..b1cb06416 100644 --- a/platform/ui/src/components/ThumbnailList/ThumbnailList.tsx +++ b/platform/ui/src/components/ThumbnailList/ThumbnailList.tsx @@ -12,7 +12,10 @@ const ThumbnailList = ({ activeDisplaySetInstanceUIDs = [], }) => { return ( -