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:
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 };
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
+109
@@ -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 };
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user