feat(command): Added command and hotkey binding for navigate to next and previous display set. (#3409)

* fix(command): Added command and hotkey binding for navigate to next and previous display set.

* Centralized the sorting (function) of display sets since at least three
areas sort display sets: hanging protocols, study/thumbnail browser and
updateViewportDisplaySet command.

* Renamed some methods for clarity.

* Scrolling active thumbnail into view for an updateViewportDisplaySet

* Added a null check.

* Added check to minimize thumbnail scrolling.
This commit is contained in:
Joe Boccanfuso 2023-05-23 15:03:55 -04:00 committed by GitHub
parent 4a17177d81
commit 685520acab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 195 additions and 23 deletions

View File

@ -23,6 +23,11 @@ export type HangingProtocolParams = {
stageId?: string; stageId?: string;
}; };
export type UpdateViewportDisplaySetParams = {
direction: number;
excludeNonImageModalities?: boolean;
};
/** /**
* Determine if a command is a hanging protocol one. * Determine if a command is a hanging protocol one.
* For now, just use the two hanging protocol commands that are in this * 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'); 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 = { const definitions = {
@ -598,6 +727,11 @@ const commandsModule = ({
openDICOMTagViewer: { openDICOMTagViewer: {
commandFn: actions.openDICOMTagViewer, commandFn: actions.openDICOMTagViewer,
}, },
updateViewportDisplaySet: {
commandFn: actions.updateViewportDisplaySet,
storeContexts: [],
options: {},
},
}; };
return { return {

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { utils } from '@ohif/core'; import { HangingProtocolService, utils } from '@ohif/core';
import { import {
StudyBrowser, StudyBrowser,
useImageViewer, useImageViewer,
@ -292,7 +292,8 @@ function PanelStudyBrowserTracking({
const tabs = _createStudyBrowserTabs( const tabs = _createStudyBrowserTabs(
StudyInstanceUIDs, StudyInstanceUIDs,
studyDisplayList, studyDisplayList,
displaySets displaySets,
hangingProtocolService
); );
// TODO: Should not fire this on "close" // TODO: Should not fire this on "close"
@ -601,7 +602,8 @@ function _getComponentType(Modality) {
function _createStudyBrowserTabs( function _createStudyBrowserTabs(
primaryStudyInstanceUIDs, primaryStudyInstanceUIDs,
studyDisplayList, studyDisplayList,
displaySets displaySets,
hangingProtocolService
) { ) {
const primaryStudies = []; const primaryStudies = [];
const recentStudies = []; const recentStudies = [];
@ -615,9 +617,8 @@ function _createStudyBrowserTabs(
); );
// Sort them // Sort them
const sortedDisplaySetsForStudy = utils.sortBySeriesDate( const dsSortFn = hangingProtocolService.getDisplaySetSortFunction();
displaySetsForStudy displaySetsForStudy.sort(dsSortFn);
);
/* Sort by series number, then by series date /* Sort by series number, then by series date
displaySetsForStudy.sort((a, b) => { displaySetsForStudy.sort((a, b) => {

View File

@ -76,18 +76,24 @@ const bindings = [
keys: ['left'], keys: ['left'],
isEditable: true, isEditable: true,
}, },
// { {
// commandName: 'nextViewportDisplaySet', commandName: 'updateViewportDisplaySet',
// label: 'Next Series', commandOptions: {
// keys: ['pageup'], direction: -1,
// isEditable: true, },
// }, label: 'Previous Series',
// { keys: ['pageup'],
// commandName: 'previousViewportDisplaySet', isEditable: true,
// label: 'Previous Series', },
// keys: ['pagedown'], {
// isEditable: true, commandName: 'updateViewportDisplaySet',
// }, commandOptions: {
direction: 1,
},
label: 'Next Series',
keys: ['pagedown'],
isEditable: true,
},
{ {
commandName: 'nextStage', commandName: 'nextStage',
context: 'DEFAULT', context: 'DEFAULT',

View File

@ -1033,6 +1033,23 @@ export default class HangingProtocolService extends PubSubService {
return this._matchViewport(useViewport, options); 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. * Updates the viewports with the selected protocol stage.
*/ */
@ -1461,7 +1478,7 @@ export default class HangingProtocolService extends PubSubService {
sortingInfo: { sortingInfo: {
score: totalMatchScore, score: totalMatchScore,
study: study.StudyInstanceUID, study: study.StudyInstanceUID,
series: parseInt(displaySet.SeriesNumber), ...this._getSeriesSortInfoForDisplaySetSort(displaySet),
}, },
}; };
@ -1484,9 +1501,7 @@ export default class HangingProtocolService extends PubSubService {
name: 'study', name: 'study',
reverse: true, reverse: true,
}, },
{ this._getSeriesFieldForDisplaySetSort()
name: 'series',
}
); );
matchingScores.sort((a, b) => matchingScores.sort((a, b) =>
sortingFunction(a.sortingInfo, b.sortingInfo) 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 * Check if the next stage is available
* @return {Boolean} True if next stage is available or false otherwise * @return {Boolean} True if next stage is available or false otherwise

View File

@ -12,7 +12,10 @@ const ThumbnailList = ({
activeDisplaySetInstanceUIDs = [], activeDisplaySetInstanceUIDs = [],
}) => { }) => {
return ( return (
<div className="py-3 bg-black overflow-y-hidden ohif-scrollbar study-min-height"> <div
id="ohif-thumbnail-list"
className="py-3 bg-black overflow-y-hidden ohif-scrollbar study-min-height"
>
{thumbnails.map( {thumbnails.map(
({ ({
displaySetInstanceUID, displaySetInstanceUID,