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;
};
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 {

View File

@ -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) => {

View File

@ -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',

View File

@ -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

View File

@ -12,7 +12,10 @@ const ThumbnailList = ({
activeDisplaySetInstanceUIDs = [],
}) => {
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(
({
displaySetInstanceUID,