feat(multimonitor): Add simple multi-monitor support to open another study(#4178)

This commit is contained in:
Bill Wallace authored and GitHub committed 2025-01-13 10:49:08 -05:00
1 parent a4a6de07f4
commit 07c628e689
49 files changed
+1476 -367

No files matched your search

@@ -0,0 +1,91 @@
import React from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
Icons,
Button,
} from '@ohif/ui-next';
/**
* The default sub-menu appearance and setup is defined here, but this can be
* replaced by
*/
const getMenuItemsDefault = ({ commandsManager, items, servicesManager, ...props }) => {
const { customizationService } = servicesManager.services;
// This allows replacing the default child item for menus, whereas the entire
// getMenuItems can also be replaced by providing it to the MoreDropdownMenu
const menuContent = customizationService.getCustomization('ohif.menuContent');
return (
<DropdownMenuContent
hideWhenDetached
align="start"
onClick={e => {
e.stopPropagation();
e.preventDefault();
}}
>
{items?.map(item =>
menuContent.content({
key: item.id,
item,
commandsManager,
servicesManager,
...props,
})
)}
</DropdownMenuContent>
);
};
/**
* The component provides a ... sub-menu for various components which appears
* on hover over the main component.
*
* @param bindProps - properties to define the sub-menu
* @returns Component bound to the bindProps
*/
export default function MoreDropdownMenu(bindProps) {
const {
menuItemsKey,
getMenuItems = getMenuItemsDefault,
commandsManager,
servicesManager,
} = bindProps;
const { customizationService } = servicesManager.services;
const items = customizationService.getCustomization(menuItemsKey)?.value;
if (!items) {
return null;
}
function BoundMoreDropdownMenu(props) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="hidden group-hover:inline-flex data-[state=open]:inline-flex"
onClick={e => {
e.preventDefault();
e.stopPropagation();
}}
>
<Icons.More />
</Button>
</DropdownMenuTrigger>
{getMenuItems({
...props,
commandsManager: commandsManager,
servicesManager: servicesManager,
items,
})}
</DropdownMenu>
);
}
return BoundMoreDropdownMenu;
}
@@ -47,15 +47,22 @@ export default class ContextMenuController {
}
const { event, subMenu, menuId, menus, selectorProps } = contextMenuProps;
if (!menus) {
console.warn('No menus found for', menuId);
return;
}
const { locking, visibility } = CsAnnotation;
const targetAnnotationId = selectorProps?.nearbyToolData?.annotationUID as string;
const isLocked = locking.isAnnotationLocked(targetAnnotationId);
const isVisible = visibility.isAnnotationVisible(targetAnnotationId);
if (isLocked || !isVisible) {
console.warn(`Annotation is ${isLocked ? 'locked' : 'not visible'}.`);
return;
if (targetAnnotationId) {
const isLocked = locking.isAnnotationLocked(targetAnnotationId);
const isVisible = visibility.isAnnotationVisible(targetAnnotationId);
if (isLocked || !isVisible) {
console.warn(`Annotation is ${isLocked ? 'locked' : 'not visible'}.`);
return;
}
}
const items = ContextMenuItemsBuilder.getMenuItems(
@@ -73,7 +80,7 @@ export default class ContextMenuController {
preventCutOf: true,
defaultPosition: ContextMenuController._getDefaultPosition(
defaultPointsPosition,
event?.detail,
event?.detail || event,
viewportElement
),
event,
@@ -89,7 +96,7 @@ export default class ContextMenuController {
menus,
event,
subMenu,
eventData: event?.detail,
eventData: event?.detail || event,
onClose: () => {
this.services.uiDialogService.dismiss({ id: 'context-menu' });
@@ -136,8 +143,8 @@ export default class ContextMenuController {
};
static _getEventDefaultPosition = eventDetail => ({
x: eventDetail && eventDetail.currentPoints.client[0],
y: eventDetail && eventDetail.currentPoints.client[1],
x: eventDetail?.currentPoints?.client[0] ?? eventDetail?.pageX,
y: eventDetail?.currentPoints?.client[1] ?? eventDetail?.pageY,
});
static _getElementDefaultPosition = element => {
@@ -7,6 +7,7 @@ import { useNavigate } from 'react-router-dom';
import { Separator } from '@ohif/ui-next';
import { PanelStudyBrowserHeader } from './PanelStudyBrowserHeader';
import { defaultActionIcons, defaultViewPresets } from './constants';
import MoreDropdownMenu from '../../Components/MoreDropdownMenu';
const { sortStudyInstances, formatDate, createStudyBrowserTabs } = utils;
@@ -280,10 +281,6 @@ function PanelStudyBrowser({
const activeDisplaySetInstanceUIDs = viewports.get(activeViewportId)?.displaySetInstanceUIDs;
const onThumbnailContextMenu = (commandName, options) => {
commandsManager.runCommand(commandName, options);
};
return (
<>
<>
@@ -304,16 +301,26 @@ function PanelStudyBrowser({
tabs={tabs}
servicesManager={servicesManager}
activeTabName={activeTabName}
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
onClickStudy={_handleStudyClick}
onClickTab={clickedTabName => {
setActiveTabName(clickedTabName);
}}
onClickThumbnail={() => {}}
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
showSettings={actionIcons.find(icon => icon.id === 'settings').value}
viewPresets={viewPresets}
onThumbnailContextMenu={onThumbnailContextMenu}
ThumbnailMenuItems={MoreDropdownMenu({
commandsManager,
servicesManager,
menuItemsKey: 'studyBrowser.thumbnailMenuItems',
})}
StudyMenuItems={MoreDropdownMenu({
commandsManager,
servicesManager,
menuItemsKey: 'studyBrowser.studyMenuItems',
})}
/>
</>
);
@@ -13,10 +13,10 @@ import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStu
* @param {object} commandsManager
* @param {object} extensionManager
*/
function WrappedPanelStudyBrowser({ commandsManager, extensionManager, servicesManager }) {
function WrappedPanelStudyBrowser({ extensionManager, servicesManager }) {
// TODO: This should be made available a different way; route should have
// already determined our datasource
const dataSource = extensionManager.getDataSources()[0];
const [dataSource] = extensionManager.getActiveDataSource();
const _getStudiesForPatientByMRN = getStudiesForPatientByMRN.bind(null, dataSource);
const _getImageSrcFromImageId = useCallback(
_createGetImageSrcFromImageIdFn(extensionManager),
@@ -13,7 +13,7 @@ function requestDisplaySetCreationForStudy(
return;
}
dataSource.retrieve.series.metadata({ StudyInstanceUID, madeInClient });
return dataSource.retrieve.series.metadata({ StudyInstanceUID, madeInClient });
}
export default requestDisplaySetCreationForStudy;
@@ -25,8 +25,6 @@ function ViewerHeader({
const onClickReturnButton = () => {
const { pathname } = location;
const dataSourceIdx = pathname.indexOf('/', 1);
const query = new URLSearchParams(window.location.search);
const configUrl = query.get('configUrl');
const dataSourceName = pathname.substring(dataSourceIdx + 1);
const existingDataSource = extensionManager.getDataSources(dataSourceName);
@@ -35,10 +33,7 @@ function ViewerHeader({
if (dataSourceIdx !== -1 && existingDataSource) {
searchQuery.append('datasources', pathname.substring(dataSourceIdx + 1));
}
if (configUrl) {
searchQuery.append('configUrl', configUrl);
}
preserveQueryParameters(searchQuery);
navigate({
pathname: publicUrl,
+72 -8
View File
@@ -1,4 +1,4 @@
import { Types } from '@ohif/core';
import { Types, DicomMetadataStore } from '@ohif/core';
import { ContextMenuController, defaultContextMenu } from './CustomizableContextMenu';
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
@@ -16,6 +16,7 @@ import { useHangingProtocolStageIndexStore } from './stores/useHangingProtocolSt
import { useToggleHangingProtocolStore } from './stores/useToggleHangingProtocolStore';
import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore';
import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore';
import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy';
export type HangingProtocolParams = {
protocolId?: string;
@@ -33,6 +34,7 @@ export type UpdateViewportDisplaySetParams = {
const commandsModule = ({
servicesManager,
commandsManager,
extensionManager,
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
const {
customizationService,
@@ -41,12 +43,56 @@ const commandsModule = ({
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 = {
/**
* 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);
}
},
/**
* 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
@@ -135,11 +181,13 @@ const commandsModule = ({
*/
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
@@ -158,7 +206,7 @@ const commandsModule = ({
}
} else if (stageIndex === undefined && stageId === undefined) {
// Re-set the same stage as was previously used
const hangingId = `${activeStudyUID || hpInfo.activeStudyUID}:${protocolId}`;
const hangingId = `${toUseStudyInstanceUID || hpInfo.activeStudyUID}:${protocolId}`;
stageIndex = hangingProtocolStageIndexMap[hangingId]?.stageIndex;
}
@@ -169,11 +217,9 @@ const commandsModule = ({
stageIndex,
});
if (activeStudyUID) {
hangingProtocolService.setActiveStudyUID(activeStudyUID);
}
const activeStudyChanged = hangingProtocolService.setActiveStudyUID(toUseStudyInstanceUID);
const storedHanging = `${hangingProtocolService.getState().activeStudyUID}:${protocolId}:${
const storedHanging = `${toUseStudyInstanceUID || hangingProtocolService.getState().activeStudyUID}:${protocolId}:${
useStageIdx || 0
}`;
@@ -181,9 +227,25 @@ const commandsModule = ({
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 &&
!activeStudyUID
!toUseStudyInstanceUID
) {
// Clear the HP setting to reset them
hangingProtocolService.setProtocol(protocolId, {
@@ -204,7 +266,7 @@ const commandsModule = ({
// Do this after successfully applying the update
const { setDisplaySetSelector } = useDisplaySetSelectorStore.getState();
setDisplaySetSelector(
`${activeStudyUID || hpInfo.activeStudyUID}:activeDisplaySet:0`,
`${toUseStudyInstanceUID || hpInfo.activeStudyUID}:activeDisplaySet:0`,
null
);
return true;
@@ -562,6 +624,8 @@ const commandsModule = ({
};
const definitions = {
multimonitor: actions.multimonitor,
loadStudy: actions.loadStudy,
showContextMenu: actions.showContextMenu,
closeContextMenu: actions.closeContextMenu,
clearMeasurements: actions.clearMeasurements,
@@ -0,0 +1,81 @@
export const studyBrowserContextMenu = {
id: 'StudyBrowser.studyContextMenu',
customizationType: 'ohif.contextMenu',
menus: [
{
id: 'studyBrowserContextMenu',
// selector restricts context menu to when there is nearbyToolData
items: [
{
label: 'Show in Grid',
commands: {
commandName: 'loadStudy',
commandOptions: {
commands: {
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGrid8',
},
},
},
},
},
{
label: 'Show in other monitor',
selector: ({ isMultimonitor }) => isMultimonitor,
commands: {
commandName: 'multimonitor',
commandOptions: {
commands: {
commandName: 'loadStudy',
commandOptions: {
commands: {
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGrid8',
},
},
},
},
},
},
},
{
label: 'Compare All',
selector: ({ isMultimonitor }) => isMultimonitor,
commands: [
{
commandName: 'loadStudy',
commandOptions: {
commands: {
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGrid',
},
},
},
},
{
commandName: 'multimonitor',
commandOptions: {
commands: {
commandName: 'loadStudy',
commandOptions: {
commands: {
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGridMonitor2',
},
},
},
},
},
},
],
},
],
},
],
};
export default studyBrowserContextMenu;
@@ -5,6 +5,15 @@ import { ProgressDropdownWithService } from './Components/ProgressDropdownWithSe
import DataSourceConfigurationComponent from './Components/DataSourceConfigurationComponent';
import { GoogleCloudDataSourceConfigurationAPI } from './DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI';
import { utils } from '@ohif/core';
import studyBrowserContextMenu from './customizations/studyBrowserContextMenu';
import {
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuPortal,
DropdownMenuSubContent,
DropdownMenuItem,
Icons,
} from '@ohif/ui-next';
const formatDate = utils.formatDate;
@@ -47,7 +56,74 @@ export default function getCustomizationModule({ servicesManager, extensionManag
],
},
},
{
name: 'multimonitor',
merge: 'Append',
value: {
id: 'studyBrowser.studyMenuItems',
customizationType: 'ohif.menuContent',
value: [
{
id: 'applyHangingProtocol',
label: 'Apply Hanging Protocol',
iconName: 'ViewportViews',
items: [
{
id: 'applyDefaultProtocol',
label: 'Default',
commands: [
'loadStudy',
{
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: 'default',
},
},
],
},
{
id: 'applyMPRProtocol',
label: '2x2 Grid',
commands: [
'loadStudy',
{
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGrid',
},
},
],
},
],
},
{
id: 'showInOtherMonitor',
label: 'Launch On Second Monitor',
iconName: 'DicomTagBrowser',
// we should use evaluator for this, as these are basically toolbar buttons
selector: ({ servicesManager }) => {
const { multiMonitorService } = servicesManager.services;
return multiMonitorService.isMultimonitor;
},
commands: {
commandName: 'multimonitor',
commandOptions: {
hashParams: '&hangingProtocolId=@ohif/mnGrid8',
commands: [
'loadStudy',
{
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGrid8',
},
},
],
},
},
},
],
},
},
{
name: 'default',
value: [
@@ -117,7 +193,6 @@ export default function getCustomizationModule({ servicesManager, extensionManag
);
},
},
{
id: 'ohif.contextMenu',
@@ -140,6 +215,7 @@ export default function getCustomizationModule({ servicesManager, extensionManag
return clonedObject;
},
},
studyBrowserContextMenu,
{
// the generic GUI component to configure a data source using an instance of a BaseDataSourceConfigurationAPI
@@ -200,6 +276,69 @@ export default function getCustomizationModule({ servicesManager, extensionManag
},
],
},
{
id: 'studyBrowser.thumbnailMenuItems',
value: [
{
id: 'tagBrowser',
label: 'Tag Browser',
iconName: 'DicomTagBrowser',
commands: 'openDICOMTagViewer',
},
],
},
{
id: 'ohif.menuContent',
content: function (props) {
const { item, commandsManager, servicesManager, ...rest } = props;
// If item has sub-items, render a submenu
if (item.items) {
return (
<DropdownMenuSub>
<DropdownMenuSubTrigger className="gap-[6px]">
{item.iconName && (
<Icons.ByName
name={item.iconName}
className="-ml-1"
/>
)}
{item.label}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
{item.items.map(subItem => this.content({ ...props, item: subItem }))}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
);
}
// Regular menu item
const isDisabled = item.selector && !item.selector({ servicesManager });
return (
<DropdownMenuItem
disabled={isDisabled}
onSelect={() => {
commandsManager.runAsync(item.commands, {
...item.commandOptions,
...rest,
});
}}
className="gap-[6px]"
>
{item.iconName && (
<Icons.ByName
name={item.iconName}
className="-ml-1"
/>
)}
{item.label}
</DropdownMenuItem>
);
},
},
],
},
];
@@ -1,4 +1,4 @@
import hpMNGrid from './hangingprotocols/hpMNGrid';
import { hpMN, hpMN8, hpMNMonitor2 } from './hangingprotocols/hpMNGrid';
import hpMNCompare from './hangingprotocols/hpCompare';
import hpMammography from './hangingprotocols/hpMammo';
import hpScale from './hangingprotocols/hpScale';
@@ -142,8 +142,16 @@ function getHangingProtocolModule() {
},
// Create a MxN hanging protocol available by default
{
name: hpMNGrid.id,
protocol: hpMNGrid,
name: hpMN.id,
protocol: hpMN,
},
{
name: hpMN8.id,
protocol: hpMN8,
},
{
name: hpMNMonitor2.id,
protocol: hpMNMonitor2,
},
];
}
@@ -1,4 +1,7 @@
import { Types } from '@ohif/core';
import { studyWithImages } from './utils/studySelectors';
import { seriesWithImages } from './utils/seriesSelectors';
import { viewportOptions } from './utils/viewportOptions';
/**
* Sync group configuration for hydrating segmentations across viewports
@@ -21,41 +24,15 @@ export const HYDRATE_SEG_SYNC_GROUP = {
* `&hangingProtocolId=@ohif/mnGrid` added to the viewer URL
* It is not included in the viewer mode by default.
*/
const hpMN: Types.HangingProtocol.Protocol = {
export const hpMN: Types.HangingProtocol.Protocol = {
id: '@ohif/mnGrid',
description: 'Has various hanging protocol grid layouts',
name: '2x2',
protocolMatchingRules: [
{
id: 'OneOrMoreSeries',
weight: 25,
attribute: 'numberOfDisplaySetsWithImages',
constraint: {
greaterThan: 0,
},
},
],
protocolMatchingRules: studyWithImages,
toolGroupIds: ['default'],
displaySetSelectors: {
defaultDisplaySetId: {
seriesMatchingRules: [
{
attribute: 'numImageFrames',
constraint: {
greaterThan: { value: 0 },
},
required: true,
},
// This display set will select the specified items by preference
// It has no affect if nothing is specified in the URL.
{
attribute: 'isDisplaySetFromUrl',
weight: 20,
constraint: {
equals: true,
},
},
],
seriesMatchingRules: seriesWithImages,
},
},
defaultViewport: {
@@ -90,21 +67,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
},
viewports: [
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
syncGroups: [
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
},
],
},
viewportOptions,
displaySets: [
{
id: 'defaultDisplaySetId',
@@ -112,10 +75,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
],
},
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 1,
@@ -124,21 +84,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
],
},
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
syncGroups: [
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
// options: {
// matchingRules: ['sameFOR'],
// },
},
],
},
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 2,
@@ -147,21 +93,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
],
},
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
syncGroups: [
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
// options: {
// matchingRules: ['sameFOR'],
// },
},
],
},
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 3,
@@ -174,11 +106,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
// 3x1 stage
{
id: '3x1',
// Obsolete settings:
requiredViewports: 1,
preferredViewports: 3,
// New equivalent:
name: '3x1',
stageActivation: {
enabled: {
minViewportsMatched: 3,
@@ -193,10 +121,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
},
viewports: [
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
viewportOptions,
displaySets: [
{
id: 'defaultDisplaySetId',
@@ -204,10 +129,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
],
},
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
viewportOptions,
displaySets: [
{
id: 'defaultDisplaySetId',
@@ -216,10 +138,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
],
},
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
viewportOptions,
displaySets: [
{
id: 'defaultDisplaySetId',
@@ -232,9 +151,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
// A 2x1 stage
{
id: '2x1',
requiredViewports: 1,
preferredViewports: 2,
name: '2x1',
stageActivation: {
enabled: {
minViewportsMatched: 2,
@@ -249,10 +166,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
},
viewports: [
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
viewportOptions,
displaySets: [
{
id: 'defaultDisplaySetId',
@@ -260,10 +174,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
],
},
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 1,
@@ -276,9 +187,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
// A 1x1 stage - should be automatically activated if there is only 1 viewable instance
{
id: '1x1',
requiredViewports: 1,
preferredViewports: 1,
name: '1x1',
stageActivation: {
enabled: {
minViewportsMatched: 1,
@@ -293,10 +202,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
},
viewports: [
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
viewportOptions,
displaySets: [
{
id: 'defaultDisplaySetId',
@@ -309,4 +215,341 @@ const hpMN: Types.HangingProtocol.Protocol = {
numberOfPriorsReferenced: -1,
};
/**
* This hanging protocol can be activated on the primary mode by directly
* referencing it in a URL or by directly including it within a mode, e.g.:
* `&hangingProtocolId=@ohif/mnGrid8` added to the viewer URL
* It is not included in the viewer mode by default.
*/
export const hpMN8: Types.HangingProtocol.Protocol = {
...hpMN,
id: '@ohif/mnGrid8',
description: 'Has various hanging protocol grid layouts up to 4x2',
name: '4x2',
stages: [
{
id: '4x2',
name: '4x2',
stageActivation: {
enabled: {
minViewportsMatched: 7,
},
},
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 2,
columns: 4,
},
},
viewports: [
{
viewportOptions,
displaySets: [
{
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 1,
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 2,
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 3,
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 4,
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 5,
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 6,
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 7,
id: 'defaultDisplaySetId',
},
],
},
],
},
{
id: '3x2',
name: '3x2',
stageActivation: {
enabled: {
minViewportsMatched: 5,
},
},
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 2,
columns: 3,
},
},
viewports: [
{
viewportOptions,
displaySets: [
{
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 1,
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 2,
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 3,
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 4,
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 5,
id: 'defaultDisplaySetId',
},
],
},
],
},
...hpMN.stages,
],
};
/**
* This hanging protocol extends the default protocol with additional
* images on the second monitor. It assumes the first four images are shown
* on monitor 0
*/
export const hpMNMonitor2: Types.HangingProtocol.Protocol = {
...hpMN,
id: '@ohif/mnGridMonitor2',
description: 'Second monitor HP with 2x2 grid',
name: '2x2 Monitor 2',
stages: [
{
id: '2x2',
name: '2x2',
stageActivation: {
enabled: {
minViewportsMatched: 3,
},
},
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 2,
columns: 2,
},
},
viewports: [
{
viewportOptions,
displaySets: [
{
id: 'defaultDisplaySetId',
matchedDisplaySetsIndex: 4,
},
],
},
{
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 5,
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 6,
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 7,
id: 'defaultDisplaySetId',
},
],
},
],
},
// A 2x1 stage
{
name: '2x1',
stageActivation: {
enabled: {
minViewportsMatched: 1,
},
},
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 2,
},
},
viewports: [
{
viewportOptions,
displaySets: [
{
id: 'defaultDisplaySetId',
matchedDisplaySetsIndex: 4,
},
],
},
{
viewportOptions,
displaySets: [
{
matchedDisplaySetsIndex: 5,
id: 'defaultDisplaySetId',
},
],
},
],
},
{
name: '1x1',
stageActivation: {
enabled: {
minViewportsMatched: 0,
},
},
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 1,
},
},
viewports: [
{
viewportOptions,
displaySets: [
{
id: 'defaultDisplaySetId',
matchedDisplaySetsIndex: 4,
},
],
},
],
},
{
name: '1x1 Base',
stageActivation: {
enabled: {
minViewportsMatched: 1,
},
},
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 1,
},
},
viewports: [
{
viewportOptions,
displaySets: [
{
id: 'defaultDisplaySetId',
matchedDisplaySetsIndex: 0,
},
],
},
],
},
],
};
export default hpMN;
@@ -7,7 +7,7 @@ import {
LCCPrior,
RMLOPrior,
LMLOPrior,
} from './mammoDisplaySetSelector';
} from './utils/mammoDisplaySetSelector';
const rightDisplayArea = {
storeAsInitialCamera: true,
@@ -1,9 +1,10 @@
import viewCodeAttribute from './viewCode';
import lateralityAttribute from './laterality';
import registerHangingProtocolAttributes from './registerHangingProtocolAttributes';
import viewCodeAttribute from './utils/viewCode';
import lateralityAttribute from './utils/laterality';
import registerHangingProtocolAttributes from './utils/registerHangingProtocolAttributes';
import hpMammography from './hpMammo';
import hpMNGrid from './hpMNGrid';
import hpCompare from './hpCompare';
export * from './hpMNGrid';
export {
viewCodeAttribute,
@@ -0,0 +1,23 @@
import { Types } from '@ohif/core';
type MatchingRule = Types.HangingProtocol.MatchingRule;
export const seriesWithImages: MatchingRule[] = [
{
attribute: 'numImageFrames',
constraint: {
greaterThan: { value: 0 },
},
weight: 1,
required: true,
},
// This display set will select the specified items by preference
// It has no affect if nothing is specified in the URL.
{
attribute: 'isDisplaySetFromUrl',
weight: 20,
constraint: {
equals: true,
},
},
];
@@ -0,0 +1,14 @@
import { Types } from '@ohif/core';
type MatchingRule = Types.HangingProtocol.MatchingRule;
export const studyWithImages: MatchingRule[] = [
{
id: 'OneOrMoreSeries',
weight: 25,
attribute: 'numberOfDisplaySetsWithImages',
constraint: {
greaterThan: 0,
},
},
];
@@ -0,0 +1,18 @@
/** A default viewport options */
export const viewportOptions = {
toolGroupId: 'default',
allowUnmatchedView: true,
syncGroups: [
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
},
],
};
export const hydrateSegDefault = viewportOptions;
+4
View File
@@ -37,6 +37,8 @@ import promptLabelAnnotation from './utils/promptLabelAnnotation';
import usePatientInfo from './hooks/usePatientInfo';
import { PanelStudyBrowserHeader } from './Panels/StudyBrowser/PanelStudyBrowserHeader';
import * as utils from './utils';
import MoreDropdownMenu from './Components/MoreDropdownMenu';
import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy';
const defaultExtension: Types.Extensions.Extension = {
/**
@@ -102,4 +104,6 @@ export {
usePatientInfo,
PanelStudyBrowserHeader,
utils,
MoreDropdownMenu,
requestDisplaySetCreationForStudy,
};