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:
parent
4a17177d81
commit
685520acab
@ -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 {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user