feat(ViewportActionBar and CinePlayer): Add new design for action bar and cine player (#3204)
* feat(ViewportActionBar): OHIF issue #3123 (#3186) * feat(ViewportActionBar): OHIF issue #3123 - Renamed previous viewport action bar to be LegacyViewportActionBar - Components LegacyViewportActionBar depends on also renamed: LegacyCinePlayer and LegacyPatientInfo - New Viewport coded to specs in issue - added React hook useResizeObserver - added some tailwind classes * Updated tailwind to 3.2.7. Put external imports like React at the top of the import list. * feat(CinePlayer and ViewportActionBar) (#3198) * feat(CinePlayer and ViewportActionBar) - OHIF issue 3123 - new look cine control implemented - new custom blue color in tailwind config for various hover backgrounds in the cine control - new icons added for cine - Tooltip component now can be placed top (center) on hover - Tooltip component border colour now consistent with specs - fixed NPE in ViewportActionBar - upgraded tailwind to 3.2.7 in platform/ui - fixed issues in various button components brought by tailwind 3.2.7 where classes now need important flag - fixed issue with InputRange component so that the tracked value property can change externally - InputRange component can now optionally show its label - added new measurement tracking state service to hydrate SR without prompting - segmentation can also now be hydrated without prompting * PR feedback: - cine centralized to OHIFCornerstoneViewport - introduced a type for the CinePlayer properties * Addressed PR comments and concerns... The DOM ref for the root component of the ViewportActionBar is now added to state so that the various callbacks and ResizeObserver are updated with it. The CinePlayer FPS slider tooltip was moved up so that its arrow does not intersect the FPS text. The hover area for the CinePlayer slider tooltip is now the FPS < > buttons and text. The tracked measurements are now filtered to only include those of the active viewport series when the tracked measurement navigation arrows are used. * Addressed PR comments... The update to tailwind 3.2.7 caused several look-and-feel, UI regressions, so we are rolling back to 3.2.4.
This commit is contained in:
1 parent
c9125a0c0a
commit
35d0787211
55 files changed
+1205
-513
No files matched your search
@@ -1,19 +1,13 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import OHIF, { utils } from '@ohif/core';
|
||||
import {
|
||||
Notification,
|
||||
ViewportActionBar,
|
||||
useViewportGrid,
|
||||
useViewportDialog,
|
||||
LoadingIndicatorProgress,
|
||||
LoadingIndicatorProgress, Notification, useViewportDialog, useViewportGrid, ViewportActionBar
|
||||
} from '@ohif/ui';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
||||
import _hydrateSEGDisplaySet from '../utils/_hydrateSEG';
|
||||
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
||||
import hydrateSEGDisplaySet from '../utils/_hydrateSEG';
|
||||
import _getStatusComponent from './_getStatusComponent';
|
||||
|
||||
const { formatDate } = utils;
|
||||
@@ -307,16 +301,15 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
SeriesNumber,
|
||||
} = referencedDisplaySetRef.current.metadata;
|
||||
|
||||
const onPillClick = () => {
|
||||
promptHydrateSEG({
|
||||
servicesManager,
|
||||
viewportIndex,
|
||||
const onStatusClick = async () => {
|
||||
const isHydrated = await hydrateSEGDisplaySet({
|
||||
segDisplaySet,
|
||||
}).then(isHydrated => {
|
||||
if (isHydrated) {
|
||||
setIsHydrated(true);
|
||||
}
|
||||
viewportIndex,
|
||||
toolGroupId,
|
||||
servicesManager,
|
||||
});
|
||||
|
||||
setIsHydrated(isHydrated);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -330,14 +323,13 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
getStatusComponent={() => {
|
||||
return _getStatusComponent({
|
||||
isHydrated,
|
||||
onPillClick,
|
||||
onStatusClick,
|
||||
});
|
||||
}}
|
||||
studyData={{
|
||||
label: viewportLabel,
|
||||
useAltStyling: true,
|
||||
studyDate: formatDate(StudyDate),
|
||||
currentSeries: SeriesNumber,
|
||||
seriesDescription: `SEG Viewport ${SeriesDescription}`,
|
||||
patientInformation: {
|
||||
patientName: PatientName
|
||||
|
||||
@@ -1,92 +1,56 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon, Tooltip } from '@ohif/ui';
|
||||
|
||||
import _hydrateSEGDisplaySet from '../utils/_hydrateSEG';
|
||||
|
||||
export default function _getStatusComponent({ isHydrated, onPillClick }) {
|
||||
export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
||||
let ToolTipMessage = null;
|
||||
let StatusIcon = null;
|
||||
|
||||
const {t} = useTranslation("Common");
|
||||
const loadStr = t("LOAD");
|
||||
|
||||
switch (isHydrated) {
|
||||
case true:
|
||||
StatusIcon = () => (
|
||||
<div
|
||||
className="flex items-center justify-center -mr-1 rounded-full"
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
backgroundColor: '#98e5c1',
|
||||
border: 'solid 1.5px #000000',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="exclamation"
|
||||
style={{ color: '#000', width: '12px', height: '12px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
StatusIcon = () => <Icon name="status-alert" />;
|
||||
|
||||
ToolTipMessage = () => (
|
||||
<div>This Segmentation is loaded in the segmentation panel</div>
|
||||
);
|
||||
break;
|
||||
case false:
|
||||
StatusIcon = () => (
|
||||
<div
|
||||
className="flex items-center justify-center -mr-1 bg-white rounded-full group-hover:bg-customblue-200"
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
border: 'solid 1.5px #000000',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="arrow-left"
|
||||
style={{ color: '#000', width: '14px', height: '14px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case false:
|
||||
StatusIcon = () => <Icon name="status-untracked" />;
|
||||
|
||||
ToolTipMessage = () => <div>Click to load segmentation.</div>;
|
||||
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
|
||||
}
|
||||
|
||||
const StatusPill = () => (
|
||||
<div
|
||||
className={classNames(
|
||||
'group relative flex items-center justify-center px-8 rounded-full cursor-default bg-customgreen-100',
|
||||
{
|
||||
'hover:bg-customblue-100': !isHydrated,
|
||||
'cursor-pointer': !isHydrated,
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
height: '24px',
|
||||
width: '55px',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!isHydrated) {
|
||||
if (onPillClick) {
|
||||
onPillClick();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="pr-1 text-base font-medium leading-none text-black">
|
||||
SEG
|
||||
const StatusArea = () => (
|
||||
<div className="flex h-6 leading-6 cursor-default text-sm text-white">
|
||||
<div className="min-w-[45px] flex items-center p-1 rounded-l-xl rounded-r bg-customgray-100">
|
||||
<StatusIcon />
|
||||
<span className="ml-1">SEG</span>
|
||||
</div>
|
||||
<StatusIcon />
|
||||
{!isHydrated && (
|
||||
<div
|
||||
className="ml-1 px-1.5 rounded cursor-pointer hover:text-black bg-primary-main hover:bg-primary-light"
|
||||
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
|
||||
onMouseUp={onStatusClick}
|
||||
>
|
||||
{loadStr}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{ToolTipMessage && (
|
||||
<Tooltip content={<ToolTipMessage />} position="bottom-left">
|
||||
<StatusPill />
|
||||
<StatusArea />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!ToolTipMessage && <StatusPill />}
|
||||
{!ToolTipMessage && <StatusArea />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import OHIF, { utils } from '@ohif/core';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import OHIF, { utils } from '@ohif/core';
|
||||
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
||||
|
||||
import {
|
||||
Notification,
|
||||
ViewportActionBar,
|
||||
useViewportGrid,
|
||||
useViewportDialog,
|
||||
Tooltip,
|
||||
Icon,
|
||||
Notification,
|
||||
Tooltip,
|
||||
useViewportDialog,
|
||||
useViewportGrid,
|
||||
ViewportActionBar,
|
||||
} from '@ohif/ui';
|
||||
import classNames from 'classnames';
|
||||
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
|
||||
|
||||
const { formatDate } = utils;
|
||||
@@ -33,8 +32,6 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
extensionManager,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation('SRViewport');
|
||||
|
||||
const {
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
@@ -379,7 +376,6 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
label: viewportLabel,
|
||||
useAltStyling: true,
|
||||
studyDate: formatDate(StudyDate),
|
||||
currentSeries: SeriesNumber,
|
||||
seriesDescription: SeriesDescription || '',
|
||||
patientInformation: {
|
||||
patientName: PatientName
|
||||
@@ -468,13 +464,16 @@ function _getStatusComponent({
|
||||
isLocked,
|
||||
sendTrackedMeasurementsEvent,
|
||||
}) {
|
||||
const onPillClick = () => {
|
||||
sendTrackedMeasurementsEvent('RESTORE_PROMPT_HYDRATE_SR', {
|
||||
const handleMouseUp = () => {
|
||||
sendTrackedMeasurementsEvent('HYDRATE_SR', {
|
||||
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
|
||||
viewportIndex,
|
||||
});
|
||||
};
|
||||
|
||||
const { t } = useTranslation('Common');
|
||||
const loadStr = t('LOAD');
|
||||
|
||||
// 1 - Incompatible
|
||||
// 2 - Locked
|
||||
// 3 - Rehydratable / Open
|
||||
@@ -485,22 +484,7 @@ function _getStatusComponent({
|
||||
|
||||
switch (state) {
|
||||
case 1:
|
||||
StatusIcon = () => (
|
||||
<div
|
||||
className="flex items-center justify-center -mr-1 rounded-full"
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
backgroundColor: '#98e5c1',
|
||||
border: 'solid 1.5px #000000',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="exclamation"
|
||||
style={{ color: '#000', width: '12px', height: '12px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
StatusIcon = () => <Icon name="status-alert" />;
|
||||
|
||||
ToolTipMessage = () => (
|
||||
<div>
|
||||
@@ -511,20 +495,7 @@ function _getStatusComponent({
|
||||
);
|
||||
break;
|
||||
case 2:
|
||||
StatusIcon = () => (
|
||||
<div
|
||||
className="flex items-center justify-center -mr-1 bg-black rounded-full"
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="lock"
|
||||
style={{ color: '#05D97C', width: '8px', height: '11px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
StatusIcon = () => <Icon name="status-locked" />;
|
||||
|
||||
ToolTipMessage = () => (
|
||||
<div>
|
||||
@@ -537,48 +508,28 @@ function _getStatusComponent({
|
||||
);
|
||||
break;
|
||||
case 3:
|
||||
StatusIcon = () => (
|
||||
<div
|
||||
className="flex items-center justify-center -mr-1 bg-white rounded-full group-hover:bg-customblue-200"
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
border: 'solid 1.5px #000000',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="arrow-left"
|
||||
style={{ color: '#000', width: '14px', height: '14px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
StatusIcon = () => <Icon name="status-untracked" />;
|
||||
|
||||
ToolTipMessage = () => <div>Click to restore measurements.</div>;
|
||||
ToolTipMessage = () => (
|
||||
<div>{`Click ${loadStr} to restore measurements.`}</div>
|
||||
);
|
||||
}
|
||||
|
||||
const StatusPill = () => (
|
||||
<div
|
||||
className={classNames(
|
||||
'group relative flex items-center justify-center px-2 rounded-full cursor-default bg-customgreen-100',
|
||||
{
|
||||
'hover:bg-customblue-100': state === 3,
|
||||
'cursor-pointer': state === 3,
|
||||
}
|
||||
const StatusArea = () => (
|
||||
<div className="flex h-6 leading-6 cursor-default text-sm text-white">
|
||||
<div className="min-w-[45px] flex items-center p-1 rounded-l-xl rounded-r bg-customgray-100">
|
||||
<StatusIcon />
|
||||
<span className="ml-1">SR</span>
|
||||
</div>
|
||||
{state === 3 && (
|
||||
<div
|
||||
className="ml-1 px-1.5 rounded cursor-pointer hover:text-black bg-primary-main hover:bg-primary-light"
|
||||
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
|
||||
onMouseUp={handleMouseUp}
|
||||
>
|
||||
{loadStr}
|
||||
</div>
|
||||
)}
|
||||
style={{
|
||||
height: '24px',
|
||||
width: '55px',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (state === 3) {
|
||||
if (onPillClick) {
|
||||
onPillClick();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="pr-1 text-lg font-bold leading-none text-black">SR</span>
|
||||
<StatusIcon />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -586,22 +537,12 @@ function _getStatusComponent({
|
||||
<>
|
||||
{ToolTipMessage && (
|
||||
<Tooltip content={<ToolTipMessage />} position="bottom-left">
|
||||
<StatusPill />
|
||||
<StatusArea />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!ToolTipMessage && <StatusPill />}
|
||||
{!ToolTipMessage && <StatusArea />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// function _onDoubleClick() {
|
||||
// const cancelActiveManipulatorsForElement = cornerstoneTools.getModule(
|
||||
// 'manipulatorState'
|
||||
// ).setters.cancelActiveManipulatorsForElement;
|
||||
// const enabledElements = cornerstoneTools.store.state.enabledElements;
|
||||
// enabledElements.forEach(element => {
|
||||
// cancelActiveManipulatorsForElement(element);
|
||||
// });
|
||||
// }
|
||||
|
||||
export default OHIFCornerstoneSRViewport;
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
IVolumeViewport,
|
||||
} from '@cornerstonejs/core/dist/esm/types';
|
||||
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||
import { CinePlayer, useCine, useViewportGrid } from '@ohif/ui';
|
||||
|
||||
const STACK = 'stack';
|
||||
|
||||
@@ -112,6 +113,9 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
} = props;
|
||||
|
||||
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
||||
const [{ isCineEnabled, cines }, cineService] = useCine();
|
||||
const [{ activeViewportIndex }] = useViewportGrid();
|
||||
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
||||
|
||||
const elementRef = useRef();
|
||||
|
||||
@@ -126,6 +130,73 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
viewportGridService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const cineHandler = () => {
|
||||
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cine = cines[viewportIndex];
|
||||
const isPlaying = cine.isPlaying || false;
|
||||
const frameRate = cine.frameRate || 24;
|
||||
|
||||
const validFrameRate = Math.max(frameRate, 1);
|
||||
|
||||
if (isPlaying) {
|
||||
cineService.playClip(enabledVPElement, {
|
||||
framesPerSecond: validFrameRate,
|
||||
});
|
||||
} else {
|
||||
cineService.stopClip(enabledVPElement);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
eventTarget.addEventListener(
|
||||
Enums.Events.STACK_VIEWPORT_NEW_STACK,
|
||||
cineHandler
|
||||
);
|
||||
|
||||
return () => {
|
||||
cineService.setCine({ id: viewportIndex, isPlaying: false });
|
||||
eventTarget.removeEventListener(
|
||||
Enums.Events.STACK_VIEWPORT_NEW_STACK,
|
||||
cineHandler
|
||||
);
|
||||
};
|
||||
}, [enabledVPElement]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
cineHandler();
|
||||
|
||||
return () => {
|
||||
if (enabledVPElement && cines?.[viewportIndex]?.isPlaying) {
|
||||
cineService.stopClip(enabledVPElement);
|
||||
}
|
||||
};
|
||||
}, [cines, viewportIndex, cineService, enabledVPElement, cineHandler]);
|
||||
|
||||
const cine = cines[viewportIndex];
|
||||
const isPlaying = (cine && cine.isPlaying) || false;
|
||||
|
||||
const handleCineClose = () => {
|
||||
toolbarService.recordInteraction({
|
||||
groupId: 'MoreTools',
|
||||
itemId: 'cine',
|
||||
interactionType: 'toggle',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'toggleCine',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
// useCallback for scroll bar height calculation
|
||||
const setImageScrollBarHeight = useCallback(() => {
|
||||
const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`;
|
||||
@@ -176,6 +247,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
const viewportIndex = viewportInfo.getViewportIndex();
|
||||
|
||||
setEnabledElement(viewportIndex, element);
|
||||
setEnabledVPElement(element);
|
||||
|
||||
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
||||
const toolGroupId = viewportInfo.getToolGroupId();
|
||||
@@ -360,6 +432,25 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
scrollbarHeight={scrollbarHeight}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
{isCineEnabled && (
|
||||
<CinePlayer
|
||||
className="absolute left-1/2 -translate-x-1/2 bottom-3"
|
||||
isPlaying={isPlaying}
|
||||
onClose={handleCineClose}
|
||||
onPlayPauseChange={isPlaying =>
|
||||
cineService.setCine({
|
||||
id: activeViewportIndex,
|
||||
isPlaying,
|
||||
})
|
||||
}
|
||||
onFrameRateChange={frameRate =>
|
||||
cineService.setCine({
|
||||
id: activeViewportIndex,
|
||||
frameRate,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, areEqual);
|
||||
|
||||
@@ -129,7 +129,7 @@ function CornerstoneViewportOverlay({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-row text-base">
|
||||
<span className="mr-1">W:</span>
|
||||
<span className="ml-1 mr-2 font-light">{windowWidth.toFixed(0)}</span>
|
||||
<span className="mr-1">L:</span>
|
||||
@@ -140,7 +140,7 @@ function CornerstoneViewportOverlay({
|
||||
|
||||
if (activeTools.includes('Zoom')) {
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-row text-base">
|
||||
<span className="mr-1">Zoom:</span>
|
||||
<span className="font-light">{scale.toFixed(2)}x</span>
|
||||
</div>
|
||||
@@ -174,7 +174,7 @@ function CornerstoneViewportOverlay({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-row text-base">
|
||||
<span className="mr-1">I:</span>
|
||||
<span className="font-light">
|
||||
{instanceNumber !== undefined
|
||||
|
||||
+5
@@ -12,6 +12,7 @@ import promptTrackNewSeries from './promptTrackNewSeries';
|
||||
import promptTrackNewStudy from './promptTrackNewStudy';
|
||||
import promptSaveReport from './promptSaveReport';
|
||||
import promptHydrateStructuredReport from './promptHydrateStructuredReport';
|
||||
import hydrateStructuredReport from './hydrateStructuredReport';
|
||||
|
||||
const TrackedMeasurementsContext = React.createContext();
|
||||
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
|
||||
@@ -106,6 +107,10 @@ function TrackedMeasurementsContextProvider(
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}),
|
||||
hydrateStructuredReport: hydrateStructuredReport.bind(null, {
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}),
|
||||
});
|
||||
|
||||
// TODO: IMPROVE
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { hydrateStructuredReport as baseHydrateStructuredReport } from '@ohif/extension-cornerstone-dicom-sr';
|
||||
|
||||
function hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager },
|
||||
ctx,
|
||||
evt
|
||||
) {
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const { viewportIndex, displaySetInstanceUID } = evt;
|
||||
const srDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const hydrationResult = baseHydrateStructuredReport(
|
||||
{ servicesManager, extensionManager },
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
const StudyInstanceUID = hydrationResult.StudyInstanceUID;
|
||||
const SeriesInstanceUIDs = hydrationResult.SeriesInstanceUIDs;
|
||||
|
||||
resolve({
|
||||
displaySetInstanceUID: evt.displaySetInstanceUID,
|
||||
srSeriesInstanceUID: srDisplaySet.SeriesInstanceUID,
|
||||
viewportIndex,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUIDs,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default hydrateStructuredReport;
|
||||
+20
@@ -1,3 +1,4 @@
|
||||
import { hydrateStructuredReport } from '@ohif/extension-cornerstone-dicom-sr';
|
||||
import { assign } from 'xstate';
|
||||
|
||||
const RESPONSE = {
|
||||
@@ -45,6 +46,7 @@ const machineConfiguration = {
|
||||
cond: 'hasNotIgnoredSRSeriesForHydration',
|
||||
},
|
||||
RESTORE_PROMPT_HYDRATE_SR: 'promptHydrateStructuredReport',
|
||||
HYDRATE_SR: 'hydrateStructuredReport',
|
||||
},
|
||||
},
|
||||
promptBeginTracking: {
|
||||
@@ -232,6 +234,24 @@ const machineConfiguration = {
|
||||
},
|
||||
},
|
||||
},
|
||||
hydrateStructuredReport: {
|
||||
invoke: {
|
||||
src: 'hydrateStructuredReport',
|
||||
onDone: [
|
||||
{
|
||||
target: 'tracking',
|
||||
actions: [
|
||||
'setTrackedStudyAndMultipleSeries',
|
||||
'jumpToFirstMeasurementInActiveViewport',
|
||||
'setIsDirtyToClean',
|
||||
],
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
target: 'idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
strict: true,
|
||||
};
|
||||
|
||||
@@ -5,8 +5,6 @@ import OHIF, { utils } from '@ohif/core';
|
||||
import {
|
||||
Notification,
|
||||
ViewportActionBar,
|
||||
useCine,
|
||||
useViewportGrid,
|
||||
useViewportDialog,
|
||||
Tooltip,
|
||||
Icon,
|
||||
@@ -14,7 +12,6 @@ import {
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { eventTarget, Enums } from '@cornerstonejs/core';
|
||||
import { annotation } from '@cornerstonejs/tools';
|
||||
import { useTrackedMeasurements } from './../getContextModule';
|
||||
|
||||
@@ -43,12 +40,9 @@ function TrackedCornerstoneViewport(props) {
|
||||
const displaySet = displaySets[0];
|
||||
|
||||
const [trackedMeasurements] = useTrackedMeasurements();
|
||||
const [{ activeViewportIndex }] = useViewportGrid();
|
||||
const [{ isCineEnabled, cines }, cineService] = useCine();
|
||||
const [viewportDialogState] = useViewportDialog();
|
||||
const [isTracked, setIsTracked] = useState(false);
|
||||
const [trackedMeasurementUID, setTrackedMeasurementUID] = useState(null);
|
||||
const [element, setElement] = useState(null);
|
||||
|
||||
const { trackedSeries } = trackedMeasurements.context;
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
@@ -71,26 +65,6 @@ function TrackedCornerstoneViewport(props) {
|
||||
ManufacturerModelName,
|
||||
} = displaySet.images[0];
|
||||
|
||||
const cineHandler = () => {
|
||||
if (!cines || !cines[viewportIndex] || !element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cine = cines[viewportIndex];
|
||||
const isPlaying = cine.isPlaying || false;
|
||||
const frameRate = cine.frameRate || 24;
|
||||
|
||||
const validFrameRate = Math.max(frameRate, 1);
|
||||
|
||||
if (isPlaying) {
|
||||
cineService.playClip(element, {
|
||||
framesPerSecond: validFrameRate,
|
||||
});
|
||||
} else {
|
||||
cineService.stopClip(element);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isTracked) {
|
||||
annotation.config.style.setViewportToolStyles(viewportId, {
|
||||
@@ -119,50 +93,10 @@ function TrackedCornerstoneViewport(props) {
|
||||
};
|
||||
}, [isTracked]);
|
||||
|
||||
// unmount cleanup
|
||||
useEffect(() => {
|
||||
eventTarget.addEventListener(
|
||||
Enums.Events.STACK_VIEWPORT_NEW_STACK,
|
||||
cineHandler
|
||||
);
|
||||
|
||||
return () => {
|
||||
cineService.setCine({ id: viewportIndex, isPlaying: false });
|
||||
eventTarget.removeEventListener(
|
||||
Enums.Events.STACK_VIEWPORT_NEW_STACK,
|
||||
cineHandler
|
||||
);
|
||||
};
|
||||
}, [element]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cines || !cines[viewportIndex] || !element) {
|
||||
return;
|
||||
}
|
||||
|
||||
cineHandler();
|
||||
|
||||
return () => {
|
||||
if (element && cines?.[viewportIndex]?.isPlaying) {
|
||||
cineService.stopClip(element);
|
||||
}
|
||||
};
|
||||
}, [cines, viewportIndex, cineService, element, cineHandler]);
|
||||
|
||||
if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) {
|
||||
setIsTracked(!isTracked);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnElementEnabled callback which is called after the cornerstoneExtension
|
||||
* has enabled the element. Note: we delegate all the image rendering to
|
||||
* cornerstoneExtension, so we don't need to do anything here regarding
|
||||
* the image rendering, element enabling etc.
|
||||
*/
|
||||
const onElementEnabled = evt => {
|
||||
setElement(evt.detail.element);
|
||||
};
|
||||
|
||||
function switchMeasurement(direction) {
|
||||
const newTrackedMeasurementUID = _getNextMeasurementUID(
|
||||
direction,
|
||||
@@ -188,12 +122,9 @@ function TrackedCornerstoneViewport(props) {
|
||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||
);
|
||||
|
||||
return <Component {...props} onElementEnabled={onElementEnabled} />;
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
||||
const cine = cines[viewportIndex];
|
||||
const isPlaying = (cine && cine.isPlaying) || false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewportActionBar
|
||||
@@ -210,9 +141,7 @@ function TrackedCornerstoneViewport(props) {
|
||||
currentSeries: SeriesNumber, // TODO - switch entire currentSeries to be UID based or actual position based
|
||||
seriesDescription: SeriesDescription,
|
||||
patientInformation: {
|
||||
patientName: PatientName
|
||||
? OHIF.utils.formatPN(PatientName)
|
||||
: '',
|
||||
patientName: PatientName ? OHIF.utils.formatPN(PatientName) : '',
|
||||
patientSex: PatientSex || '',
|
||||
patientAge: PatientAge || '',
|
||||
MRN: PatientID || '',
|
||||
@@ -226,22 +155,6 @@ function TrackedCornerstoneViewport(props) {
|
||||
scanner: ManufacturerModelName || '',
|
||||
},
|
||||
}}
|
||||
showNavArrows={!isCineEnabled}
|
||||
showCine={isCineEnabled}
|
||||
cineProps={{
|
||||
isPlaying,
|
||||
onClose: () => commandsManager.runCommand('toggleCine'),
|
||||
onPlayPauseChange: isPlaying =>
|
||||
cineService.setCine({
|
||||
id: activeViewportIndex,
|
||||
isPlaying,
|
||||
}),
|
||||
onFrameRateChange: frameRate =>
|
||||
cineService.setCine({
|
||||
id: activeViewportIndex,
|
||||
frameRate,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
{/* TODO: Viewport interface to accept stack or layers of content like this? */}
|
||||
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
||||
@@ -281,17 +194,25 @@ function _getNextMeasurementUID(
|
||||
trackedMeasurementId,
|
||||
trackedMeasurements
|
||||
) {
|
||||
const { measurementService } = servicesManager.services;
|
||||
const { measurementService, viewportGridService } = servicesManager.services;
|
||||
const measurements = measurementService.getMeasurements();
|
||||
|
||||
const { activeViewportIndex, viewports } = viewportGridService.getState();
|
||||
const {
|
||||
displaySetInstanceUIDs: activeViewportDisplaySetInstanceUIDs,
|
||||
} = viewports[activeViewportIndex];
|
||||
|
||||
const { trackedSeries } = trackedMeasurements.context;
|
||||
|
||||
// Get the potentially trackable measurements for this series,
|
||||
// Get the potentially trackable measurements for the series of the
|
||||
// active viewport.
|
||||
// The measurements to jump between are the same
|
||||
// regardless if this series is tracked or not.
|
||||
|
||||
const filteredMeasurements = measurements.filter(m =>
|
||||
trackedSeries.includes(m.referenceSeriesUID)
|
||||
const filteredMeasurements = measurements.filter(
|
||||
m =>
|
||||
trackedSeries.includes(m.referenceSeriesUID) &&
|
||||
activeViewportDisplaySetInstanceUIDs.includes(m.displaySetInstanceUID)
|
||||
);
|
||||
|
||||
if (!filteredMeasurements.length) {
|
||||
@@ -329,7 +250,7 @@ function _getNextMeasurementUID(
|
||||
}
|
||||
|
||||
function _getStatusComponent(isTracked) {
|
||||
const trackedIcon = isTracked ? 'tracked' : 'dotted-circle';
|
||||
const trackedIcon = isTracked ? 'status-tracked' : 'status-untracked';
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
@@ -361,7 +282,7 @@ function _getStatusComponent(isTracked) {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Icon name={trackedIcon} className="w-6 text-primary-light" />
|
||||
<Icon name={trackedIcon} className="text-primary-light" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user