feat(4D): Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode (#3664)

Co-authored-by: Alireza <ar.sedghi@gmail.com>
Co-authored-by: Neil <neil.a.macphee@gmail.com>
This commit is contained in:
authored and GitHub committed 2024-04-09 23:57:34 -04:00
1 parent dc0b183dd4
commit d57e8bc157
200 files changed
+9589 -2178

No files matched your search

@@ -0,0 +1,32 @@
import React from 'react';
import { LineChart } from '@ohif/ui';
const LineChartViewport = ({ displaySets }) => {
const displaySet = displaySets[0];
const { axis: chartAxis, series: chartSeries } = displaySet.instance.chartData;
return (
<LineChart
showLegend={true}
legendWidth={150}
axis={{
x: {
label: chartAxis.x.label,
indexRef: 0,
type: 'x',
range: {
min: 0,
},
},
y: {
label: chartAxis.y.label,
indexRef: 1,
type: 'y',
},
}}
series={chartSeries}
/>
);
};
export { LineChartViewport as default };
@@ -0,0 +1 @@
export { default } from './LineChartViewport';
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import { SidePanel } from '@ohif/ui';
import { PanelService, ServicesManager } from '@ohif/core';
import { PanelService, ServicesManager, Types } from '@ohif/core';
export type SidePanelWithServicesProps = {
servicesManager: ServicesManager;
@@ -14,10 +14,10 @@ export type SidePanelWithServicesProps = {
const SidePanelWithServices = ({
servicesManager,
side,
className,
activeTabIndex: activeTabIndexProp,
tabs,
tabs: tabsProp,
expandedWidth,
...props
}: SidePanelWithServicesProps) => {
const panelService: PanelService = servicesManager?.services?.panelService;
@@ -25,36 +25,64 @@ const SidePanelWithServices = ({
// Thus going to the Study List page and back to the viewer resets this flag for a SidePanel.
const [hasBeenOpened, setHasBeenOpened] = useState(false);
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp);
const [tabs, setTabs] = useState(tabsProp ?? panelService.getPanels(side));
const handleSidePanelOpen = useCallback(() => {
setHasBeenOpened(true);
}, []);
const handleActiveTabIndexChange = useCallback(({ activeTabIndex }) => {
setActiveTabIndex(activeTabIndex);
}, []);
/** update the active tab index from outside */
useEffect(() => {
setActiveTabIndex(activeTabIndexProp);
}, [activeTabIndexProp]);
useEffect(() => {
if (panelService) {
const activatePanelSubscription = panelService.subscribe(
panelService.EVENTS.ACTIVATE_PANEL,
(activatePanelEvent: Types.ActivatePanelEvent) => {
if (!hasBeenOpened || activatePanelEvent.forceActive) {
const tabIndex = tabs.findIndex(tab => tab.id === activatePanelEvent.panelId);
if (tabIndex !== -1) {
setActiveTabIndex(tabIndex);
}
const { unsubscribe } = panelService.subscribe(
panelService.EVENTS.PANELS_CHANGED,
panelChangedEvent => {
if (panelChangedEvent.position !== side) {
return;
}
setTabs(panelService.getPanels(side));
}
);
return () => {
unsubscribe();
};
}, [panelService, side]);
useEffect(() => {
const activatePanelSubscription = panelService.subscribe(
panelService.EVENTS.ACTIVATE_PANEL,
(activatePanelEvent: Types.ActivatePanelEvent) => {
if (!hasBeenOpened || activatePanelEvent.forceActive) {
const tabIndex = tabs.findIndex(tab => tab.id === activatePanelEvent.panelId);
if (tabIndex !== -1) {
setActiveTabIndex(tabIndex);
}
}
);
}
);
return () => {
activatePanelSubscription.unsubscribe();
};
}
return () => {
activatePanelSubscription.unsubscribe();
};
}, [tabs, hasBeenOpened, panelService]);
return (
<SidePanel
{...props}
side={side}
className={className}
activeTabIndex={activeTabIndex}
tabs={tabs}
onOpen={() => {
setHasBeenOpened(true);
}}
activeTabIndex={activeTabIndex}
onOpen={handleSidePanelOpen}
onActiveTabIndexChange={handleActiveTabIndexChange}
expandedWidth={expandedWidth}
></SidePanel>
);
@@ -1,44 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { LegacyButton, LegacyButtonGroup } from '@ohif/ui';
function ActionButtons({ onExportClick, onCreateReportClick }) {
const { t } = useTranslation('MeasurementTable');
return (
<React.Fragment>
<LegacyButtonGroup
color="black"
size="inherit"
>
{/* TODO Revisit design of LegacyButtonGroup later - for now use LegacyButton for its children.*/}
<LegacyButton
className="px-2 py-2 text-base"
onClick={onExportClick}
>
{t('Export CSV')}
</LegacyButton>
<LegacyButton
className="px-2 py-2 text-base"
onClick={onCreateReportClick}
>
{t('Create Report')}
</LegacyButton>
</LegacyButtonGroup>
</React.Fragment>
);
}
ActionButtons.propTypes = {
onExportClick: PropTypes.func,
onCreateReportClick: PropTypes.func,
};
ActionButtons.defaultProps = {
onExportClick: () => alert('Export'),
onCreateReportClick: () => alert('Create Report'),
};
export default ActionButtons;
@@ -2,8 +2,14 @@ import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { utils, ServicesManager } from '@ohif/core';
import { MeasurementTable, Dialog, Input, useViewportGrid, ButtonEnums } from '@ohif/ui';
import ActionButtons from './ActionButtons';
import {
MeasurementTable,
Dialog,
Input,
useViewportGrid,
ButtonEnums,
ActionButtons,
} from '@ohif/ui';
import debounce from 'lodash.debounce';
import createReportDialogPrompt, {
@@ -212,7 +218,7 @@ export default function PanelMeasurementTable({
data-cy={'measurements-panel'}
>
<MeasurementTable
title={t("Measurements")}
title={t('Measurements')}
servicesManager={servicesManager}
data={displayMeasurements}
onClick={jumpToImage}
@@ -221,9 +227,17 @@ export default function PanelMeasurementTable({
</div>
<div className="flex justify-center p-4">
<ActionButtons
onExportClick={exportReport}
onClearMeasurementsClick={clearMeasurements}
onCreateReportClick={createReport}
t={t('MeasurementTable')}
actions={[
{
label: 'Export',
onClick: exportReport,
},
{
label: 'Create Report',
onClick: createReport,
},
]}
/>
</div>
</>
@@ -0,0 +1,97 @@
import { Types, DisplaySetService, utils } from '@ohif/core';
import { id } from '../id';
type InstanceMetadata = Types.InstanceMetadata;
const SOPClassHandlerName = 'chart';
const CHART_MODALITY = 'CHT';
// Private SOPClassUid for chart data
const ChartDataSOPClassUid = '1.9.451.13215.7.3.2.7.6.1';
const sopClassUids = [ChartDataSOPClassUid];
const makeChartDataDisplaySet = (instance, sopClassUids) => {
const {
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID,
SeriesDescription,
SeriesNumber,
SeriesDate,
SOPClassUID,
} = instance;
return {
Modality: CHART_MODALITY,
loading: false,
isReconstructable: false,
displaySetInstanceUID: utils.guid(),
SeriesDescription,
SeriesNumber,
SeriesDate,
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
SOPClassHandlerId: `${id}.sopClassHandlerModule.${SOPClassHandlerName}`,
SOPClassUID,
isDerivedDisplaySet: true,
isLoaded: true,
sopClassUids,
instance,
instances: [instance],
/**
* Adds instances to the chart displaySet, rather than creating a new one
* when user moves to a different workflow step and gets back to a step that
* recreates the chart
*/
addInstances: function (instances: InstanceMetadata[], _displaySetService: DisplaySetService) {
this.instances.push(...instances);
this.instance = this.instances[this.instances.length - 1];
return this;
},
};
};
function getSopClassUids(instances) {
const uniqueSopClassUidsInSeries = new Set();
instances.forEach(instance => {
uniqueSopClassUidsInSeries.add(instance.SOPClassUID);
});
const sopClassUids = Array.from(uniqueSopClassUidsInSeries);
return sopClassUids;
}
function _getDisplaySetsFromSeries(instances) {
debugger;
// If the series has no instances, stop here
if (!instances || !instances.length) {
throw new Error('No instances were provided');
}
const sopClassUids = getSopClassUids(instances);
const displaySets = instances.map(instance => {
if (instance.Modality === CHART_MODALITY) {
return makeChartDataDisplaySet(instance, sopClassUids);
}
throw new Error('Unsupported modality');
});
return displaySets;
}
const chartHandler = {
name: SOPClassHandlerName,
sopClassUids,
getDisplaySetsFromSeries: instances => {
return _getDisplaySetsFromSeries(instances);
},
};
export { chartHandler };
+2 -2
View File
@@ -3,10 +3,10 @@ import { Tooltip } from '@ohif/ui';
import classnames from 'classnames';
import { useToolbar } from '@ohif/core';
export function Toolbar({ servicesManager }) {
export function Toolbar({ servicesManager, buttonSection = 'primary' }) {
const { toolbarButtons, onInteraction } = useToolbar({
servicesManager,
buttonSection: 'primary',
buttonSection,
});
if (!toolbarButtons.length) {
@@ -6,13 +6,11 @@ import { useLocation } from 'react-router';
import { ErrorBoundary, UserPreferences, AboutModal, Header, useModal } from '@ohif/ui';
import i18n from '@ohif/i18n';
import { hotkeys } from '@ohif/core';
import { useAppConfig } from '@state';
import { Toolbar } from '../Toolbar/Toolbar';
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
function ViewerHeader({ hotkeysManager, extensionManager, servicesManager }) {
const [appConfig] = useAppConfig();
function ViewerHeader({ hotkeysManager, extensionManager, servicesManager, appConfig }) {
const navigate = useNavigate();
const location = useLocation();
@@ -107,6 +105,13 @@ function ViewerHeader({ hotkeysManager, extensionManager, servicesManager }) {
WhiteLabeling={appConfig.whiteLabeling}
showPatientInfo={appConfig.showPatientInfo}
servicesManager={servicesManager}
Secondary={
<Toolbar
servicesManager={servicesManager}
buttonSection="secondary"
/>
}
appConfig={appConfig}
>
<ErrorBoundary context="Primary Toolbar">
<div className="relative flex justify-center">
+42 -36
View File
@@ -1,12 +1,7 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import {
SidePanel,
ErrorBoundary,
LoadingIndicatorProgress,
InvestigationalUseDialog,
} from '@ohif/ui';
import { ErrorBoundary, LoadingIndicatorProgress, InvestigationalUseDialog } from '@ohif/ui';
import { ServicesManager, HangingProtocolService, CommandsManager } from '@ohif/core';
import { useAppConfig } from '@state';
import ViewerHeader from './ViewerHeader';
@@ -21,16 +16,24 @@ function ViewerLayout({
// From Modes
viewports,
ViewportGridComp,
leftPanels = [],
rightPanels = [],
leftPanelDefaultClosed = false,
rightPanelDefaultClosed = false,
leftPanelClosed = false,
rightPanelClosed = false,
}): React.FunctionComponent {
const [appConfig] = useAppConfig();
const { hangingProtocolService } = servicesManager.services;
const { panelService, hangingProtocolService } = servicesManager.services;
const [showLoadingIndicator, setShowLoadingIndicator] = useState(appConfig.showLoadingIndicator);
const hasPanels = useCallback(
(side): boolean => !!panelService.getPanels(side).length,
[panelService]
);
const [hasRightPanels, setHasRightPanels] = useState(hasPanels('right'));
const [hasLeftPanels, setHasLeftPanels] = useState(hasPanels('left'));
const [leftPanelClosedState, setLeftPanelClosed] = useState(leftPanelClosed);
const [rightPanelClosedState, setRightPanelClosed] = useState(rightPanelClosed);
/**
* Set body classes (tailwindcss) that don't allow vertical
* or horizontal overflow (no scrolling). Also guarantee window
@@ -57,20 +60,6 @@ function ViewerLayout({
return { entry, content: entry.component };
};
const getPanelData = id => {
const { content, entry } = getComponent(id);
return {
id: entry.id,
iconName: entry.iconName,
iconLabel: entry.iconLabel,
label: entry.label,
name: entry.name,
content,
contexts: entry.contexts,
};
};
useEffect(() => {
const { unsubscribe } = hangingProtocolService.subscribe(
HangingProtocolService.EVENTS.PROTOCOL_CHANGED,
@@ -97,8 +86,26 @@ function ViewerLayout({
};
};
const leftPanelComponents = leftPanels.map(getPanelData);
const rightPanelComponents = rightPanels.map(getPanelData);
useEffect(() => {
const { unsubscribe } = panelService.subscribe(
panelService.EVENTS.PANELS_CHANGED,
({ options }) => {
setHasLeftPanels(hasPanels('left'));
setHasRightPanels(hasPanels('right'));
if (options?.leftPanelClosed !== undefined) {
setLeftPanelClosed(options.leftPanelClosed);
}
if (options?.rightPanelClosed !== undefined) {
setRightPanelClosed(options.rightPanelClosed);
}
}
);
return () => {
unsubscribe();
};
}, [panelService, hasPanels]);
const viewportComponents = viewports.map(getViewportComponentData);
return (
@@ -107,6 +114,7 @@ function ViewerLayout({
hotkeysManager={hotkeysManager}
extensionManager={extensionManager}
servicesManager={servicesManager}
appConfig={appConfig}
/>
<div
className="relative flex w-full flex-row flex-nowrap items-stretch overflow-hidden bg-black"
@@ -115,12 +123,11 @@ function ViewerLayout({
<React.Fragment>
{showLoadingIndicator && <LoadingIndicatorProgress className="h-full w-full bg-black" />}
{/* LEFT SIDEPANELS */}
{leftPanelComponents.length ? (
{hasLeftPanels ? (
<ErrorBoundary context="Left Panel">
<SidePanelWithServices
side="left"
activeTabIndex={leftPanelDefaultClosed ? null : 0}
tabs={leftPanelComponents}
activeTabIndex={leftPanelClosedState ? null : 0}
servicesManager={servicesManager}
/>
</ErrorBoundary>
@@ -137,12 +144,11 @@ function ViewerLayout({
</ErrorBoundary>
</div>
</div>
{rightPanelComponents.length ? (
{hasRightPanels ? (
<ErrorBoundary context="Right Panel">
<SidePanelWithServices
side="right"
activeTabIndex={rightPanelDefaultClosed ? null : 0}
tabs={rightPanelComponents}
activeTabIndex={rightPanelClosedState ? null : 0}
servicesManager={servicesManager}
/>
</ErrorBoundary>
@@ -165,8 +171,8 @@ ViewerLayout.propTypes = {
// From modes
leftPanels: PropTypes.array,
rightPanels: PropTypes.array,
leftPanelDefaultClosed: PropTypes.bool.isRequired,
rightPanelDefaultClosed: PropTypes.bool.isRequired,
leftPanelClosed: PropTypes.bool.isRequired,
rightPanelClosed: PropTypes.bool.isRequired,
/** Responsible for rendering our grid of viewports; provided by consuming application */
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
viewports: PropTypes.array,
-10
View File
@@ -26,15 +26,6 @@ export type UpdateViewportDisplaySetParams = {
excludeNonImageModalities?: boolean;
};
/**
* Determine if a command is a hanging protocol one.
* For now, just use the two hanging protocol commands that are in this
* commands module, but if others get added elsewhere this may need enhancing.
*/
const isHangingProtocolCommand = command =>
command &&
(command.commandName === 'setHangingProtocol' || command.commandName === 'toggleHangingProtocol');
const commandsModule = ({
servicesManager,
commandsManager,
@@ -47,7 +38,6 @@ const commandsModule = ({
viewportGridService,
displaySetService,
stateSyncService,
toolbarService,
} = (servicesManager as ServicesManager).services;
// Define a context menu controller for use with any context menus
@@ -0,0 +1,109 @@
import React, { useEffect, useState, useCallback, ReactElement } from 'react';
import { ServicesManager } from '@ohif/core';
import { ProgressDropdown } from '@ohif/ui';
const workflowStepsToDropdownOptions = (steps = []) =>
steps.map(step => ({
label: step.name,
value: step.id,
info: step.info,
activated: false,
completed: false,
}));
function ProgressDropdownWithService({
servicesManager,
}: {
servicesManager: ServicesManager;
}): ReactElement {
const { workflowStepsService } = servicesManager.services;
const [activeStepId, setActiveStepId] = useState(workflowStepsService.activeWorkflowStep?.id);
const [dropdownOptions, setDropdownOptions] = useState(
workflowStepsToDropdownOptions(workflowStepsService.workflowSteps)
);
const setCurrentAndPreviousOptionsAsCompleted = useCallback(currentOption => {
if (currentOption.completed) {
return;
}
setDropdownOptions(prevOptions => {
const newOptionsState = [...prevOptions];
const startIndex = newOptionsState.findIndex(option => option.value === currentOption.value);
for (let i = startIndex; i >= 0; i--) {
const option = newOptionsState[i];
if (option.completed) {
break;
}
newOptionsState[i] = {
...option,
completed: true,
};
}
return newOptionsState;
});
}, []);
const handleDropdownChange = useCallback(
({ selectedOption }) => {
if (!selectedOption) {
return;
}
// TODO: Steps should be marked as completed after user has
// completed some action when required (not implemented)
setCurrentAndPreviousOptionsAsCompleted(selectedOption);
setActiveStepId(selectedOption.value);
},
[setCurrentAndPreviousOptionsAsCompleted]
);
useEffect(() => {
let timeoutId;
if (activeStepId) {
// We've used setTimeout to give it more time to update the UI since
// create3DFilterableFromDataArray from Texture.js may take 600+ ms to run
// when there is a new series to load in the next step but that resulted
// in the followed React error when updating the content from left/right panels
// and all component states were being lost:
// Error: Can't perform a React state update on an unmounted component
workflowStepsService.setActiveWorkflowStep(activeStepId);
}
return () => clearTimeout(timeoutId);
}, [activeStepId, workflowStepsService]);
useEffect(() => {
const { unsubscribe: unsubStepsChanged } = workflowStepsService.subscribe(
workflowStepsService.EVENTS.STEPS_CHANGED,
() => setDropdownOptions(workflowStepsToDropdownOptions(workflowStepsService.workflowSteps))
);
const { unsubscribe: unsubActiveStepChanged } = workflowStepsService.subscribe(
workflowStepsService.EVENTS.ACTIVE_STEP_CHANGED,
() => setActiveStepId(workflowStepsService.activeWorkflowStep.id)
);
return () => {
unsubStepsChanged();
unsubActiveStepChanged();
};
}, [servicesManager, workflowStepsService]);
return (
<ProgressDropdown
options={dropdownOptions}
value={activeStepId}
onChange={handleDropdownChange}
/>
);
}
export default ProgressDropdownWithService;
@@ -0,0 +1 @@
export { default } from './ProgressDropdownWithService';
@@ -1,6 +1,7 @@
import { CustomizationService } from '@ohif/core';
import React from 'react';
import DataSourceSelector from './Panels/DataSourceSelector';
import ProgressDropdownWithService from './components/ProgressDropdownWithService';
import DataSourceConfigurationComponent from './Components/DataSourceConfigurationComponent';
import { GoogleCloudDataSourceConfigurationAPI } from './DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI';
@@ -154,6 +155,11 @@ export default function getCustomizationModule({ servicesManager, extensionManag
extensionManager
),
},
{
id: 'progressDropdownWithServiceComponent',
component: ProgressDropdownWithService,
},
],
},
];
@@ -10,9 +10,15 @@ import checkSingleFrames from './utils/validations/checkSingleFrames';
*/
export default function getDisplaySetMessages(
instances: Array<any>,
isReconstructable: boolean
isReconstructable: boolean,
isDynamicVolume: boolean
): DisplaySetMessageList {
const messages = new DisplaySetMessageList();
if (isDynamicVolume) {
return messages;
}
if (!instances.length) {
messages.addMessage(DisplaySetMessage.CODES.NO_VALID_INSTANCES);
return;
@@ -5,23 +5,79 @@ import isDisplaySetReconstructable from '@ohif/core/src/utils/isDisplaySetRecons
import { id } from './id';
import getDisplaySetMessages from './getDisplaySetMessages';
import getDisplaySetsFromUnsupportedSeries from './getDisplaySetsFromUnsupportedSeries';
import { chartHandler } from './SOPClassHandlers/chartSOPClassHandler';
const DEFAULT_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
const DYNAMIC_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingDynamicImageVolume';
const sopClassHandlerName = 'stack';
let appContext = {};
const getDynamicVolumeInfo = instances => {
const { extensionManager } = appContext;
if (!extensionManager) {
throw new Error('extensionManager is not available');
}
const imageIds = instances.map(({ imageId }) => imageId);
const volumeLoaderUtility = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.volumeLoader'
);
const { getDynamicVolumeInfo: csGetDynamicVolumeInfo } = volumeLoaderUtility.exports;
return csGetDynamicVolumeInfo(imageIds);
};
const isMultiFrame = instance => {
return instance.NumberOfFrames > 1;
};
function getDisplaySetInfo(instances) {
const dynamicVolumeInfo = getDynamicVolumeInfo(instances);
const { isDynamicVolume, timePoints } = dynamicVolumeInfo;
let displaySetInfo;
if (isDynamicVolume) {
const timePoint = timePoints[0];
const instancesMap = new Map();
// O(n) to convert it into a map and O(1) to find each instance
instances.forEach(instance => instancesMap.set(instance.imageId, instance));
const firstTimePointInstances = timePoint.map(imageId => instancesMap.get(imageId));
displaySetInfo = isDisplaySetReconstructable(firstTimePointInstances);
} else {
displaySetInfo = isDisplaySetReconstructable(instances);
}
return {
isDynamicVolume,
...displaySetInfo,
dynamicVolumeInfo,
};
}
const makeDisplaySet = instances => {
const instance = instances[0];
const imageSet = new ImageSet(instances);
const { value: isReconstructable, averageSpacingBetweenFrames } =
isDisplaySetReconstructable(instances);
const {
isDynamicVolume,
value: isReconstructable,
averageSpacingBetweenFrames,
dynamicVolumeInfo,
} = getDisplaySetInfo(instances);
const volumeLoaderSchema = isDynamicVolume
? DYNAMIC_VOLUME_LOADER_SCHEME
: DEFAULT_VOLUME_LOADER_SCHEME;
// set appropriate attributes to image set...
const messages = getDisplaySetMessages(instances, isReconstructable);
const messages = getDisplaySetMessages(instances, isReconstructable, isDynamicVolume);
imageSet.setAttributes({
volumeLoaderSchema,
displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
SeriesDate: instance.SeriesDate,
SeriesTime: instance.SeriesTime,
@@ -39,6 +95,8 @@ const makeDisplaySet = instances => {
isReconstructable,
messages,
averageSpacingBetweenFrames: averageSpacingBetweenFrames || null,
isDynamicVolume,
dynamicVolumeInfo,
});
// Sort the images in this series if needed
@@ -88,7 +146,6 @@ function getSopClassUids(instances) {
* - For all Image types that are stackable, create
* a displaySet with a stack of images
*
* @param {Array} sopClassHandlerModules List of SOP Class Modules
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
* @returns {Array} The list of display sets created for the given series object
*/
@@ -203,7 +260,9 @@ const sopClassUids = [
sopClassDictionary.EnhancedUSVolumeStorage,
];
function getSopClassHandlerModule() {
function getSopClassHandlerModule(appContextParam) {
appContext = appContextParam;
return [
{
name: sopClassHandlerName,
@@ -215,6 +274,11 @@ function getSopClassHandlerModule() {
sopClassUids: [],
getDisplaySetsFromSeries: getDisplaySetsFromUnsupportedSeries,
},
{
name: chartHandler.name,
sopClassUids: chartHandler.sopClassUids,
getDisplaySetsFromSeries: chartHandler.getDisplaySetsFromSeries,
},
];
}
@@ -3,6 +3,7 @@ import ToolbarLayoutSelectorWithServices from './Toolbar/ToolbarLayoutSelector';
import ToolbarSplitButtonWithServices from './Toolbar/ToolbarSplitButtonWithServices';
import ToolbarButtonGroupWithServices from './Toolbar/ToolbarButtonGroupWithServices';
import { ToolbarButton } from '@ohif/ui';
import ProgressDropdownWithService from './components/ProgressDropdownWithService';
const getClassName = isToggled => {
return {
@@ -36,6 +37,10 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
name: 'ohif.buttonGroup',
defaultComponent: ToolbarButtonGroupWithServices,
},
{
name: 'ohif.progressDropdown',
defaultComponent: ProgressDropdownWithService,
},
{
name: 'evaluate.group.promoteToPrimary',
evaluate: ({ viewportId, button, itemId }) => {
@@ -0,0 +1,21 @@
import { ServicesManager, CommandsManager, ExtensionManager } from '@ohif/core';
import LineChartViewport from './Components/LineChartViewport/index';
const getViewportModule = ({
servicesManager,
commandsManager,
extensionManager,
}: {
servicesManager: ServicesManager;
commandsManager: CommandsManager;
extensionManager: ExtensionManager;
}) => {
return [
{
name: 'chartViewport',
component: LineChartViewport,
},
];
};
export { getViewportModule as default };
+2
View File
@@ -9,6 +9,7 @@ import getCommandsModule from './commandsModule';
import getHangingProtocolModule from './getHangingProtocolModule';
import getStudiesForPatientByMRN from './Panels/getStudiesForPatientByMRN';
import getCustomizationModule from './getCustomizationModule';
import getViewportModule from './getViewportModule';
import { id } from './id.js';
import preRegistration from './init';
import { ContextMenuController, CustomizableContextMenuTypes } from './CustomizableContextMenu';
@@ -23,6 +24,7 @@ const defaultExtension: Types.Extensions.Extension = {
id,
preRegistration,
getDataSourcesModule,
getViewportModule,
getLayoutTemplateModule,
getPanelModule,
getHangingProtocolModule,
+1 -1
View File
@@ -77,7 +77,7 @@ export default function init({ servicesManager, configuration = {}, commandsMana
toolbarService.subscribe(toolbarService.EVENTS.TOOL_BAR_MODIFIED, state => {
const { buttons } = state;
for (const [id, button] of Object.entries(buttons)) {
const { groupId, items, listeners } = button.props;
const { groupId, items, listeners } = button.props || {};
// Handle group items' listeners
if (groupId && items) {
@@ -21,6 +21,11 @@ const reuseCachedLayout = (
): ReturnType => {
const { activeViewportId } = state;
const { protocol } = hangingProtocolService.getActiveProtocol();
if (!protocol) {
return;
}
const hpInfo = hangingProtocolService.getState();
const { protocolId, stageIndex, activeStudyUID } = hpInfo;