Merge remote-tracking branch 'upstream/v3-stable' into v3-stable

This commit is contained in:
Salim Kanoun 2023-03-12 08:01:06 +01:00
commit 00099c0136
378 changed files with 14857 additions and 9552 deletions

View File

@ -2,7 +2,7 @@ version: '3.5'
services: services:
orthanc: orthanc:
image: jodogne/orthanc-plugins:1.5.6 image: jodogne/orthanc-plugins:1.5.7
hostname: orthanc hostname: orthanc
volumes: volumes:
# Config # Config
@ -13,3 +13,11 @@ services:
- '4242:4242' # DICOM - '4242:4242' # DICOM
- '8042:8042' # Web - '8042:8042' # Web
restart: unless-stopped restart: unless-stopped
nginx:
image: nginx:latest
volumes:
- ./config/nginx.conf:/etc/nginx/nginx.conf
ports:
- '80:80' #ngnix proxy
depends_on:
- orthanc

View File

@ -39,7 +39,7 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
const config = { const config = {
mode: isProdBuild ? 'production' : 'development', mode: isProdBuild ? 'production' : 'development',
devtool: isProdBuild ? 'source-map' : 'eval-source-map', devtool: isProdBuild ? 'source-map' : 'cheap-module-source-map',
entry: { entry: {
app: `${SRC_DIR}/index.js`, app: `${SRC_DIR}/index.js`,
}, },

View File

@ -227,7 +227,7 @@ These projects comprise the
### Extensions ### Extensions
This is a list of Extensions maintained by the OHIF Core team. It's possible to This is a list of Extensions maintained by the OHIF Core team. It is possible to
customize and configure these extensions, and you can even create your own. You customize and configure these extensions, and you can even create your own. You
can [read more about extensions here][ohif-extensions]. can [read more about extensions here][ohif-extensions].
@ -271,6 +271,8 @@ Cancer Institute, Informatics Technology for Cancer Research (ITCR) program,
under a under a
[grant to Dr. Gordon Harris at Massachusetts General Hospital (U24 CA199460)](https://projectreporter.nih.gov/project_info_description.cfm?aid=8971104). [grant to Dr. Gordon Harris at Massachusetts General Hospital (U24 CA199460)](https://projectreporter.nih.gov/project_info_description.cfm?aid=8971104).
This project is tested with BrowserStack. Thank you for supporting open source
## License ## License
MIT © [OHIF](https://github.com/OHIF) MIT © [OHIF](https://github.com/OHIF)

View File

@ -36,13 +36,13 @@
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-i18next": "^10.11.0", "react-i18next": "^10.11.0",
"react-router": "^6.3.0", "react-router": "^6.8.1",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.8.1",
"webpack": "^5.50.0", "webpack": "^5.50.0",
"webpack-merge": "^5.7.3" "webpack-merge": "^5.7.3"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.7.6", "@babel/runtime": "^7.20.13",
"react-color": "^2.19.3" "react-color": "^2.19.3"
}, },
"devDependencies": { "devDependencies": {
@ -59,7 +59,7 @@
"babel-eslint": "^8.0.3", "babel-eslint": "^8.0.3",
"babel-loader": "^8.0.0-beta.4", "babel-loader": "^8.0.0-beta.4",
"clean-webpack-plugin": "^4.0.0", "clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.2.0", "copy-webpack-plugin": "^11.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^14.1.0", "dotenv": "^14.1.0",
"eslint": "^5.0.1", "eslint": "^5.0.1",

View File

@ -71,8 +71,8 @@ function _getDisplaySetsFromSeries(
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID; displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
displaySet.getReferenceDisplaySet = () => { displaySet.getReferenceDisplaySet = () => {
const { DisplaySetService } = servicesManager.services; const { displaySetService } = servicesManager.services;
const referencedDisplaySets = DisplaySetService.getDisplaySetsForSeries( const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID displaySet.referencedSeriesInstanceUID
); );
@ -113,9 +113,9 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
// We don't want to fire multiple loads, so we'll wait for the first to finish // We don't want to fire multiple loads, so we'll wait for the first to finish
// and also return the same promise to any other callers. // and also return the same promise to any other callers.
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => { loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
const { SegmentationService } = servicesManager.services; const { segmentationService } = servicesManager.services;
if (_segmentationExistsInCache(segDisplaySet, SegmentationService)) { if (_segmentationExistsInCache(segDisplaySet, segmentationService)) {
return; return;
} }
@ -133,7 +133,7 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
} }
const suppressEvents = true; const suppressEvents = true;
SegmentationService.createSegmentationForSEGDisplaySet( segmentationService.createSegmentationForSEGDisplaySet(
segDisplaySet, segDisplaySet,
null, null,
suppressEvents suppressEvents
@ -175,10 +175,10 @@ async function _loadSegments(extensionManager, segDisplaySet, headers) {
return segments; return segments;
} }
function _segmentationExistsInCache(segDisplaySet, SegmentationService) { function _segmentationExistsInCache(segDisplaySet, segmentationService) {
// This should be abstracted with the CornerstoneCacheService // This should be abstracted with the CornerstoneCacheService
const labelmapVolumeId = segDisplaySet.displaySetInstanceUID; const labelmapVolumeId = segDisplaySet.displaySetInstanceUID;
const segVolume = SegmentationService.getLabelmapVolume(labelmapVolumeId); const segVolume = segmentationService.getLabelmapVolume(labelmapVolumeId);
return segVolume !== undefined; return segVolume !== undefined;
} }

View File

@ -1,6 +1,8 @@
import { id } from './id'; import { id } from './id';
import React from 'react'; import React from 'react';
import { Types } from '@ohif/core';
import getSopClassHandlerModule from './getSopClassHandlerModule'; import getSopClassHandlerModule from './getSopClassHandlerModule';
import PanelSegmentation from './panels/PanelSegmentation'; import PanelSegmentation from './panels/PanelSegmentation';
@ -39,13 +41,13 @@ const extension = {
commandsManager, commandsManager,
configuration = {}, configuration = {},
}) => {}, }) => {},
/** /**
* PanelModule should provide a list of panels that will be available in OHIF * PanelModule should provide a list of panels that will be available in OHIF
* for Modes to consume and render. Each panel is defined by a {name, * for Modes to consume and render. Each panel is defined by a {name,
* iconName, iconLabel, label, component} object. Example of a panel module * iconName, iconLabel, label, component} object. Example of a panel module
* is the StudyBrowserPanel that is provided by the default extension in OHIF. * is the StudyBrowserPanel that is provided by the default extension in OHIF.
*/ */
getPanelModule: ({ servicesManager, commandsManager, extensionManager }) => { getPanelModule: ({ servicesManager, commandsManager, extensionManager }): Types.Panel[] => {
const wrappedPanelSegmentation = () => { const wrappedPanelSegmentation = () => {
return ( return (
<PanelSegmentation <PanelSegmentation

View File

@ -4,18 +4,17 @@ import { SegmentationGroupTable } from '@ohif/ui';
import callInputDialog from './callInputDialog'; import callInputDialog from './callInputDialog';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import callColorPickerDialog from './callColorPickerDialog';
export default function PanelSegmentation({ export default function PanelSegmentation({
servicesManager, servicesManager,
commandsManager, commandsManager,
}) { }) {
const { const {
SegmentationService, segmentationService,
UIDialogService, uiDialogService,
ViewportGridService, viewportGridService,
ToolGroupService, toolGroupService,
CornerstoneViewportService, cornerstoneViewportService,
} = servicesManager.services; } = servicesManager.services;
const { t } = useTranslation('PanelSegmentation'); const { t } = useTranslation('PanelSegmentation');
@ -23,10 +22,10 @@ export default function PanelSegmentation({
const [ const [
initialSegmentationConfigurations, initialSegmentationConfigurations,
setInitialSegmentationConfigurations, setInitialSegmentationConfigurations,
] = useState(SegmentationService.getConfiguration()); ] = useState(segmentationService.getConfiguration());
const [segmentations, setSegmentations] = useState(() => const [segmentations, setSegmentations] = useState(() =>
SegmentationService.getSegmentations() segmentationService.getSegmentations()
); );
const [isMinimized, setIsMinimized] = useState({}); const [isMinimized, setIsMinimized] = useState({});
@ -54,14 +53,14 @@ export default function PanelSegmentation({
useEffect(() => { useEffect(() => {
// ~~ Subscription // ~~ Subscription
const added = SegmentationService.EVENTS.SEGMENTATION_ADDED; const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
const updated = SegmentationService.EVENTS.SEGMENTATION_UPDATED; const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
const removed = SegmentationService.EVENTS.SEGMENTATION_REMOVED; const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
const subscriptions = []; const subscriptions = [];
[added, updated, removed].forEach(evt => { [added, updated, removed].forEach(evt => {
const { unsubscribe } = SegmentationService.subscribe(evt, () => { const { unsubscribe } = segmentationService.subscribe(evt, () => {
const segmentations = SegmentationService.getSegmentations(); const segmentations = segmentationService.getSegmentations();
setSegmentations(segmentations); setSegmentations(segmentations);
}); });
subscriptions.push(unsubscribe); subscriptions.push(unsubscribe);
@ -75,15 +74,15 @@ export default function PanelSegmentation({
}, []); }, []);
const onSegmentationClick = (segmentationId: string) => { const onSegmentationClick = (segmentationId: string) => {
SegmentationService.setActiveSegmentationForToolGroup(segmentationId); segmentationService.setActiveSegmentationForToolGroup(segmentationId);
}; };
const onSegmentationDelete = (segmentationId: string) => { const onSegmentationDelete = (segmentationId: string) => {
SegmentationService.remove(segmentationId); segmentationService.remove(segmentationId);
}; };
const getToolGroupIds = segmentationId => { const getToolGroupIds = segmentationId => {
const toolGroupIds = SegmentationService.getToolGroupIdsWithSegmentation( const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(
segmentationId segmentationId
); );
@ -91,7 +90,7 @@ export default function PanelSegmentation({
}; };
const onSegmentClick = (segmentationId, segmentIndex) => { const onSegmentClick = (segmentationId, segmentIndex) => {
SegmentationService.setActiveSegmentForSegmentation( segmentationService.setActiveSegmentForSegmentation(
segmentationId, segmentationId,
segmentIndex segmentIndex
); );
@ -100,11 +99,11 @@ export default function PanelSegmentation({
toolGroupIds.forEach(toolGroupId => { toolGroupIds.forEach(toolGroupId => {
// const toolGroupId = // const toolGroupId =
SegmentationService.setActiveSegmentationForToolGroup( segmentationService.setActiveSegmentationForToolGroup(
segmentationId, segmentationId,
toolGroupId toolGroupId
); );
SegmentationService.jumpToSegmentCenter( segmentationService.jumpToSegmentCenter(
segmentationId, segmentationId,
segmentIndex, segmentIndex,
toolGroupId toolGroupId
@ -113,17 +112,17 @@ export default function PanelSegmentation({
}; };
const onSegmentEdit = (segmentationId, segmentIndex) => { const onSegmentEdit = (segmentationId, segmentIndex) => {
const segmentation = SegmentationService.getSegmentation(segmentationId); const segmentation = segmentationService.getSegmentation(segmentationId);
const segment = segmentation.segments[segmentIndex]; const segment = segmentation.segments[segmentIndex];
const { label } = segment; const { label } = segment;
callInputDialog(UIDialogService, label, (label, actionId) => { callInputDialog(uiDialogService, label, (label, actionId) => {
if (label === '') { if (label === '') {
return; return;
} }
SegmentationService.setSegmentLabelForSegmentation( segmentationService.setSegmentLabelForSegmentation(
segmentationId, segmentationId,
segmentIndex, segmentIndex,
label label
@ -132,15 +131,15 @@ export default function PanelSegmentation({
}; };
const onSegmentationEdit = segmentationId => { const onSegmentationEdit = segmentationId => {
const segmentation = SegmentationService.getSegmentation(segmentationId); const segmentation = segmentationService.getSegmentation(segmentationId);
const { label } = segmentation; const { label } = segmentation;
callInputDialog(UIDialogService, label, (label, actionId) => { callInputDialog(uiDialogService, label, (label, actionId) => {
if (label === '') { if (label === '') {
return; return;
} }
SegmentationService.addOrUpdateSegmentation( segmentationService.addOrUpdateSegmentation(
{ {
id: segmentationId, id: segmentationId,
label, label,
@ -157,7 +156,7 @@ export default function PanelSegmentation({
}; };
const onSegmentDelete = (segmentationId, segmentIndex) => { const onSegmentDelete = (segmentationId, segmentIndex) => {
// SegmentationService.removeSegmentFromSegmentation( // segmentationService.removeSegmentFromSegmentation(
// segmentationId, // segmentationId,
// segmentIndex // segmentIndex
// ); // );
@ -165,14 +164,14 @@ export default function PanelSegmentation({
}; };
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => { const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
const segmentation = SegmentationService.getSegmentation(segmentationId); const segmentation = segmentationService.getSegmentation(segmentationId);
const segmentInfo = segmentation.segments[segmentIndex]; const segmentInfo = segmentation.segments[segmentIndex];
const isVisible = !segmentInfo.isVisible; const isVisible = !segmentInfo.isVisible;
const toolGroupIds = getToolGroupIds(segmentationId); const toolGroupIds = getToolGroupIds(segmentationId);
// Todo: right now we apply the visibility to all tool groups // Todo: right now we apply the visibility to all tool groups
toolGroupIds.forEach(toolGroupId => { toolGroupIds.forEach(toolGroupId => {
SegmentationService.setSegmentVisibility( segmentationService.setSegmentVisibility(
segmentationId, segmentationId,
segmentIndex, segmentIndex,
isVisible, isVisible,
@ -182,21 +181,21 @@ export default function PanelSegmentation({
}; };
const onToggleSegmentationVisibility = segmentationId => { const onToggleSegmentationVisibility = segmentationId => {
SegmentationService.toggleSegmentationVisibility(segmentationId); segmentationService.toggleSegmentationVisibility(segmentationId);
}; };
const setSegmentationConfiguration = useCallback( const setSegmentationConfiguration = useCallback(
(segmentationId, key, value) => { (segmentationId, key, value) => {
SegmentationService.setConfiguration({ segmentationService.setConfiguration({
segmentationId, segmentationId,
[key]: value, [key]: value,
}); });
}, },
[SegmentationService] [segmentationService]
); );
return ( return (
<div className="flex flex-col justify-between mt-1"> <div className="flex flex-col flex-auto min-h-0 justify-between mt-1">
{/* show segmentation table */} {/* show segmentation table */}
{segmentations?.length ? ( {segmentations?.length ? (
<SegmentationGroupTable <SegmentationGroupTable
@ -204,7 +203,7 @@ export default function PanelSegmentation({
showAddSegmentation={false} showAddSegmentation={false}
segmentations={segmentations} segmentations={segmentations}
isMinimized={isMinimized} isMinimized={isMinimized}
activeSegmentationId={selectedSegmentationId} activeSegmentationId={selectedSegmentationId || ''}
onSegmentationClick={onSegmentationClick} onSegmentationClick={onSegmentationClick}
onSegmentationDelete={onSegmentationDelete} onSegmentationDelete={onSegmentationDelete}
onSegmentationEdit={onSegmentationEdit} onSegmentationEdit={onSegmentationEdit}
@ -280,7 +279,7 @@ PanelSegmentation.propTypes = {
}), }),
servicesManager: PropTypes.shape({ servicesManager: PropTypes.shape({
services: PropTypes.shape({ services: PropTypes.shape({
SegmentationService: PropTypes.shape({ segmentationService: PropTypes.shape({
getSegmentation: PropTypes.func.isRequired, getSegmentation: PropTypes.func.isRequired,
getSegmentations: PropTypes.func.isRequired, getSegmentations: PropTypes.func.isRequired,
toggleSegmentationVisibility: PropTypes.func.isRequired, toggleSegmentationVisibility: PropTypes.func.isRequired,

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Input, Dialog } from '@ohif/ui'; import { Input, Dialog } from '@ohif/ui';
function callInputDialog(UIDialogService, label, callback) { function callInputDialog(uiDialogService, label, callback) {
const dialogId = 'enter-segment-label'; const dialogId = 'enter-segment-label';
const onSubmitHandler = ({ action, value }) => { const onSubmitHandler = ({ action, value }) => {
@ -13,11 +13,11 @@ function callInputDialog(UIDialogService, label, callback) {
callback('', action.id); callback('', action.id);
break; break;
} }
UIDialogService.dismiss({ id: dialogId }); uiDialogService.dismiss({ id: dialogId });
}; };
if (UIDialogService) { if (uiDialogService) {
UIDialogService.create({ uiDialogService.create({
id: dialogId, id: dialogId,
centralize: true, centralize: true,
isDraggable: false, isDraggable: false,
@ -27,7 +27,7 @@ function callInputDialog(UIDialogService, label, callback) {
title: 'Enter Segment Label', title: 'Enter Segment Label',
value: { label }, value: { label },
noCloseButton: true, noCloseButton: true,
onClose: () => UIDialogService.dismiss({ id: dialogId }), onClose: () => uiDialogService.dismiss({ id: dialogId }),
actions: [ actions: [
{ id: 'cancel', text: 'Cancel', type: 'primary' }, { id: 'cancel', text: 'Cancel', type: 'primary' },
{ id: 'save', text: 'Confirm', type: 'secondary' }, { id: 'save', text: 'Confirm', type: 'secondary' },

View File

@ -1,13 +1,12 @@
async function _hydrateSEGDisplaySet({ async function _hydrateSEGDisplaySet({
segDisplaySet, segDisplaySet,
viewportIndex, viewportIndex,
toolGroupId,
servicesManager, servicesManager,
}) { }) {
const { const {
SegmentationService, segmentationService,
HangingProtocolService, hangingProtocolService,
ViewportGridService, viewportGridService,
} = servicesManager.services; } = servicesManager.services;
const displaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID; const displaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
@ -17,22 +16,22 @@ async function _hydrateSEGDisplaySet({
// We need the hydration to notify panels about the new segmentation added // We need the hydration to notify panels about the new segmentation added
const suppressEvents = false; const suppressEvents = false;
segmentationId = await SegmentationService.createSegmentationForSEGDisplaySet( segmentationId = await segmentationService.createSegmentationForSEGDisplaySet(
segDisplaySet, segDisplaySet,
segmentationId, segmentationId,
suppressEvents suppressEvents
); );
SegmentationService.hydrateSegmentation(segDisplaySet.displaySetInstanceUID); segmentationService.hydrateSegmentation(segDisplaySet.displaySetInstanceUID);
const { viewports } = ViewportGridService.getState(); const { viewports } = viewportGridService.getState();
const updatedViewports = HangingProtocolService.getViewportsRequireUpdate( const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportIndex, viewportIndex,
displaySetInstanceUID displaySetInstanceUID
); );
ViewportGridService.setDisplaySetsForViewports(updatedViewports); viewportGridService.setDisplaySetsForViewports(updatedViewports);
// Todo: fix this after we have a better way for stack viewport segmentations // Todo: fix this after we have a better way for stack viewport segmentations
@ -45,18 +44,16 @@ async function _hydrateSEGDisplaySet({
return; return;
} }
const shouldDisplaySeg = SegmentationService.shouldRenderSegmentation( const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
viewport.displaySetInstanceUIDs, viewport.displaySetInstanceUIDs,
segDisplaySet.displaySetInstanceUID segDisplaySet.displaySetInstanceUID
); );
if (shouldDisplaySeg) { if (shouldDisplaySeg) {
ViewportGridService.setDisplaySetsForViewport({ viewportGridService.setDisplaySetsForViewport({
viewportIndex: index, viewportIndex: index,
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs, displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
viewportOptions: { viewportOptions: {
viewportType: 'volume',
toolGroupId,
initialImageOptions: { initialImageOptions: {
preset: 'middle', preset: 'middle',
}, },

View File

@ -1,5 +1,5 @@
function createSEGToolGroupAndAddTools( function createSEGToolGroupAndAddTools(
ToolGroupService, toolGroupService,
toolGroupId, toolGroupId,
extensionManager extensionManager
) { ) {
@ -28,7 +28,7 @@ function createSEGToolGroupAndAddTools(
enabled: [{ toolName: toolNames.SegmentationDisplay }], enabled: [{ toolName: toolNames.SegmentationDisplay }],
}; };
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {}); return toolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {});
} }
export default createSEGToolGroupAndAddTools; export default createSEGToolGroupAndAddTools;

View File

@ -10,13 +10,12 @@ function promptHydrateSEG({
servicesManager, servicesManager,
segDisplaySet, segDisplaySet,
viewportIndex, viewportIndex,
toolGroupId = 'default',
}) { }) {
const { UIViewportDialogService } = servicesManager.services; const { uiViewportDialogService } = servicesManager.services;
return new Promise(async function(resolve, reject) { return new Promise(async function(resolve, reject) {
const promptResult = await _askHydrate( const promptResult = await _askHydrate(
UIViewportDialogService, uiViewportDialogService,
viewportIndex viewportIndex
); );
@ -24,7 +23,6 @@ function promptHydrateSEG({
const isHydrated = await hydrateSEGDisplaySet({ const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet, segDisplaySet,
viewportIndex, viewportIndex,
toolGroupId,
servicesManager, servicesManager,
}); });
@ -33,7 +31,7 @@ function promptHydrateSEG({
}); });
} }
function _askHydrate(UIViewportDialogService, viewportIndex) { function _askHydrate(uiViewportDialogService, viewportIndex) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
const message = 'Do you want to open this Segmentation?'; const message = 'Do you want to open this Segmentation?';
const actions = [ const actions = [
@ -49,18 +47,18 @@ function _askHydrate(UIViewportDialogService, viewportIndex) {
}, },
]; ];
const onSubmit = result => { const onSubmit = result => {
UIViewportDialogService.hide(); uiViewportDialogService.hide();
resolve(result); resolve(result);
}; };
UIViewportDialogService.show({ uiViewportDialogService.show({
viewportIndex, viewportIndex,
type: 'info', type: 'info',
message, message,
actions, actions,
onSubmit, onSubmit,
onOutsideClick: () => { onOutsideClick: () => {
UIViewportDialogService.hide(); uiViewportDialogService.hide();
resolve(RESPONSE.CANCEL); resolve(RESPONSE.CANCEL);
}, },
}); });

View File

@ -1,19 +1,13 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types'; 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 OHIF, { utils } from '@ohif/core';
import { import {
Notification, LoadingIndicatorProgress, Notification, useViewportDialog, useViewportGrid, ViewportActionBar
ViewportActionBar,
useViewportGrid,
useViewportDialog,
LoadingIndicatorProgress,
} from '@ohif/ui'; } from '@ohif/ui';
import { useTranslation } from 'react-i18next';
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup'; import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import _hydrateSEGDisplaySet from '../utils/_hydrateSEG';
import promptHydrateSEG from '../utils/promptHydrateSEG'; import promptHydrateSEG from '../utils/promptHydrateSEG';
import hydrateSEGDisplaySet from '../utils/_hydrateSEG';
import _getStatusComponent from './_getStatusComponent'; import _getStatusComponent from './_getStatusComponent';
const { formatDate } = utils; const { formatDate } = utils;
@ -33,10 +27,10 @@ function OHIFCornerstoneSEGViewport(props) {
const { t } = useTranslation('SEGViewport'); const { t } = useTranslation('SEGViewport');
const { const {
DisplaySetService, displaySetService,
ToolGroupService, toolGroupService,
SegmentationService, segmentationService,
UINotificationService, uiNotificationService,
} = servicesManager.services; } = servicesManager.services;
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportIndex}`; const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportIndex}`;
@ -127,7 +121,7 @@ function OHIFCornerstoneSEGViewport(props) {
direction => { direction => {
direction = direction === 'left' ? -1 : 1; direction = direction === 'left' ? -1 : 1;
const segmentationId = segDisplaySet.displaySetInstanceUID; const segmentationId = segDisplaySet.displaySetInstanceUID;
const segmentation = SegmentationService.getSegmentation(segmentationId); const segmentation = segmentationService.getSegmentation(segmentationId);
const { segments } = segmentation; const { segments } = segmentation;
@ -141,7 +135,7 @@ function OHIFCornerstoneSEGViewport(props) {
newSelectedSegmentIndex = numberOfSegments - 1; newSelectedSegmentIndex = numberOfSegments - 1;
} }
SegmentationService.jumpToSegmentCenter( segmentationService.jumpToSegmentCenter(
segmentationId, segmentationId,
newSelectedSegmentIndex, newSelectedSegmentIndex,
toolGroupId toolGroupId
@ -168,8 +162,8 @@ function OHIFCornerstoneSEGViewport(props) {
}, [servicesManager, viewportIndex, segDisplaySet, segIsLoading]); }, [servicesManager, viewportIndex, segDisplaySet, segIsLoading]);
useEffect(() => { useEffect(() => {
const { unsubscribe } = SegmentationService.subscribe( const { unsubscribe } = segmentationService.subscribe(
SegmentationService.EVENTS.SEGMENTATION_PIXEL_DATA_CREATED, segmentationService.EVENTS.SEGMENTATION_PIXEL_DATA_CREATED,
evt => { evt => {
if ( if (
evt.segDisplaySet.displaySetInstanceUID === evt.segDisplaySet.displaySetInstanceUID ===
@ -179,7 +173,7 @@ function OHIFCornerstoneSEGViewport(props) {
} }
if (evt.overlappingSegments) { if (evt.overlappingSegments) {
UINotificationService.show({ uiNotificationService.show({
title: 'Overlapping Segments', title: 'Overlapping Segments',
message: message:
'Overlapping segments detected which is not currently supported', 'Overlapping segments detected which is not currently supported',
@ -195,8 +189,8 @@ function OHIFCornerstoneSEGViewport(props) {
}, [segDisplaySet]); }, [segDisplaySet]);
useEffect(() => { useEffect(() => {
const { unsubscribe } = SegmentationService.subscribe( const { unsubscribe } = segmentationService.subscribe(
SegmentationService.EVENTS.SEGMENT_PIXEL_DATA_CREATED, segmentationService.EVENTS.SEGMENT_PIXEL_DATA_CREATED,
({ segmentIndex, numSegments }) => { ({ segmentIndex, numSegments }) => {
setProcessingProgress({ setProcessingProgress({
segmentIndex, segmentIndex,
@ -214,8 +208,8 @@ function OHIFCornerstoneSEGViewport(props) {
Cleanup the SEG viewport when the viewport is destroyed Cleanup the SEG viewport when the viewport is destroyed
*/ */
useEffect(() => { useEffect(() => {
const onDisplaySetsRemovedSubscription = DisplaySetService.subscribe( const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_REMOVED, displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
({ displaySetInstanceUIDs }) => { ({ displaySetInstanceUIDs }) => {
const activeViewport = viewports[activeViewportIndex]; const activeViewport = viewports[activeViewportIndex];
if ( if (
@ -235,14 +229,16 @@ function OHIFCornerstoneSEGViewport(props) {
}, []); }, []);
useEffect(() => { useEffect(() => {
let toolGroup = ToolGroupService.getToolGroup(toolGroupId); let toolGroup = toolGroupService.getToolGroup(toolGroupId);
if (toolGroup) { if (toolGroup) {
return; return;
} }
// This creates a custom tool group which has the lifetime of this view
// only, and does NOT interfere with currently displayed segmentations.
toolGroup = createSEGToolGroupAndAddTools( toolGroup = createSEGToolGroupAndAddTools(
ToolGroupService, toolGroupService,
toolGroupId, toolGroupId,
extensionManager extensionManager
); );
@ -251,11 +247,12 @@ function OHIFCornerstoneSEGViewport(props) {
return () => { return () => {
// remove the segmentation representations if seg displayset changed // remove the segmentation representations if seg displayset changed
SegmentationService.removeSegmentationRepresentationFromToolGroup( segmentationService.removeSegmentationRepresentationFromToolGroup(
toolGroupId toolGroupId
); );
ToolGroupService.destroyToolGroup(toolGroupId); // Only destroy the viewport specific implementation
toolGroupService.destroyToolGroup(toolGroupId);
}; };
}, []); }, []);
@ -264,7 +261,7 @@ function OHIFCornerstoneSEGViewport(props) {
return () => { return () => {
// remove the segmentation representations if seg displayset changed // remove the segmentation representations if seg displayset changed
SegmentationService.removeSegmentationRepresentationFromToolGroup( segmentationService.removeSegmentationRepresentationFromToolGroup(
toolGroupId toolGroupId
); );
referencedDisplaySetRef.current = null; referencedDisplaySetRef.current = null;
@ -304,19 +301,16 @@ function OHIFCornerstoneSEGViewport(props) {
StudyDate, StudyDate,
SeriesDescription, SeriesDescription,
SpacingBetweenSlices, SpacingBetweenSlices,
SeriesNumber,
} = referencedDisplaySetRef.current.metadata; } = referencedDisplaySetRef.current.metadata;
const onPillClick = () => { const onStatusClick = async () => {
promptHydrateSEG({ const isHydrated = await hydrateSEGDisplaySet({
servicesManager,
viewportIndex,
segDisplaySet, segDisplaySet,
}).then(isHydrated => { viewportIndex,
if (isHydrated) { servicesManager,
setIsHydrated(true);
}
}); });
setIsHydrated(isHydrated);
}; };
return ( return (
@ -330,14 +324,13 @@ function OHIFCornerstoneSEGViewport(props) {
getStatusComponent={() => { getStatusComponent={() => {
return _getStatusComponent({ return _getStatusComponent({
isHydrated, isHydrated,
onPillClick, onStatusClick,
}); });
}} }}
studyData={{ studyData={{
label: viewportLabel, label: viewportLabel,
useAltStyling: true, useAltStyling: true,
studyDate: formatDate(StudyDate), studyDate: formatDate(StudyDate),
currentSeries: SeriesNumber,
seriesDescription: `SEG Viewport ${SeriesDescription}`, seriesDescription: `SEG Viewport ${SeriesDescription}`,
patientInformation: { patientInformation: {
patientName: PatientName patientName: PatientName

View File

@ -1,92 +1,56 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import { useTranslation } from 'react-i18next';
import { Icon, Tooltip } from '@ohif/ui'; 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 ToolTipMessage = null;
let StatusIcon = null; let StatusIcon = null;
const {t} = useTranslation("Common");
const loadStr = t("LOAD");
switch (isHydrated) { switch (isHydrated) {
case true: case true:
StatusIcon = () => ( StatusIcon = () => <Icon name="status-alert" />;
<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>
);
ToolTipMessage = () => ( ToolTipMessage = () => (
<div>This Segmentation is loaded in the segmentation panel</div> <div>This Segmentation is loaded in the segmentation panel</div>
); );
break; break;
case false: case false:
StatusIcon = () => ( StatusIcon = () => <Icon name="status-untracked" />;
<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>
);
ToolTipMessage = () => <div>Click to load segmentation.</div>; ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
} }
const StatusPill = () => ( const StatusArea = () => (
<div <div className="flex h-6 leading-6 cursor-default text-sm text-white">
className={classNames( <div className="min-w-[45px] flex items-center p-1 rounded-l-xl rounded-r bg-customgray-100">
'group relative flex items-center justify-center px-8 rounded-full cursor-default bg-customgreen-100', <StatusIcon />
{ <span className="ml-1">SEG</span>
'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
</div> </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> </div>
); );
return ( return (
<> <>
{ToolTipMessage && ( {ToolTipMessage && (
<Tooltip content={<ToolTipMessage />} position="bottom-left"> <Tooltip content={<ToolTipMessage />} position="bottom-left">
<StatusPill /> <StatusArea />
</Tooltip> </Tooltip>
)} )}
{!ToolTipMessage && <StatusPill />} {!ToolTipMessage && <StatusArea />}
</> </>
); );
} }

View File

@ -36,16 +36,17 @@
"@ohif/extension-cornerstone": "^3.0.0", "@ohif/extension-cornerstone": "^3.0.0",
"@ohif/extension-measurement-tracking": "^3.0.0", "@ohif/extension-measurement-tracking": "^3.0.0",
"@ohif/ui": "^2.0.0", "@ohif/ui": "^2.0.0",
"dcmjs": "^0.28.3", "dcmjs": "^0.29.4",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^17.0.2" "react": "^17.0.2"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.16.3", "@babel/runtime": "^7.20.13",
"classnames": "^2.2.6", "classnames": "^2.3.2",
"@cornerstonejs/core": "^0.22.3", "@cornerstonejs/adapters": "^0.4.1",
"@cornerstonejs/tools": "^0.30.6" "@cornerstonejs/core": "^0.33.2",
"@cornerstonejs/tools": "^0.50.2"
} }
} }

View File

@ -2,10 +2,11 @@ import { metaData, utilities } from '@cornerstonejs/core';
import OHIF from '@ohif/core'; import OHIF from '@ohif/core';
import dcmjs from 'dcmjs'; import dcmjs from 'dcmjs';
import { adaptersSR } from '@cornerstonejs/adapters';
import getFilteredCornerstoneToolState from './utils/getFilteredCornerstoneToolState'; import getFilteredCornerstoneToolState from './utils/getFilteredCornerstoneToolState';
const { MeasurementReport } = dcmjs.adapters.Cornerstone3D; const { MeasurementReport } = adaptersSR.Cornerstone3D;
const { log } = OHIF; const { log } = OHIF;
/** /**
@ -29,13 +30,17 @@ const _generateReport = (
const report = MeasurementReport.generateReport( const report = MeasurementReport.generateReport(
filteredToolState, filteredToolState,
metaData, metaData,
utilities.worldToImageCoords utilities.worldToImageCoords,
options
); );
const { dataset } = report; const { dataset } = report;
// Add in top level series options // Set the default character set as UTF-8
Object.assign(dataset, options); // https://dicom.innolitics.com/ciods/nm-image/sop-common/00080005
if (typeof dataset.SpecificCharacterSet === 'undefined') {
dataset.SpecificCharacterSet = 'ISO_IR 192';
}
return dataset; return dataset;
}; };
@ -82,7 +87,7 @@ const commandsModule = ({}) => {
additionalFindingTypes, additionalFindingTypes,
options = {}, options = {},
}) => { }) => {
// TODO -> Eventually use the measurements directly and not the dcmjs adapter, // Use the @cornerstonejs adapter for converting to/from DICOM
// But it is good enough for now whilst we only have cornerstone as a datasource. // But it is good enough for now whilst we only have cornerstone as a datasource.
log.info('[DICOMSR] storeMeasurements'); log.info('[DICOMSR] storeMeasurements');

View File

@ -2,9 +2,9 @@ import { SOPClassHandlerName, SOPClassHandlerId } from './id';
import { utils, classes } from '@ohif/core'; import { utils, classes } from '@ohif/core';
import addMeasurement from './utils/addMeasurement'; import addMeasurement from './utils/addMeasurement';
import isRehydratable from './utils/isRehydratable'; import isRehydratable from './utils/isRehydratable';
import { adapters } from 'dcmjs'; import { adaptersSR } from '@cornerstonejs/adapters';
const { CodeScheme: Cornerstone3DCodeScheme } = adapters.Cornerstone3D; const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
const { ImageSet, MetadataProvider: metadataProvider } = classes; const { ImageSet, MetadataProvider: metadataProvider } = classes;
// TODO -> // TODO ->
@ -51,13 +51,17 @@ const RELATIONSHIP_TYPE = {
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT'; const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
/** /**
* Basic SOPClassHandler: * DICOM SR SOP Class Handler
* - For all Image types that are stackable, create * For all referenced images in the TID 1500/300 sections, add an image to the
* a displaySet with a stack of images * display (this is TODO - it is not the actual behaviour below unfortunately)
* *
* @param {Array} sopClassHandlerModules List of SOP Class Modules * This will only display and rehydrate the latest DICOM SR in the given series
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created * It would be possible to add the ability to view older series rehydrations
* @returns {Array} The list of display sets created for the given series object * in the future.
*
* @param instances is a set of instances all from the same series
* @param servicesManager is the services that can be used for creating
* @returns The list of display sets created for the given instances object
*/ */
function _getDisplaySetsFromSeries( function _getDisplaySetsFromSeries(
instances, instances,
@ -69,7 +73,8 @@ function _getDisplaySetsFromSeries(
throw new Error('No instances were provided'); throw new Error('No instances were provided');
} }
const instance = instances[0]; utils.sortStudyInstances(instances);
const instance = instances[instances.length - 1];
const { const {
StudyInstanceUID, StudyInstanceUID,
@ -85,10 +90,10 @@ function _getDisplaySetsFromSeries(
if ( if (
!ConceptNameCodeSequence || !ConceptNameCodeSequence ||
ConceptNameCodeSequence.CodeValue !== ConceptNameCodeSequence.CodeValue !==
CodeNameCodeSequenceValues.ImagingMeasurementReport CodeNameCodeSequenceValues.ImagingMeasurementReport
) { ) {
console.warn( console.log(
'Only support Imaging Measurement Report SRs (TID1500) for now' 'Only support Imaging Measurement Report SRs (TID1500) for this renderer.'
); );
return []; return [];
} }
@ -105,6 +110,9 @@ function _getDisplaySetsFromSeries(
StudyInstanceUID, StudyInstanceUID,
SOPClassHandlerId, SOPClassHandlerId,
SOPClassUID, SOPClassUID,
instances,
// Others is a historical value used for instances which is deprecated and will be removed
others: instances,
referencedImages: null, referencedImages: null,
measurements: null, measurements: null,
isDerivedDisplaySet: true, isDerivedDisplaySet: true,
@ -119,7 +127,7 @@ function _getDisplaySetsFromSeries(
} }
function _load(displaySet, servicesManager, extensionManager) { function _load(displaySet, servicesManager, extensionManager) {
const { DisplaySetService, MeasurementService } = servicesManager.services; const { displaySetService, measurementService } = servicesManager.services;
const dataSources = extensionManager.getDataSources(); const dataSources = extensionManager.getDataSources();
const dataSource = dataSources[0]; const dataSource = dataSources[0];
@ -128,7 +136,7 @@ function _load(displaySet, servicesManager, extensionManager) {
displaySet.referencedImages = _getReferencedImagesList(ContentSequence); displaySet.referencedImages = _getReferencedImagesList(ContentSequence);
displaySet.measurements = _getMeasurements(ContentSequence); displaySet.measurements = _getMeasurements(ContentSequence);
const mappings = MeasurementService.getSourceMappings( const mappings = measurementService.getSourceMappings(
CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION CORNERSTONE_3D_TOOLS_SOURCE_VERSION
); );
@ -138,7 +146,7 @@ function _load(displaySet, servicesManager, extensionManager) {
displaySet.isLoaded = true; displaySet.isLoaded = true;
// Check currently added displaySets and add measurements if the sources exist. // Check currently added displaySets and add measurements if the sources exist.
DisplaySetService.activeDisplaySets.forEach(activeDisplaySet => { displaySetService.activeDisplaySets.forEach(activeDisplaySet => {
_checkIfCanAddMeasurementsToDisplaySet( _checkIfCanAddMeasurementsToDisplaySet(
displaySet, displaySet,
activeDisplaySet, activeDisplaySet,
@ -147,8 +155,8 @@ function _load(displaySet, servicesManager, extensionManager) {
}); });
// Subscribe to new displaySets as the source may come in after. // Subscribe to new displaySets as the source may come in after.
DisplaySetService.subscribe( displaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED, displaySetService.EVENTS.DISPLAY_SETS_ADDED,
data => { data => {
const { displaySetsAdded } = data; const { displaySetsAdded } = data;
// If there are still some measurements that have not yet been loaded into cornerstone, // If there are still some measurements that have not yet been loaded into cornerstone,
@ -313,13 +321,13 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
MeasurementGroups MeasurementGroups
); );
let measurements = []; const measurements = [];
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach( Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
trackingUniqueIdentifier => { trackingUniqueIdentifier => {
const mergedContentSequence = const mergedContentSequence =
mergedContentSequencesByTrackingUniqueIdentifiers[ mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier trackingUniqueIdentifier
]; ];
const measurement = _processMeasurement(mergedContentSequence); const measurement = _processMeasurement(mergedContentSequence);
@ -359,7 +367,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(
if ( if (
mergedContentSequencesByTrackingUniqueIdentifiers[ mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier trackingUniqueIdentifier
] === undefined ] === undefined
) { ) {
// Add the full ContentSequence // Add the full ContentSequence
@ -474,9 +482,9 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
const FindingSites = mergedContentSequence.filter( const FindingSites = mergedContentSequence.filter(
item => item =>
item.ConceptNameCodeSequence.CodingSchemeDesignator === item.ConceptNameCodeSequence.CodingSchemeDesignator ===
CodingSchemeDesignators.SRT && CodingSchemeDesignators.SRT &&
item.ConceptNameCodeSequence.CodeValue === item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.FindingSite CodeNameCodeSequenceValues.FindingSite
); );
const measurement = { const measurement = {
@ -493,7 +501,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
Finding.ConceptCodeSequence.CodingSchemeDesignator Finding.ConceptCodeSequence.CodingSchemeDesignator
) && ) &&
Finding.ConceptCodeSequence.CodeValue === Finding.ConceptCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.CornerstoneFreeText CodeNameCodeSequenceValues.CornerstoneFreeText
) { ) {
measurement.labels.push({ measurement.labels.push({
label: CORNERSTONE_FREETEXT_CODE_VALUE, label: CORNERSTONE_FREETEXT_CODE_VALUE,
@ -509,7 +517,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
FindingSite.ConceptCodeSequence.CodingSchemeDesignator FindingSite.ConceptCodeSequence.CodingSchemeDesignator
) && ) &&
FindingSite.ConceptCodeSequence.CodeValue === FindingSite.ConceptCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.CornerstoneFreeText CodeNameCodeSequenceValues.CornerstoneFreeText
); );
if (cornerstoneFreeTextFindingSite) { if (cornerstoneFreeTextFindingSite) {
@ -597,7 +605,7 @@ function _getLabelFromMeasuredValueSequence(
const { CodeValue } = MeasurementUnitsCodeSequence; const { CodeValue } = MeasurementUnitsCodeSequence;
const formatedNumericValue = NumericValue const formatedNumericValue = NumericValue
? Number(NumericValue).toFixed(1) ? Number(NumericValue).toFixed(2)
: ''; : '';
return { return {

View File

@ -5,6 +5,7 @@ import commandsModule from './commandsModule';
import init from './init'; import init from './init';
import { id } from './id.js'; import { id } from './id.js';
import toolNames from './tools/toolNames'; import toolNames from './tools/toolNames';
import hydrateStructuredReport from './utils/hydrateStructuredReport';
const Component = React.lazy(() => { const Component = React.lazy(() => {
return import( return import(
@ -74,3 +75,4 @@ const dicomSRExtension = {
}; };
export default dicomSRExtension; export default dicomSRExtension;
export { hydrateStructuredReport };

View File

@ -1,29 +0,0 @@
import { addTool, annotation } from '@cornerstonejs/tools';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import SRLengthTool from './tools/tools/SRLength';
import SRBidirectionalTool from './tools/tools/SRBidirectional';
import SREllipticalROITool from './tools/tools/SREllipticalROI';
import SRArrowAnnotateTool from './tools/tools/SRArrowAnnotate';
/**
* @param {object} configuration
*/
export default function init({ configuration = {} }) {
addTool(DICOMSRDisplayTool);
addTool(SRLengthTool);
addTool(SRBidirectionalTool);
addTool(SREllipticalROITool);
addTool(SRArrowAnnotateTool);
// Modify annotation tools to use dashed lines on SR
const dashedLine = {
lineDash: '4,4',
};
annotation.config.style.setToolGroupToolStyles('SRToolGroup', {
[SRLengthTool.toolName]: dashedLine,
[SRBidirectionalTool.toolName]: dashedLine,
[SREllipticalROITool.toolName]: dashedLine,
[SRArrowAnnotateTool.toolName]: dashedLine,
global: {},
});
}

View File

@ -0,0 +1,49 @@
import {
addTool,
AngleTool,
annotation,
ArrowAnnotateTool,
BidirectionalTool,
CobbAngleTool,
EllipticalROITool,
LengthTool,
PlanarFreehandROITool,
} from '@cornerstonejs/tools';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import addToolInstance from './utils/addToolInstance';
import { Types } from '@ohif/core';
import toolNames from './tools/toolNames';
/**
* @param {object} configuration
*/
export default function init({
configuration = {},
}: Types.Extensions.ExtensionParams): void {
addTool(DICOMSRDisplayTool);
addToolInstance(toolNames.SRLength, LengthTool, {});
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
addToolInstance(toolNames.SREllipticalROI, EllipticalROITool);
addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool);
addToolInstance(toolNames.SRAngle, AngleTool);
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
addToolInstance(toolNames.SRCobbAngle, CobbAngleTool);
// TODO - fix the rehydration of Freehand, as it throws an exception
// on a missing polyline. The fix is probably in CS3D
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
// Modify annotation tools to use dashed lines on SR
const dashedLine = {
lineDash: '4,4',
};
annotation.config.style.setToolGroupToolStyles('SRToolGroup', {
SRLength: dashedLine,
SRBidirectional: dashedLine,
SREllipticalROI: dashedLine,
SRArrowAnnotate: dashedLine,
SRCobbAngle: dashedLine,
SRAngle: dashedLine,
SRPlanarFreehandROI: dashedLine,
global: {},
});
}

View File

@ -1,8 +1,8 @@
import { SOPClassHandlerId } from './id'; import { SOPClassHandlerId } from './id';
export default function onModeEnter({ servicesManager }) { export default function onModeEnter({ servicesManager }) {
const { DisplaySetService } = servicesManager.services; const { displaySetService } = servicesManager.services;
const displaySetCache = DisplaySetService.getDisplaySetCache(); const displaySetCache = displaySetService.getDisplaySetCache();
const srDisplaySets = displaySetCache.filter( const srDisplaySets = displaySetCache.filter(
ds => ds.SOPClassHandlerId === SOPClassHandlerId ds => ds.SOPClassHandlerId === SOPClassHandlerId

View File

@ -48,8 +48,8 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
const { element } = viewport; const { element } = viewport;
let annotations = annotation.state.getAnnotations( let annotations = annotation.state.getAnnotations(
element, this.getToolName(),
this.getToolName() element
); );
// Todo: We don't need this anymore, filtering happens in triggerAnnotationRender // Todo: We don't need this anymore, filtering happens in triggerAnnotationRender
@ -121,28 +121,23 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
const renderableDataForGraphicType = renderableData[GraphicType]; const renderableDataForGraphicType = renderableData[GraphicType];
let renderMethod; let renderMethod;
let renderTextBox;
let canvasCoordinatesAdapter; let canvasCoordinatesAdapter;
switch (GraphicType) { switch (GraphicType) {
case SCOORD_TYPES.POINT: case SCOORD_TYPES.POINT:
renderMethod = this.renderPoint; renderMethod = this.renderPoint;
renderTextBox = this.renderTextBox;
break; break;
case SCOORD_TYPES.MULTIPOINT: case SCOORD_TYPES.MULTIPOINT:
renderMethod = this.renderMultipoint; renderMethod = this.renderMultipoint;
renderTextBox = this.renderTextBox;
break; break;
case SCOORD_TYPES.POLYLINE: case SCOORD_TYPES.POLYLINE:
renderMethod = this.renderPolyLine; renderMethod = this.renderPolyLine;
break; break;
case SCOORD_TYPES.CIRCLE: case SCOORD_TYPES.CIRCLE:
renderMethod = this.renderEllipse; renderMethod = this.renderEllipse;
renderTextBox = this.renderTextBox;
break; break;
case SCOORD_TYPES.ELLIPSE: case SCOORD_TYPES.ELLIPSE:
renderMethod = this.renderEllipse; renderMethod = this.renderEllipse;
renderTextBox = this.renderTextBox;
canvasCoordinatesAdapter = canvasCoordinatesAdapter =
utilities.math.ellipse.getCanvasEllipseCorners; utilities.math.ellipse.getCanvasEllipseCorners;
break; break;
@ -159,18 +154,15 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
options options
); );
if (typeof renderTextBox === 'function') { this.renderTextBox(
renderTextBox.call( svgDrawingHelper,
this, viewport,
svgDrawingHelper, canvasCoordinates,
viewport, canvasCoordinatesAdapter,
canvasCoordinates, annotation,
canvasCoordinatesAdapter, styleSpecifier,
annotation, options
styleSpecifier, );
options
);
}
}); });
} }
}; };
@ -187,11 +179,11 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
color: options.color, color: options.color,
width: options.lineWidth, width: options.lineWidth,
}; };
let canvasCoordinates; let allCanvasCoordinates = [];
renderableData.map((data, index) => { renderableData.map((data, index) => {
canvasCoordinates = data.map(p => viewport.worldToCanvas(p)); const canvasCoordinates = data.map(p => viewport.worldToCanvas(p));
const lineUID = `${index}`; const lineUID = `${index}`;
if (canvasCoordinates.length === 2) { if (canvasCoordinates.length === 2) {
drawing.drawLine( drawing.drawLine(
svgDrawingHelper, svgDrawingHelper,
@ -210,9 +202,11 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
drawingOptions drawingOptions
); );
} }
allCanvasCoordinates = allCanvasCoordinates.concat(canvasCoordinates);
}); });
return canvasCoordinates; // used for drawing textBox return allCanvasCoordinates; // used for drawing textBox
} }
renderMultipoint( renderMultipoint(
@ -405,9 +399,9 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
} }
const SHORT_HAND_MAP = { const SHORT_HAND_MAP = {
'Short Axis': 'W ', 'Short Axis': 'W: ',
'Long Axis': 'L ', 'Long Axis': 'L: ',
AREA: 'Area ', AREA: 'Area: ',
Length: '', Length: '',
CORNERSTONEFREETEXT: '', CORNERSTONEFREETEXT: '',
}; };

View File

@ -1,15 +1,15 @@
import DICOMSRDisplayTool from './DICOMSRDisplayTool'; import DICOMSRDisplayTool from './DICOMSRDisplayTool';
import SRLengthTool from './tools/SRLength';
import SRBidirectional from './tools/SRBidirectional';
import SREllipticalROI from './tools/SREllipticalROI';
import SRArrowAnnotate from './tools/SRArrowAnnotate';
const toolNames = { const toolNames = {
DICOMSRDisplay: DICOMSRDisplayTool.toolName, DICOMSRDisplay: DICOMSRDisplayTool.toolName,
SRLength: SRLengthTool.toolName, SRLength: 'SRLength',
SRBidirectional: SRBidirectional.toolName, SRBidirectional: 'SRBidirectional',
SREllipticalROI: SREllipticalROI.toolName, SREllipticalROI: 'SREllipticalROI',
SRArrowAnnotate: SRArrowAnnotate.toolName, SRArrowAnnotate: 'SRArrowAnnotate',
SRAngle: 'SRAngle',
SRCobbAngle: 'SRCobbAngle',
SRRectangleROI: 'SRRectangleROI',
SRPlanarFreehandROI: 'SRPlanarFreehandROI',
}; };
export default toolNames; export default toolNames;

View File

@ -1,16 +0,0 @@
import { ArrowAnnotateTool } from '@cornerstonejs/tools';
/**
* The reason we are extending ArrowAnnotateTool is to create a new tool for SR
* viewport which basically has a different name. This is done since Cornerstone
* has shifted from creating tool instances for each annotation, and we have ArrowAnnotate
* mappers at the MeasurementService, so if we didn't do this, we would be mapping
* the SR annotation to the MeasurementService (since there is a ArrowAnnotateTool mapper),
* but with extending and renaming it, there is not mapper for SRArrowAnnotateTool; hence
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
*/
class SRArrowAnnotateTool extends ArrowAnnotateTool {
static toolName = 'SRArrowAnnotate';
}
export default SRArrowAnnotateTool;

View File

@ -1,16 +0,0 @@
import { BidirectionalTool } from '@cornerstonejs/tools';
/**
* The reason we are extending BidirectionalTool is to create a new tool for SR
* viewport which basically has a different name. This is done since Cornerstone
* has shifted from creating tool instances for each annotation, and we have Bidirectional
* mappers at the MeasurementService, so if we didn't do this, we would be mapping
* the SR annotation to the MeasurementService (since there is a BidirectionalTool mapper),
* but with extending and renaming it, there is not mapper for SRBidirectionalTool; hence
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
*/
class SRBidirectional extends BidirectionalTool {
static toolName = 'SRBidirectional';
}
export default SRBidirectional;

View File

@ -1,16 +0,0 @@
import { EllipticalROITool } from '@cornerstonejs/tools';
/**
* The reason we are extending EllipticalROITool is to create a new tool for SR
* viewport which basically has a different name. This is done since Cornerstone
* has shifted from creating tool instances for each annotation, and we have EllipticalROI
* mappers at the MeasurementService, so if we didn't do this, we would be mapping
* the SR annotation to the MeasurementService (since there is a EllipticalROITool mapper),
* but with extending and renaming it, there is not mapper for SREllipticalROITool; hence
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
*/
class SREllipticalROI extends EllipticalROITool {
static toolName = 'SREllipticalROI';
}
export default SREllipticalROI;

View File

@ -1,16 +0,0 @@
import { LengthTool } from '@cornerstonejs/tools';
/**
* The reason we are extending LengthTool is to create a new tool for SR
* viewport which basically has a different name. This is done since Cornerstone
* has shifted from creating tool instances for each annotation, and we have Length
* mappers at the MeasurementService, so if we didn't do this, we would be mapping
* the SR annotation to the MeasurementService (since there is a LengthTool mapper),
* but with extending and renaming it, there is not mapper for SRLengthTool; hence
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
*/
class SRLengthTool extends LengthTool {
static toolName = 'SRLength';
}
export default SRLengthTool;

View File

@ -43,7 +43,7 @@ export default function addMeasurement(
// Use the metadata provider to grab its imagePlaneModule metadata // Use the metadata provider to grab its imagePlaneModule metadata
const imagePlaneModule = metaData.get('imagePlaneModule', imageId); const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
const annotationManager = annotation.state.getDefaultAnnotationManager(); const annotationManager = annotation.state.getAnnotationManager();
// Create Cornerstone3D Annotation from measurement // Create Cornerstone3D Annotation from measurement
const frameNumber = const frameNumber =

View File

@ -0,0 +1,12 @@
import { addTool } from '@cornerstonejs/tools';
export default function addToolInstance(
name: string,
toolClass,
configuration?
): void {
class InstanceClass extends toolClass {
static toolName = name;
}
addTool(InstanceClass);
}

View File

@ -39,7 +39,7 @@ function getFilteredCornerstoneToolState(
const findingSites = []; const findingSites = [];
// NOTE -> We use the CORNERSTONEJS coding schemeDesignator which we have // NOTE -> We use the CORNERSTONEJS coding schemeDesignator which we have
// defined in the dcmjs adapters // defined in the @cornerstonejs/adapters
if (measurementDataI.label) { if (measurementDataI.label) {
if (additionalFindingTypes.includes(toolType)) { if (additionalFindingTypes.includes(toolType)) {
finding = { finding = {
@ -67,7 +67,7 @@ function getFilteredCornerstoneToolState(
const uidFilter = measurementData.map(md => md.uid); const uidFilter = measurementData.map(md => md.uid);
const uids = uidFilter.slice(); const uids = uidFilter.slice();
const annotationManager = annotation.state.getDefaultAnnotationManager(); const annotationManager = annotation.state.getAnnotationManager();
const framesOfReference = annotationManager.getFramesOfReference(); const framesOfReference = annotationManager.getFramesOfReference();
for (let i = 0; i < framesOfReference.length; i++) { for (let i = 0; i < framesOfReference.length; i++) {

View File

@ -1,6 +1,6 @@
/** /**
* Extracts the label from the toolData imported from dcmjs. We need to do this * Extracts the label from the toolData imported from dcmjs. We need to do this
* as dcmjs does not depeend on OHIF/the MeasurementService, it just produces data for cornestoneTools. * as dcmjs does not depeend on OHIF/the measurementService, it just produces data for cornestoneTools.
* This optional data is available for the consumer to process if they wish to. * This optional data is available for the consumer to process if they wish to.
* @param {object} toolData The tooldata relating to the * @param {object} toolData The tooldata relating to the
* *

View File

@ -1,10 +1,10 @@
import { utilities, metaData } from '@cornerstonejs/core'; import { utilities, metaData } from '@cornerstonejs/core';
import OHIF, { DicomMetadataStore } from '@ohif/core'; import OHIF, { DicomMetadataStore } from '@ohif/core';
import getLabelFromDCMJSImportedToolData from './utils/getLabelFromDCMJSImportedToolData'; import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
import { adapters } from 'dcmjs'; import { adaptersSR } from '@cornerstonejs/adapters';
const { guid } = OHIF.utils; const { guid } = OHIF.utils;
const { MeasurementReport, CORNERSTONE_3D_TAG } = adapters.Cornerstone3D; const { MeasurementReport, CORNERSTONE_3D_TAG } = adaptersSR.Cornerstone3D;
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools'; const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1'; const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
@ -12,21 +12,22 @@ const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
/** /**
* Hydrates a structured report, for default viewports.
* *
*/ */
export default function _hydrateStructuredReport( export default function hydrateStructuredReport(
{ servicesManager, extensionManager }, { servicesManager, extensionManager },
displaySetInstanceUID displaySetInstanceUID
) { ) {
const dataSource = extensionManager.getActiveDataSource()[0]; const dataSource = extensionManager.getActiveDataSource()[0];
const { MeasurementService, DisplaySetService } = servicesManager.services; const { measurementService, displaySetService } = servicesManager.services;
const displaySet = DisplaySetService.getDisplaySetByUID( const displaySet = displaySetService.getDisplaySetByUID(
displaySetInstanceUID displaySetInstanceUID
); );
// TODO -> We should define a strict versioning somewhere. // TODO -> We should define a strict versioning somewhere.
const mappings = MeasurementService.getSourceMappings( const mappings = measurementService.getSourceMappings(
CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION CORNERSTONE_3D_TOOLS_SOURCE_VERSION
); );
@ -165,7 +166,7 @@ export default function _hydrateStructuredReport(
}, },
}; };
const source = MeasurementService.getSource( const source = measurementService.getSource(
CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION CORNERSTONE_3D_TOOLS_SOURCE_VERSION
); );
@ -175,7 +176,7 @@ export default function _hydrateStructuredReport(
m => m.annotationType === annotationType m => m.annotationType === annotationType
); );
MeasurementService.addRawMeasurement( measurementService.addRawMeasurement(
source, source,
annotationType, annotationType,
{ annotation }, { annotation },
@ -250,7 +251,7 @@ function _mapLegacyDataSet(dataset) {
return dataset; return dataset;
} }
const toArray = function(x) { const toArray = function (x) {
return Array.isArray(x) ? x : [x]; return Array.isArray(x) ? x : [x];
}; };

View File

@ -1,16 +1,18 @@
import { adapters } from 'dcmjs'; import { adaptersSR } from '@cornerstonejs/adapters';
const cornerstoneAdapters = adapters.Cornerstone3D; const cornerstoneAdapters =
adaptersSR.Cornerstone3D.MeasurementReport
.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG; const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG;
/** /**
* Checks if the given `displaySet`can be rehydrated into the `MeasurementService`. * Checks if the given `displaySet`can be rehydrated into the `measurementService`.
* *
* @param {object} displaySet The SR `displaySet` to check. * @param {object} displaySet The SR `displaySet` to check.
* @param {object[]} mappings The CornerstoneTools 4 mappings to the `MeasurementService`. * @param {object[]} mappings The CornerstoneTools 4 mappings to the `measurementService`.
* @returns {boolean} True if the SR can be rehydrated into the `MeasurementService`. * @returns {boolean} True if the SR can be rehydrated into the `measurementService`.
*/ */
export default function isRehydratable(displaySet, mappings) { export default function isRehydratable(displaySet, mappings) {
if (!mappings || !mappings.length) { if (!mappings || !mappings.length) {
@ -30,14 +32,14 @@ export default function isRehydratable(displaySet, mappings) {
adapterKeys.forEach(key => { adapterKeys.forEach(key => {
if (mappingDefinitions.includes(key)) { if (mappingDefinitions.includes(key)) {
// Must have both a dcmjs adapter and a MeasurementService // Must have both a dcmjs adapter and a measurementService
// Definition in order to be a candidate for import. // Definition in order to be a candidate for import.
adapters.push(cornerstoneAdapters[key]); adapters.push(cornerstoneAdapters[key]);
} }
}); });
for (let i = 0; i < measurements.length; i++) { for (let i = 0; i < measurements.length; i++) {
const TrackingIdentifier = measurements[i].TrackingIdentifier; const { TrackingIdentifier } = measurements[i] || {};
const hydratable = adapters.some(adapter => { const hydratable = adapters.some(adapter => {
let [cornerstoneTag, toolName] = TrackingIdentifier.split(':'); let [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
if (supportedLegacyCornerstoneTags.includes(cornerstoneTag)) { if (supportedLegacyCornerstoneTags.includes(cornerstoneTag)) {
@ -54,7 +56,13 @@ export default function isRehydratable(displaySet, mappings) {
if (hydratable) { if (hydratable) {
return true; return true;
} }
console.log(
'Measurement is not rehydratable',
TrackingIdentifier,
measurements[i]
);
} }
console.log('No measurements found which were rehydratable');
return false; return false;
} }

View File

@ -1,4 +1,4 @@
import dcmjs from 'dcmjs'; import { adaptersSR } from '@cornerstonejs/adapters';
/** /**
* Checks if dcmjs has support to determined tool * Checks if dcmjs has support to determined tool
@ -7,7 +7,7 @@ import dcmjs from 'dcmjs';
* @returns {boolean} * @returns {boolean}
*/ */
const isToolSupported = toolName => { const isToolSupported = toolName => {
const adapter = dcmjs.adapters.Cornerstone3D; const adapter = adaptersSR.Cornerstone3D;
return !!adapter[toolName]; return !!adapter[toolName];
}; };

View File

@ -1,18 +1,18 @@
import React, { useCallback, useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types'; 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 { useTranslation } from 'react-i18next';
import OHIF, { utils } from '@ohif/core';
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule'; import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
import { import {
Notification,
ViewportActionBar,
useViewportGrid,
useViewportDialog,
Tooltip,
Icon, Icon,
Notification,
Tooltip,
useViewportDialog,
useViewportGrid,
ViewportActionBar,
} from '@ohif/ui'; } from '@ohif/ui';
import classNames from 'classnames'; import hydrateStructuredReport from '../utils/hydrateStructuredReport';
const { formatDate } = utils; const { formatDate } = utils;
@ -32,11 +32,10 @@ function OHIFCornerstoneSRViewport(props) {
extensionManager, extensionManager,
} = props; } = props;
const { t } = useTranslation('SRViewport');
const { const {
DisplaySetService, displaySetService,
CornerstoneViewportService, cornerstoneViewportService,
measurementService,
} = servicesManager.services; } = servicesManager.services;
// SR viewport will always have a single display set // SR viewport will always have a single display set
@ -69,18 +68,34 @@ function OHIFCornerstoneSRViewport(props) {
MEASUREMENT_TRACKING_EXTENSION_ID MEASUREMENT_TRACKING_EXTENSION_ID
); );
// TODO: this is a hook that fails if we register/de-register
if (hasMeasurementTrackingExtension) { if (hasMeasurementTrackingExtension) {
const contextModule = extensionManager.getModuleEntry( const contextModule = extensionManager.getModuleEntry(
'@ohif/extension-measurement-tracking.contextModule.TrackedMeasurementsContext' '@ohif/extension-measurement-tracking.contextModule.TrackedMeasurementsContext'
); );
const useTrackedMeasurements = () => useContext(contextModule.context); const tracked = useContext(contextModule.context);
trackedMeasurements = tracked?.[0];
[ sendTrackedMeasurementsEvent = tracked?.[1];
trackedMeasurements, }
sendTrackedMeasurementsEvent, if (!sendTrackedMeasurementsEvent) {
] = useTrackedMeasurements(); // if no panels from measurement-tracking extension is used, this code will run
trackedMeasurements = null;
sendTrackedMeasurementsEvent = (eventName, { displaySetInstanceUID }) => {
measurementService.clearMeasurements();
const { SeriesInstanceUIDs } = hydrateStructuredReport(
{ servicesManager, extensionManager },
displaySetInstanceUID
);
const displaySets = displaySetService.getDisplaySetsForSeries(
SeriesInstanceUIDs[0]
);
if (displaySets.length) {
viewportGridService.setDisplaySetsForViewport({
viewportIndex: activeViewportIndex,
displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID],
});
}
};
} }
/** /**
@ -134,7 +149,7 @@ function OHIFCornerstoneSRViewport(props) {
_getViewportReferencedDisplaySetData( _getViewportReferencedDisplaySetData(
srDisplaySet, srDisplaySet,
newMeasurementSelected, newMeasurementSelected,
DisplaySetService displaySetService
).then(({ referencedDisplaySet, referencedDisplaySetMetadata }) => { ).then(({ referencedDisplaySet, referencedDisplaySetMetadata }) => {
setMeasurementSelected(newMeasurementSelected); setMeasurementSelected(newMeasurementSelected);
setActiveImageDisplaySetData(referencedDisplaySet); setActiveImageDisplaySetData(referencedDisplaySet);
@ -148,13 +163,13 @@ function OHIFCornerstoneSRViewport(props) {
// it means that we have a new referenced display set, and the // it means that we have a new referenced display set, and the
// imageIdIndex will handle it by updating the viewport, but if they // imageIdIndex will handle it by updating the viewport, but if they
// are the same we just need to use MeasurementService to jump to the // are the same we just need to use measurementService to jump to the
// new measurement // new measurement
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex( const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex viewportIndex
); );
const csViewport = CornerstoneViewportService.getCornerstoneViewport( const csViewport = cornerstoneViewportService.getCornerstoneViewport(
viewportInfo.getViewportId() viewportInfo.getViewportId()
); );
@ -241,8 +256,8 @@ function OHIFCornerstoneSRViewport(props) {
Cleanup the SR viewport when the viewport is destroyed Cleanup the SR viewport when the viewport is destroyed
*/ */
useEffect(() => { useEffect(() => {
const onDisplaySetsRemovedSubscription = DisplaySetService.subscribe( const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_REMOVED, displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
({ displaySetInstanceUIDs }) => { ({ displaySetInstanceUIDs }) => {
const activeViewport = viewports[activeViewportIndex]; const activeViewport = viewports[activeViewportIndex];
if ( if (
@ -266,7 +281,7 @@ function OHIFCornerstoneSRViewport(props) {
* isHydratable check, the outcome for the isHydrated state here is always FALSE * isHydratable check, the outcome for the isHydrated state here is always FALSE
* since we don't do the hydration here. Todo: can't we just set it as false? why * since we don't do the hydration here. Todo: can't we just set it as false? why
* we are changing the state here? isHydrated is always false at this stage, and * we are changing the state here? isHydrated is always false at this stage, and
* if it is hydrated we don't event use the SR viewport. * if it is hydrated we don't even use the SR viewport.
*/ */
useEffect(() => { useEffect(() => {
if (!srDisplaySet.isLoaded) { if (!srDisplaySet.isLoaded) {
@ -361,8 +376,7 @@ function OHIFCornerstoneSRViewport(props) {
label: viewportLabel, label: viewportLabel,
useAltStyling: true, useAltStyling: true,
studyDate: formatDate(StudyDate), studyDate: formatDate(StudyDate),
currentSeries: SeriesNumber, seriesDescription: SeriesDescription || '',
seriesDescription: SeriesDescription,
patientInformation: { patientInformation: {
patientName: PatientName patientName: PatientName
? OHIF.utils.formatPN(PatientName.Alphabetic) ? OHIF.utils.formatPN(PatientName.Alphabetic)
@ -414,14 +428,14 @@ OHIFCornerstoneSRViewport.defaultProps = {
async function _getViewportReferencedDisplaySetData( async function _getViewportReferencedDisplaySetData(
displaySet, displaySet,
measurementSelected, measurementSelected,
DisplaySetService displaySetService
) { ) {
const { measurements } = displaySet; const { measurements } = displaySet;
const measurement = measurements[measurementSelected]; const measurement = measurements[measurementSelected];
const { displaySetInstanceUID } = measurement; const { displaySetInstanceUID } = measurement;
const referencedDisplaySet = DisplaySetService.getDisplaySetByUID( const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySetInstanceUID displaySetInstanceUID
); );
@ -450,13 +464,16 @@ function _getStatusComponent({
isLocked, isLocked,
sendTrackedMeasurementsEvent, sendTrackedMeasurementsEvent,
}) { }) {
const onPillClick = () => { const handleMouseUp = () => {
sendTrackedMeasurementsEvent('RESTORE_PROMPT_HYDRATE_SR', { sendTrackedMeasurementsEvent('HYDRATE_SR', {
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID, displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
viewportIndex, viewportIndex,
}); });
}; };
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
// 1 - Incompatible // 1 - Incompatible
// 2 - Locked // 2 - Locked
// 3 - Rehydratable / Open // 3 - Rehydratable / Open
@ -467,22 +484,7 @@ function _getStatusComponent({
switch (state) { switch (state) {
case 1: case 1:
StatusIcon = () => ( StatusIcon = () => <Icon name="status-alert" />;
<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>
);
ToolTipMessage = () => ( ToolTipMessage = () => (
<div> <div>
@ -493,20 +495,7 @@ function _getStatusComponent({
); );
break; break;
case 2: case 2:
StatusIcon = () => ( StatusIcon = () => <Icon name="status-locked" />;
<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>
);
ToolTipMessage = () => ( ToolTipMessage = () => (
<div> <div>
@ -519,48 +508,28 @@ function _getStatusComponent({
); );
break; break;
case 3: case 3:
StatusIcon = () => ( StatusIcon = () => <Icon name="status-untracked" />;
<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>
);
ToolTipMessage = () => <div>Click to restore measurements.</div>; ToolTipMessage = () => (
<div>{`Click ${loadStr} to restore measurements.`}</div>
);
} }
const StatusPill = () => ( const StatusArea = () => (
<div <div className="flex h-6 leading-6 cursor-default text-sm text-white">
className={classNames( <div className="min-w-[45px] flex items-center p-1 rounded-l-xl rounded-r bg-customgray-100">
'group relative flex items-center justify-center px-2 rounded-full cursor-default bg-customgreen-100', <StatusIcon />
{ <span className="ml-1">SR</span>
'hover:bg-customblue-100': state === 3, </div>
'cursor-pointer': state === 3, {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> </div>
); );
@ -568,22 +537,12 @@ function _getStatusComponent({
<> <>
{ToolTipMessage && ( {ToolTipMessage && (
<Tooltip content={<ToolTipMessage />} position="bottom-left"> <Tooltip content={<ToolTipMessage />} position="bottom-left">
<StatusPill /> <StatusArea />
</Tooltip> </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; export default OHIFCornerstoneSRViewport;

View File

@ -30,7 +30,7 @@
"@ohif/core": "^3.0.0", "@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0", "@ohif/ui": "^2.0.0",
"cornerstone-wado-image-loader": "^4.2.1", "cornerstone-wado-image-loader": "^4.2.1",
"dcmjs": "^0.28.3", "dcmjs": "^0.29.4",
"dicom-parser": "^1.8.9", "dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
@ -39,14 +39,15 @@
"react-resize-detector": "^6.7.6" "react-resize-detector": "^6.7.6"
}, },
"devDependencies": { "devDependencies": {
"lodash": "4.17.21" "lodash": "^4.17.21"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.17.9", "@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^0.22.3", "@cornerstonejs/adapters": "^0.4.1",
"@cornerstonejs/streaming-image-volume-loader": "^0.6.11", "@cornerstonejs/core": "^0.33.2",
"@cornerstonejs/tools": "^0.30.6", "@cornerstonejs/streaming-image-volume-loader": "^0.14.1",
"@kitware/vtk.js": "25.9.0", "@cornerstonejs/tools": "^0.50.2",
"@kitware/vtk.js": "26.5.6",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"lodash.debounce": "4.0.8", "lodash.debounce": "4.0.8",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",

View File

@ -20,6 +20,7 @@ import {
IVolumeViewport, IVolumeViewport,
} from '@cornerstonejs/core/dist/esm/types'; } from '@cornerstonejs/core/dist/esm/types';
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes'; import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
import { CinePlayer, useCine, useViewportGrid } from '@ohif/ui';
const STACK = 'stack'; const STACK = 'stack';
@ -112,20 +113,90 @@ const OHIFCornerstoneViewport = React.memo(props => {
} = props; } = props;
const [scrollbarHeight, setScrollbarHeight] = useState('100px'); const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [{ isCineEnabled, cines }, cineService] = useCine();
const [{ activeViewportIndex }] = useViewportGrid();
const [enabledVPElement, setEnabledVPElement] = useState(null);
const elementRef = useRef(); const elementRef = useRef();
const { const {
MeasurementService, measurementService,
DisplaySetService, displaySetService,
ToolBarService, toolbarService,
ToolGroupService, toolGroupService,
SyncGroupService, syncGroupService,
CornerstoneViewportService, cornerstoneViewportService,
CornerstoneCacheService, cornerstoneCacheService,
ViewportGridService, viewportGridService,
} = servicesManager.services; } = 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 // useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => { const setImageScrollBarHeight = useCallback(() => {
const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`; const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`;
@ -135,13 +206,13 @@ const OHIFCornerstoneViewport = React.memo(props => {
// useCallback for onResize // useCallback for onResize
const onResize = useCallback(() => { const onResize = useCallback(() => {
if (elementRef.current) { if (elementRef.current) {
CornerstoneViewportService.resize(); cornerstoneViewportService.resize();
setImageScrollBarHeight(); setImageScrollBarHeight();
} }
}, [elementRef]); }, [elementRef]);
const cleanUpServices = useCallback(() => { const cleanUpServices = useCallback(() => {
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex( const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex viewportIndex
); );
@ -153,9 +224,9 @@ const OHIFCornerstoneViewport = React.memo(props => {
const renderingEngineId = viewportInfo.getRenderingEngineId(); const renderingEngineId = viewportInfo.getRenderingEngineId();
const syncGroups = viewportInfo.getSyncGroups(); const syncGroups = viewportInfo.getSyncGroups();
ToolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId); toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
SyncGroupService.removeViewportFromSyncGroup( syncGroupService.removeViewportFromSyncGroup(
viewportId, viewportId,
renderingEngineId, renderingEngineId,
syncGroups syncGroups
@ -170,24 +241,25 @@ const OHIFCornerstoneViewport = React.memo(props => {
} }
const { viewportId, element } = evt.detail; const { viewportId, element } = evt.detail;
const viewportInfo = CornerstoneViewportService.getViewportInfo( const viewportInfo = cornerstoneViewportService.getViewportInfo(
viewportId viewportId
); );
const viewportIndex = viewportInfo.getViewportIndex(); const viewportIndex = viewportInfo.getViewportIndex();
setEnabledElement(viewportIndex, element); setEnabledElement(viewportIndex, element);
setEnabledVPElement(element);
const renderingEngineId = viewportInfo.getRenderingEngineId(); const renderingEngineId = viewportInfo.getRenderingEngineId();
const toolGroupId = viewportInfo.getToolGroupId(); const toolGroupId = viewportInfo.getToolGroupId();
const syncGroups = viewportInfo.getSyncGroups(); const syncGroups = viewportInfo.getSyncGroups();
ToolGroupService.addViewportToToolGroup( toolGroupService.addViewportToToolGroup(
viewportId, viewportId,
renderingEngineId, renderingEngineId,
toolGroupId toolGroupId
); );
SyncGroupService.addViewportToSyncGroup( syncGroupService.addViewportToSyncGroup(
viewportId, viewportId,
renderingEngineId, renderingEngineId,
syncGroups syncGroups
@ -197,12 +269,12 @@ const OHIFCornerstoneViewport = React.memo(props => {
onElementEnabled(evt); onElementEnabled(evt);
} }
}, },
[viewportIndex, onElementEnabled, ToolGroupService] [viewportIndex, onElementEnabled, toolGroupService]
); );
// disable the element upon unmounting // disable the element upon unmounting
useEffect(() => { useEffect(() => {
CornerstoneViewportService.enableViewport( cornerstoneViewportService.enableViewport(
viewportIndex, viewportIndex,
viewportOptions, viewportOptions,
elementRef.current elementRef.current
@ -218,10 +290,10 @@ const OHIFCornerstoneViewport = React.memo(props => {
return () => { return () => {
cleanUpServices(); cleanUpServices();
CornerstoneViewportService.disableElement(viewportIndex); cornerstoneViewportService.disableElement(viewportIndex);
if (onElementDisabled) { if (onElementDisabled) {
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex( const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex viewportIndex
); );
@ -244,24 +316,24 @@ const OHIFCornerstoneViewport = React.memo(props => {
// Note: this approach does not actually end of sending network requests // Note: this approach does not actually end of sending network requests
// and it uses the network cache // and it uses the network cache
useEffect(() => { useEffect(() => {
const { unsubscribe } = DisplaySetService.subscribe( const { unsubscribe } = displaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
async invalidatedDisplaySetInstanceUID => { async invalidatedDisplaySetInstanceUID => {
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex( const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex viewportIndex
); );
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) { if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
const viewportData = viewportInfo.getViewportData(); const viewportData = viewportInfo.getViewportData();
const newViewportData = await CornerstoneCacheService.invalidateViewportData( const newViewportData = await cornerstoneCacheService.invalidateViewportData(
viewportData, viewportData,
invalidatedDisplaySetInstanceUID, invalidatedDisplaySetInstanceUID,
dataSource, dataSource,
DisplaySetService displaySetService
); );
const keepCamera = true; const keepCamera = true;
CornerstoneViewportService.updateViewport( cornerstoneViewportService.updateViewport(
viewportIndex, viewportIndex,
newViewportData, newViewportData,
keepCamera keepCamera
@ -281,14 +353,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
} }
const loadViewportData = async () => { const loadViewportData = async () => {
const viewportData = await CornerstoneCacheService.createViewportData( const viewportData = await cornerstoneCacheService.createViewportData(
displaySets, displaySets,
viewportOptions, viewportOptions,
dataSource, dataSource,
initialImageIndex initialImageIndex
); );
CornerstoneViewportService.setViewportData( cornerstoneViewportService.setViewportData(
viewportIndex, viewportIndex,
viewportData, viewportData,
viewportOptions, viewportOptions,
@ -311,23 +383,23 @@ const OHIFCornerstoneViewport = React.memo(props => {
*/ */
useEffect(() => { useEffect(() => {
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents( const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
MeasurementService, measurementService,
DisplaySetService, displaySetService,
elementRef, elementRef,
viewportIndex, viewportIndex,
displaySets, displaySets,
ViewportGridService, viewportGridService,
CornerstoneViewportService cornerstoneViewportService
); );
_checkForCachedJumpToMeasurementEvents( _checkForCachedJumpToMeasurementEvents(
MeasurementService, measurementService,
DisplaySetService, displaySetService,
elementRef, elementRef,
viewportIndex, viewportIndex,
displaySets, displaySets,
ViewportGridService, viewportGridService,
CornerstoneViewportService cornerstoneViewportService
); );
return () => { return () => {
@ -355,29 +427,48 @@ const OHIFCornerstoneViewport = React.memo(props => {
></div> ></div>
<CornerstoneOverlays <CornerstoneOverlays
viewportIndex={viewportIndex} viewportIndex={viewportIndex}
ToolBarService={ToolBarService} toolbarService={toolbarService}
element={elementRef.current} element={elementRef.current}
scrollbarHeight={scrollbarHeight} scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager} 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> </div>
); );
}, areEqual); }, areEqual);
function _subscribeToJumpToMeasurementEvents( function _subscribeToJumpToMeasurementEvents(
MeasurementService, measurementService,
DisplaySetService, displaySetService,
elementRef, elementRef,
viewportIndex, viewportIndex,
displaySets, displaySets,
ViewportGridService, viewportGridService,
CornerstoneViewportService cornerstoneViewportService
) { ) {
const displaysUIDs = displaySets.map( const displaysUIDs = displaySets.map(
displaySet => displaySet.displaySetInstanceUID displaySet => displaySet.displaySetInstanceUID
); );
const { unsubscribe } = MeasurementService.subscribe( const { unsubscribe } = measurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT, measurementService.EVENTS.JUMP_TO_MEASUREMENT,
({ measurement }) => { ({ measurement }) => {
if (!measurement) return; if (!measurement) return;
@ -387,10 +478,10 @@ function _subscribeToJumpToMeasurementEvents(
measurement, measurement,
elementRef, elementRef,
viewportIndex, viewportIndex,
MeasurementService, measurementService,
DisplaySetService, displaySetService,
ViewportGridService, viewportGridService,
CornerstoneViewportService cornerstoneViewportService
); );
} }
} }
@ -401,25 +492,25 @@ function _subscribeToJumpToMeasurementEvents(
// Check if there is a queued jumpToMeasurement event // Check if there is a queued jumpToMeasurement event
function _checkForCachedJumpToMeasurementEvents( function _checkForCachedJumpToMeasurementEvents(
MeasurementService, measurementService,
DisplaySetService, displaySetService,
elementRef, elementRef,
viewportIndex, viewportIndex,
displaySets, displaySets,
ViewportGridService, viewportGridService,
CornerstoneViewportService cornerstoneViewportService
) { ) {
const displaysUIDs = displaySets.map( const displaysUIDs = displaySets.map(
displaySet => displaySet.displaySetInstanceUID displaySet => displaySet.displaySetInstanceUID
); );
const measurementIdToJumpTo = MeasurementService.getJumpToMeasurement( const measurementIdToJumpTo = measurementService.getJumpToMeasurement(
viewportIndex viewportIndex
); );
if (measurementIdToJumpTo && elementRef) { if (measurementIdToJumpTo && elementRef) {
// Jump to measurement if the measurement exists // Jump to measurement if the measurement exists
const measurement = MeasurementService.getMeasurement( const measurement = measurementService.getMeasurement(
measurementIdToJumpTo measurementIdToJumpTo
); );
@ -428,10 +519,10 @@ function _checkForCachedJumpToMeasurementEvents(
measurement, measurement,
elementRef, elementRef,
viewportIndex, viewportIndex,
MeasurementService, measurementService,
DisplaySetService, displaySetService,
ViewportGridService, viewportGridService,
CornerstoneViewportService cornerstoneViewportService
); );
} }
} }
@ -441,10 +532,10 @@ function _jumpToMeasurement(
measurement, measurement,
targetElementRef, targetElementRef,
viewportIndex, viewportIndex,
MeasurementService, measurementService,
DisplaySetService, displaySetService,
ViewportGridService, viewportGridService,
CornerstoneViewportService cornerstoneViewportService
) { ) {
const targetElement = targetElementRef.current; const targetElement = targetElementRef.current;
const { displaySetInstanceUID, SOPInstanceUID, frameNumber } = measurement; const { displaySetInstanceUID, SOPInstanceUID, frameNumber } = measurement;
@ -453,7 +544,7 @@ function _jumpToMeasurement(
console.warn('cannot jump in a non-acquisition plane measurements yet'); console.warn('cannot jump in a non-acquisition plane measurements yet');
} }
const referencedDisplaySet = DisplaySetService.getDisplaySetByUID( const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySetInstanceUID displaySetInstanceUID
); );
@ -461,11 +552,11 @@ function _jumpToMeasurement(
// to set it properly // to set it properly
// setCornerstoneMeasurementActive(measurement); // setCornerstoneMeasurementActive(measurement);
ViewportGridService.setActiveViewportIndex(viewportIndex); viewportGridService.setActiveViewportIndex(viewportIndex);
const enableElement = getEnabledElement(targetElement); const enableElement = getEnabledElement(targetElement);
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex( const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex viewportIndex
); );
@ -520,7 +611,7 @@ function _jumpToMeasurement(
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid); cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
// Jump to measurement consumed, remove. // Jump to measurement consumed, remove.
MeasurementService.removeJumpToMeasurement(viewportIndex); measurementService.removeJumpToMeasurement(viewportIndex);
} }
} }

View File

@ -1,13 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import ViewportImageScrollbar from './ViewportImageScrollbar'; import ViewportImageScrollbar from './ViewportImageScrollbar';
import ViewportOverlay from './ViewportOverlay'; import CustomizableViewportOverlay from './CustomizableViewportOverlay';
import ViewportOrientationMarkers from './ViewportOrientationMarkers'; import ViewportOrientationMarkers from './ViewportOrientationMarkers';
import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator'; import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator';
function CornerstoneOverlays(props) { function CornerstoneOverlays(props) {
const { viewportIndex, element, scrollbarHeight, servicesManager } = props; const { viewportIndex, element, scrollbarHeight, servicesManager } = props;
const { CornerstoneViewportService } = servicesManager.services; const { cornerstoneViewportService } = servicesManager.services;
const [imageSliceData, setImageSliceData] = useState({ const [imageSliceData, setImageSliceData] = useState({
imageIndex: 0, imageIndex: 0,
numberOfSlices: 0, numberOfSlices: 0,
@ -15,8 +15,8 @@ function CornerstoneOverlays(props) {
const [viewportData, setViewportData] = useState(null); const [viewportData, setViewportData] = useState(null);
useEffect(() => { useEffect(() => {
const { unsubscribe } = CornerstoneViewportService.subscribe( const { unsubscribe } = cornerstoneViewportService.subscribe(
CornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED, cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED,
props => { props => {
if (props.viewportIndex !== viewportIndex) { if (props.viewportIndex !== viewportIndex) {
return; return;
@ -36,9 +36,8 @@ function CornerstoneOverlays(props) {
} }
if (viewportData) { if (viewportData) {
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex( const viewportInfo =
viewportIndex cornerstoneViewportService.getViewportInfoByIndex(viewportIndex);
);
if (viewportInfo?.viewportOptions?.customViewportProps?.hideOverlays) { if (viewportInfo?.viewportOptions?.customViewportProps?.hideOverlays) {
return null; return null;
@ -56,17 +55,20 @@ function CornerstoneOverlays(props) {
scrollbarHeight={scrollbarHeight} scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager} servicesManager={servicesManager}
/> />
<ViewportOverlay
<CustomizableViewportOverlay
imageSliceData={imageSliceData} imageSliceData={imageSliceData}
viewportData={viewportData} viewportData={viewportData}
viewportIndex={viewportIndex} viewportIndex={viewportIndex}
servicesManager={servicesManager} servicesManager={servicesManager}
element={element} element={element}
/> />
<ViewportImageSliceLoadingIndicator <ViewportImageSliceLoadingIndicator
viewportData={viewportData} viewportData={viewportData}
element={element} element={element}
/> />
<ViewportOrientationMarkers <ViewportOrientationMarkers
imageSliceData={imageSliceData} imageSliceData={imageSliceData}
element={element} element={element}

View File

@ -0,0 +1,25 @@
/*
custom overlay panels: top-left, top-right, bottom-left and bottom-right
If any text to be displayed on the overlay is too long to hold on a single
line, it will be truncated with ellipsis in the end.
*/
.viewport-overlay {
max-width: 40%;
}
.viewport-overlay span {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.viewport-overlay.left-viewport {
text-align: left;
}
.viewport-overlay.right-viewport-scrollbar {
text-align: right;
}
.viewport-overlay.right-viewport-scrollbar .flex.flex-row {
justify-content: flex-end;
}

View File

@ -0,0 +1,489 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core';
import { ViewportOverlay } from '@ohif/ui';
import {
formatPN,
formatDICOMDate,
formatDICOMTime,
formatNumberPrecision,
} from './utils';
import { InstanceMetadata } from 'platform/core/src/types';
import { ServicesManager } from '@ohif/core';
import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
import './CustomizableViewportOverlay.css';
const EPSILON = 1e-4;
interface OverlayItemProps {
element: any;
viewportData: any;
imageSliceData: ImageSliceData;
viewportIndex: number | null;
servicesManager: ServicesManager;
instance: InstanceMetadata;
customization: any;
formatters: {
formatPN: (val) => string;
formatDate: (val) => string;
formatTime: (val) => string;
formatNumberPrecision: (val, number) => string;
};
// calculated values
voi: {
windowWidth: number;
windowCenter: number;
};
instanceNumber?: number;
scale?: number;
}
/**
* Window Level / Center Overlay item
*/
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
const { windowWidth, windowCenter } = voi;
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
return null;
}
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">W:</span>
<span className="ml-1 mr-2 font-light shrink-0">
{windowWidth.toFixed(0)}
</span>
<span className="mr-1 shrink-0">L:</span>
<span className="ml-1 font-light shrink-0">
{windowCenter.toFixed(0)}
</span>
</div>
);
}
/**
* Zoom Level Overlay item
*/
function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">Zoom:</span>
<span className="font-light">{scale.toFixed(2)}x</span>
</div>
);
}
/**
* Instance Number Overlay Item
*/
function InstanceNumberOverlayItem({
instanceNumber,
imageSliceData,
customization,
}: OverlayItemProps) {
const { imageIndex, numberOfSlices } = imageSliceData;
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">I:</span>
<span className="font-light">
{instanceNumber !== undefined && instanceNumber !== null
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
: `${imageIndex + 1}/${numberOfSlices}`}
</span>
</div>
);
}
/**
* Customizable Viewport Overlay
*/
function CustomizableViewportOverlay({
element,
viewportData,
imageSliceData,
viewportIndex,
servicesManager,
}) {
const { toolbarService, cornerstoneViewportService, customizationService } =
servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1);
const [activeTools, setActiveTools] = useState([]);
const { imageIndex } = imageSliceData;
const topLeftCustomization = customizationService.getModeCustomization(
'cornerstoneOverlayTopLeft'
);
const topRightCustomization = customizationService.getModeCustomization(
'cornerstoneOverlayTopRight'
);
const bottomLeftCustomization = customizationService.getModeCustomization(
'cornerstoneOverlayBottomLeft'
);
const bottomRightCustomization = customizationService.getModeCustomization(
'cornerstoneOverlayBottomRight'
);
const instance = useMemo(() => {
if (viewportData != null) {
return _getViewportInstance(viewportData, imageIndex);
} else {
return null;
}
}, [viewportData, imageIndex]);
const instanceNumber = useMemo(() => {
if (viewportData != null) {
return _getInstanceNumber(
viewportData,
viewportIndex,
imageIndex,
cornerstoneViewportService
);
}
return null;
}, [viewportData, viewportIndex, imageIndex, cornerstoneViewportService]);
/**
* Initial toolbar state
*/
useEffect(() => {
setActiveTools(toolbarService.getActiveTools());
}, []);
/**
* Updating the VOI when the viewport changes its voi
*/
useEffect(() => {
const updateVOI = eventDetail => {
const { range } = eventDetail.detail;
if (!range) {
return;
}
const { lower, upper } = range;
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(
lower,
upper
);
setVOI({ windowCenter, windowWidth });
};
element.addEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
return () => {
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
};
}, [viewportIndex, viewportData, voi, element]);
/**
* Updating the scale when the viewport changes its zoom
*/
useEffect(() => {
const updateScale = eventDetail => {
const { previousCamera, camera } = eventDetail.detail;
if (
previousCamera.parallelScale !== camera.parallelScale ||
previousCamera.scale !== camera.scale
) {
const viewport =
cornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex
);
if (!viewport) {
return;
}
const imageData = viewport.getImageData();
if (!imageData) {
return;
}
if (camera.scale) {
setScale(camera.scale);
return;
}
const { spacing } = imageData;
// convert parallel scale to scale
const scale =
(element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
setScale(scale);
}
};
element.addEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
return () => {
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
};
}, [viewportIndex, viewportData, cornerstoneViewportService, element]);
/**
* Updating the active tools when the toolbar changes
*/
// Todo: this should act on the toolGroups instead of the toolbar state
useEffect(() => {
const { unsubscribe } = toolbarService.subscribe(
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
() => {
setActiveTools(toolbarService.getActiveTools());
}
);
return () => {
unsubscribe();
};
}, [toolbarService]);
const _renderOverlayItem = useCallback(
item => {
const overlayItemProps: OverlayItemProps = {
element,
viewportData,
imageSliceData,
viewportIndex,
servicesManager,
customization: item,
formatters: {
formatPN: formatPN,
formatDate: formatDICOMDate,
formatTime: formatDICOMTime,
formatNumberPrecision: formatNumberPrecision,
},
instance,
// calculated
voi,
scale,
instanceNumber,
};
if (item.customizationType === 'ohif.overlayItem.windowLevel') {
return <VOIOverlayItem {...overlayItemProps} />;
} else if (item.customizationType === 'ohif.overlayItem.zoomLevel') {
return <ZoomOverlayItem {...overlayItemProps} />;
} else if (item.customizationType === 'ohif.overlayItem.instanceNumber') {
return <InstanceNumberOverlayItem {...overlayItemProps} />;
} else {
const renderItem = customizationService.applyType(item);
if (typeof renderItem.content === 'function') {
return renderItem.content(overlayItemProps);
}
}
},
[
element,
viewportData,
imageSliceData,
viewportIndex,
servicesManager,
customizationService,
instance,
voi,
scale,
instanceNumber,
]
);
const getTopLeftContent = useCallback(() => {
const items = topLeftCustomization?.items || [
{
id: 'WindowLevel',
customizationType: 'ohif.overlayItem.windowLevel',
},
];
return (
<>
{items.map((item, i) => (
<div key={`topLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
}, [topLeftCustomization, _renderOverlayItem]);
const getTopRightContent = useCallback(() => {
const items = topRightCustomization?.items || [
{
id: 'InstanceNmber',
customizationType: 'ohif.overlayItem.instanceNumber',
},
];
return (
<>
{items.map((item, i) => (
<div key={`topRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
}, [topRightCustomization, _renderOverlayItem]);
const getBottomLeftContent = useCallback(() => {
const items = bottomLeftCustomization?.items || [];
return (
<>
{items.map((item, i) => (
<div key={`bottomLeftOverlayItem_${i}`}>
{_renderOverlayItem(item)}
</div>
))}
</>
);
}, [bottomLeftCustomization, _renderOverlayItem]);
const getBottomRightContent = useCallback(() => {
const items = bottomRightCustomization?.items || [];
return (
<>
{items.map((item, i) => (
<div key={`bottomRightOverlayItem_${i}`}>
{_renderOverlayItem(item)}
</div>
))}
</>
);
}, [bottomRightCustomization, _renderOverlayItem]);
return (
<ViewportOverlay
topLeft={getTopLeftContent()}
topRight={getTopRightContent()}
bottomLeft={getBottomLeftContent()}
bottomRight={getBottomRightContent()}
/>
);
}
function _getViewportInstance(viewportData, imageIndex) {
let imageId = null;
if (viewportData.viewportType === Enums.ViewportType.STACK) {
imageId = viewportData.data.imageIds[imageIndex];
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
const volumes = viewportData.volumes;
if (volumes && volumes.length == 1) {
const volume = volumes[0];
imageId = volume.imageIds[imageIndex];
}
}
return imageId ? metaData.get('instance', imageId) || {} : {};
}
function _getInstanceNumber(
viewportData,
viewportIndex,
imageIndex,
cornerstoneViewportService
) {
let instanceNumber;
if (viewportData.viewportType === Enums.ViewportType.STACK) {
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
if (!instanceNumber && instanceNumber !== 0) {
return null;
}
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
instanceNumber = _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportIndex,
cornerstoneViewportService
);
}
return instanceNumber;
}
function _getInstanceNumberFromStack(viewportData, imageIndex) {
const imageIds = viewportData.data.imageIds;
const imageId = imageIds[imageIndex];
if (!imageId) {
return;
}
const generalImageModule = metaData.get('generalImageModule', imageId) || {};
const { instanceNumber } = generalImageModule;
const stackSize = imageIds.length;
if (stackSize <= 1) {
return;
}
return parseInt(instanceNumber);
}
// Since volume viewports can be in any view direction, they can render
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
function _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportIndex,
cornerstoneViewportService
) {
const volumes = viewportData.volumes;
// Todo: support fusion of acquisition plane which has instanceNumber
if (!volumes || volumes.length > 1) {
return;
}
const volume = volumes[0];
const { direction, imageIds } = volume;
const cornerstoneViewport =
cornerstoneViewportService.getCornerstoneViewportByIndex(viewportIndex);
if (!cornerstoneViewport) {
return;
}
const camera = cornerstoneViewport.getCamera();
const { viewPlaneNormal } = camera;
// checking if camera is looking at the acquisition plane (defined by the direction on the volume)
const scanAxisNormal = direction.slice(6, 9);
// check if viewPlaneNormal is parallel to scanAxisNormal
const cross = vec3.cross(vec3.create(), viewPlaneNormal, scanAxisNormal);
const isAcquisitionPlane = vec3.length(cross) < EPSILON;
if (isAcquisitionPlane) {
const imageId = imageIds[imageIndex];
if (!imageId) {
return {};
}
const { instanceNumber } =
metaData.get('generalImageModule', imageId) || {};
return parseInt(instanceNumber);
}
}
CustomizableViewportOverlay.propTypes = {
viewportData: PropTypes.object,
imageIndex: PropTypes.number,
viewportIndex: PropTypes.number,
};
export default CustomizableViewportOverlay;

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { Enums, Types, utilities } from '@cornerstonejs/core'; import { Enums, Types, utilities } from '@cornerstonejs/core';
import { utilities as csToolsUtils } from '@cornerstonejs/tools'; import { utilities as csToolsUtils } from '@cornerstonejs/tools';
import { ImageScrollbar } from '@ohif/ui'; import { ImageScrollbar } from '@ohif/ui';
import { ServicesManger } from '@ohif/core';
function CornerstoneImageScrollbar({ function CornerstoneImageScrollbar({
viewportData, viewportData,
@ -13,24 +14,27 @@ function CornerstoneImageScrollbar({
scrollbarHeight, scrollbarHeight,
servicesManager, servicesManager,
}) { }) {
const { CineService, CornerstoneViewportService } = servicesManager.services; const {
cineService,
cornerstoneViewportService,
} = (servicesManager as ServicesManger).services;
const onImageScrollbarChange = (imageIndex, viewportIndex) => { const onImageScrollbarChange = (imageIndex, viewportIndex) => {
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex( const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex viewportIndex
); );
const viewportId = viewportInfo.getViewportId(); const viewportId = viewportInfo.getViewportId();
const viewport = CornerstoneViewportService.getCornerstoneViewport( const viewport = cornerstoneViewportService.getCornerstoneViewport(
viewportId viewportId
); );
const { isCineEnabled } = CineService.getState(); const { isCineEnabled } = cineService.getState();
if (isCineEnabled) { if (isCineEnabled) {
// on image scrollbar change, stop the CINE if it is playing // on image scrollbar change, stop the CINE if it is playing
CineService.stopClip(element); cineService.stopClip(element);
CineService.setCine({ id: viewportIndex, isPlaying: false }); cineService.setCine({ id: viewportIndex, isPlaying: false });
} }
csToolsUtils.jumpToSlice(viewport.element, { csToolsUtils.jumpToSlice(viewport.element, {
@ -44,7 +48,7 @@ function CornerstoneImageScrollbar({
return; return;
} }
const viewport = CornerstoneViewportService.getCornerstoneViewportByIndex( const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex viewportIndex
); );
@ -144,6 +148,7 @@ CornerstoneImageScrollbar.propTypes = {
scrollbarHeight: PropTypes.string, scrollbarHeight: PropTypes.string,
imageSliceData: PropTypes.object.isRequired, imageSliceData: PropTypes.object.isRequired,
setImageSliceData: PropTypes.func.isRequired, setImageSliceData: PropTypes.func.isRequired,
servicesManager: PropTypes.object.isRequired,
}; };
export default CornerstoneImageScrollbar; export default CornerstoneImageScrollbar;

View File

@ -30,7 +30,7 @@ function ViewportOrientationMarkers({
const [rotation, setRotation] = useState(0); const [rotation, setRotation] = useState(0);
const [flipHorizontal, setFlipHorizontal] = useState(false); const [flipHorizontal, setFlipHorizontal] = useState(false);
const [flipVertical, setFlipVertical] = useState(false); const [flipVertical, setFlipVertical] = useState(false);
const { CornerstoneViewportService } = servicesManager.services; const { cornerstoneViewportService } = servicesManager.services;
useEffect(() => { useEffect(() => {
const cameraModifiedListener = ( const cameraModifiedListener = (
@ -114,7 +114,7 @@ function ViewportOrientationMarkers({
flipHorizontal flipHorizontal
); );
const ohifViewport = CornerstoneViewportService.getViewportInfoByIndex( const ohifViewport = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex viewportIndex
); );

View File

@ -3,6 +3,7 @@ import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core'; import { metaData, Enums, utilities } from '@cornerstonejs/core';
import { ViewportOverlay } from '@ohif/ui'; import { ViewportOverlay } from '@ohif/ui';
import { ServicesManager } from '@ohif/core';
const EPSILON = 1e-4; const EPSILON = 1e-4;
@ -14,8 +15,8 @@ function CornerstoneViewportOverlay({
servicesManager, servicesManager,
}) { }) {
const { const {
CornerstoneViewportService, cornerstoneViewportService,
ToolBarService, toolbarService,
} = servicesManager.services; } = servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null }); const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1); const [scale, setScale] = useState(1);
@ -25,19 +26,19 @@ function CornerstoneViewportOverlay({
* Initial toolbar state * Initial toolbar state
*/ */
useEffect(() => { useEffect(() => {
setActiveTools(ToolBarService.getActiveTools()); setActiveTools(toolbarService.getActiveTools());
}, []); }, []);
useEffect(() => { useEffect(() => {
let isMounted = true; let isMounted = true;
const { unsubscribe } = ToolBarService.subscribe( const { unsubscribe } = toolbarService.subscribe(
ToolBarService.EVENTS.TOOL_BAR_STATE_MODIFIED, toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
() => { () => {
if (!isMounted) { if (!isMounted) {
return; return;
} }
setActiveTools(ToolBarService.getActiveTools()); setActiveTools(toolbarService.getActiveTools());
} }
); );
@ -85,7 +86,7 @@ function CornerstoneViewportOverlay({
previousCamera.parallelScale !== camera.parallelScale || previousCamera.parallelScale !== camera.parallelScale ||
previousCamera.scale !== camera.scale previousCamera.scale !== camera.scale
) { ) {
const viewport = CornerstoneViewportService.getCornerstoneViewportByIndex( const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex viewportIndex
); );
@ -128,7 +129,7 @@ function CornerstoneViewportOverlay({
} }
return ( return (
<div className="flex flex-row"> <div className="flex flex-row text-base">
<span className="mr-1">W:</span> <span className="mr-1">W:</span>
<span className="ml-1 mr-2 font-light">{windowWidth.toFixed(0)}</span> <span className="ml-1 mr-2 font-light">{windowWidth.toFixed(0)}</span>
<span className="mr-1">L:</span> <span className="mr-1">L:</span>
@ -139,7 +140,7 @@ function CornerstoneViewportOverlay({
if (activeTools.includes('Zoom')) { if (activeTools.includes('Zoom')) {
return ( return (
<div className="flex flex-row"> <div className="flex flex-row text-base">
<span className="mr-1">Zoom:</span> <span className="mr-1">Zoom:</span>
<span className="font-light">{scale.toFixed(2)}x</span> <span className="font-light">{scale.toFixed(2)}x</span>
</div> </div>
@ -168,12 +169,12 @@ function CornerstoneViewportOverlay({
viewportData, viewportData,
imageIndex, imageIndex,
viewportIndex, viewportIndex,
CornerstoneViewportService cornerstoneViewportService
); );
} }
return ( return (
<div className="flex flex-row"> <div className="flex flex-row text-base">
<span className="mr-1">I:</span> <span className="mr-1">I:</span>
<span className="font-light"> <span className="font-light">
{instanceNumber !== undefined {instanceNumber !== undefined
@ -188,7 +189,7 @@ function CornerstoneViewportOverlay({
return null; return null;
} }
const ohifViewport = CornerstoneViewportService.getViewportInfoByIndex( const ohifViewport = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex viewportIndex
); );
@ -240,7 +241,7 @@ function _getInstanceNumberFromVolume(
viewportData, viewportData,
imageIndex, imageIndex,
viewportIndex, viewportIndex,
CornerstoneViewportService cornerstoneViewportService
) { ) {
const volumes = viewportData.volumes; const volumes = viewportData.volumes;
@ -252,7 +253,7 @@ function _getInstanceNumberFromVolume(
const volume = volumes[0]; const volume = volumes[0];
const { direction, imageIds } = volume; const { direction, imageIds } = volume;
const cornerstoneViewport = CornerstoneViewportService.getCornerstoneViewportByIndex( const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex viewportIndex
); );
@ -287,6 +288,7 @@ CornerstoneViewportOverlay.propTypes = {
viewportData: PropTypes.object, viewportData: PropTypes.object,
imageIndex: PropTypes.number, imageIndex: PropTypes.number,
viewportIndex: PropTypes.number, viewportIndex: PropTypes.number,
servicesManager: PropTypes.instanceOf(ServicesManager),
}; };
export default CornerstoneViewportOverlay; export default CornerstoneViewportOverlay;

View File

@ -0,0 +1,98 @@
import moment from 'moment';
import { metaData } from '@cornerstonejs/core';
/**
* Checks if value is valid.
*
* @param {number} value
* @returns {boolean} is valid.
*/
export function isValidNumber(value) {
return typeof value === 'number' && !isNaN(value);
}
/**
* Formats number precision.
*
* @param {number} number
* @param {number} precision
* @returns {number} formatted number.
*/
export function formatNumberPrecision(number, precision = 0) {
if (number !== null) {
return parseFloat(number).toFixed(precision);
}
}
/**
* Formats DICOM date.
*
* @param {string} date
* @param {string} strFormat
* @returns {string} formatted date.
*/
export function formatDICOMDate(date, strFormat = 'MMM D, YYYY') {
return moment(date, 'YYYYMMDD').format(strFormat);
}
/**
* DICOM Time is stored as HHmmss.SSS, where:
* HH 24 hour time:
* m mm 0..59 Minutes
* s ss 0..59 Seconds
* S SS SSS 0..999 Fractional seconds
*
* Goal: '24:12:12'
*
* @param {*} time
* @param {string} strFormat
* @returns {string} formatted name.
*/
export function formatDICOMTime(time, strFormat = 'HH:mm:ss') {
return moment(time, 'HH:mm:ss').format(strFormat);
}
/**
* Formats a patient name for display purposes
*
* @param {string} name
* @returns {string} formatted name.
*/
export function formatPN(name) {
if (!name) {
return '';
}
const cleaned = name
.split('^')
.filter(s => !!s)
.join(', ')
.trim();
return cleaned === ',' || cleaned === '' ? '' : cleaned;
}
/**
* Gets compression type
*
* @param {number} imageId
* @returns {string} comrpession type.
*/
export function getCompression(imageId) {
const generalImageModule = metaData.get('generalImageModule', imageId) || {};
const {
lossyImageCompression,
lossyImageCompressionRatio,
lossyImageCompressionMethod,
} = generalImageModule;
if (lossyImageCompression === '01' && lossyImageCompressionRatio !== '') {
const compressionMethod = lossyImageCompressionMethod || 'Lossy: ';
const compressionRatio = formatNumberPrecision(
lossyImageCompressionRatio,
2
);
return compressionMethod + compressionRatio + ' : 1';
}
return 'Lossless / Uncompressed';
}

View File

@ -1,6 +1,7 @@
import { import {
getEnabledElement, getEnabledElement,
StackViewport, StackViewport,
VolumeViewport,
utilities as csUtils, utilities as csUtils,
} from '@cornerstonejs/core'; } from '@cornerstonejs/core';
import { import {
@ -9,31 +10,29 @@ import {
utilities as cstUtils, utilities as cstUtils,
ReferenceLinesTool, ReferenceLinesTool,
} from '@cornerstonejs/tools'; } from '@cornerstonejs/tools';
import { ServicesManager } from '@ohif/core';
import { getEnabledElement as OHIFgetEnabledElement } from './state';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm'; import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import callInputDialog from './utils/callInputDialog'; import callInputDialog from './utils/callInputDialog';
import { setColormap } from './utils/colormap/transferFunctionHelpers'; import { setColormap } from './utils/colormap/transferFunctionHelpers';
import toggleMPRHangingProtocol from './utils/mpr/toggleMPRHangingProtocol'; import toggleMPRHangingProtocol from './utils/mpr/toggleMPRHangingProtocol';
import toggleStackImageSync from './utils/stackSync/toggleStackImageSync'; import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
const commandsModule = ({ servicesManager }) => { const commandsModule = ({ servicesManager }) => {
const { const {
ViewportGridService, viewportGridService,
ToolGroupService, toolGroupService,
CineService, cineService,
ToolBarService, toolbarService,
UIDialogService, uiDialogService,
CornerstoneViewportService, cornerstoneViewportService,
HangingProtocolService, hangingProtocolService,
UINotificationService, uiNotificationService,
} = servicesManager.services; } = (servicesManager as ServicesManager).services;
function _getActiveViewportEnabledElement() { function _getActiveViewportEnabledElement() {
const { activeViewportIndex } = ViewportGridService.getState(); return getActiveViewportEnabledElement(viewportGridService);
const { element } = OHIFgetEnabledElement(activeViewportIndex) || {};
const enabledElement = getEnabledElement(element);
return enabledElement;
} }
function _getToolGroup(toolGroupId) { function _getToolGroup(toolGroupId) {
@ -66,7 +65,7 @@ const commandsModule = ({ servicesManager }) => {
toolGroupIdToUse = toolGroup.id; toolGroupIdToUse = toolGroup.id;
} }
const toolGroup = ToolGroupService.getToolGroup(toolGroupIdToUse); const toolGroup = toolGroupService.getToolGroup(toolGroupIdToUse);
return toolGroup; return toolGroup;
} }
@ -75,7 +74,7 @@ const commandsModule = ({ servicesManager }) => {
return _getActiveViewportEnabledElement(); return _getActiveViewportEnabledElement();
}, },
setViewportActive: ({ viewportId }) => { setViewportActive: ({ viewportId }) => {
const viewportInfo = CornerstoneViewportService.getViewportInfo( const viewportInfo = cornerstoneViewportService.getViewportInfo(
viewportId viewportId
); );
if (!viewportInfo) { if (!viewportInfo) {
@ -84,18 +83,18 @@ const commandsModule = ({ servicesManager }) => {
} }
const viewportIndex = viewportInfo.getViewportIndex(); const viewportIndex = viewportInfo.getViewportIndex();
ViewportGridService.setActiveViewportIndex(viewportIndex); viewportGridService.setActiveViewportIndex(viewportIndex);
}, },
arrowTextCallback: ({ callback, data }) => { arrowTextCallback: ({ callback, data }) => {
callInputDialog(UIDialogService, data, callback); callInputDialog(uiDialogService, data, callback);
}, },
toggleCine: () => { toggleCine: () => {
const { viewports } = ViewportGridService.getState(); const { viewports } = viewportGridService.getState();
const { isCineEnabled } = CineService.getState(); const { isCineEnabled } = cineService.getState();
CineService.setIsCineEnabled(!isCineEnabled); cineService.setIsCineEnabled(!isCineEnabled);
ToolBarService.setButton('Cine', { props: { isActive: !isCineEnabled } }); toolbarService.setButton('Cine', { props: { isActive: !isCineEnabled } });
viewports.forEach((_, index) => viewports.forEach((_, index) =>
CineService.setCine({ id: index, isPlaying: false }) cineService.setCine({ id: index, isPlaying: false })
); );
}, },
setWindowLevel({ window, level, toolGroupId }) { setWindowLevel({ window, level, toolGroupId }) {
@ -104,7 +103,7 @@ const commandsModule = ({ servicesManager }) => {
const windowCenterNum = Number(level); const windowCenterNum = Number(level);
const { viewportId } = _getActiveViewportEnabledElement(); const { viewportId } = _getActiveViewportEnabledElement();
const viewportToolGroupId = ToolGroupService.getToolGroupForViewport( const viewportToolGroupId = toolGroupService.getToolGroupForViewport(
viewportId viewportId
); );
@ -113,7 +112,7 @@ const commandsModule = ({ servicesManager }) => {
} }
// get actor from the viewport // get actor from the viewport
const renderingEngine = CornerstoneViewportService.getRenderingEngine(); const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const viewport = renderingEngine.getViewport(viewportId); const viewport = renderingEngine.getViewport(viewportId);
const { lower, upper } = csUtils.windowLevel.toLowHighRange( const { lower, upper } = csUtils.windowLevel.toLowHighRange(
@ -134,7 +133,7 @@ const commandsModule = ({ servicesManager }) => {
const activeViewportToolGroup = _getToolGroup(null); const activeViewportToolGroup = _getToolGroup(null);
if (!activeViewportToolGroup._toolInstances.Crosshairs) { if (!activeViewportToolGroup._toolInstances.Crosshairs) {
UINotificationService.show({ uiNotificationService.show({
title: 'Crosshairs', title: 'Crosshairs',
message: message:
'You need to be in a MPR view to use Crosshairs. Click on MPR button in the toolbar to activate it.', 'You need to be in a MPR view to use Crosshairs. Click on MPR button in the toolbar to activate it.',
@ -146,7 +145,7 @@ const commandsModule = ({ servicesManager }) => {
} }
} }
const { viewports } = ViewportGridService.getState() || { const { viewports } = viewportGridService.getState() || {
viewports: [], viewports: [],
}; };
@ -193,17 +192,17 @@ const commandsModule = ({ servicesManager }) => {
}); });
}, },
showDownloadViewportModal: () => { showDownloadViewportModal: () => {
const { activeViewportIndex } = ViewportGridService.getState(); const { activeViewportIndex } = viewportGridService.getState();
const { UIModalService } = servicesManager.services; const { uiModalService } = servicesManager.services;
if (UIModalService) { if (uiModalService) {
UIModalService.show({ uiModalService.show({
content: CornerstoneViewportDownloadForm, content: CornerstoneViewportDownloadForm,
title: 'Download High Quality Image', title: 'Download High Quality Image',
contentProps: { contentProps: {
activeViewportIndex, activeViewportIndex,
onClose: UIModalService.hide, onClose: uiModalService.hide,
CornerstoneViewportService, cornerstoneViewportService,
}, },
}); });
} }
@ -313,6 +312,51 @@ const commandsModule = ({ servicesManager }) => {
} }
} }
}, },
firstImage: () => {
// Get current active viewport (return if none active)
const enabledElement = _getActiveViewportEnabledElement();
if (!enabledElement) {
return;
}
const { viewport } = enabledElement;
// Check viewport is supported
if (
viewport! instanceof StackViewport &&
viewport! instanceof VolumeViewport
) {
throw new Error('Unsupported viewport type');
}
// Set slice to first slice
const options = { imageIndex: 0 };
cstUtils.jumpToSlice(viewport.element, options);
},
lastImage: () => {
// Get current active viewport (return if none active)
const enabledElement = _getActiveViewportEnabledElement();
if (!enabledElement) {
return;
}
const { viewport } = enabledElement;
// Get number of slices
// -> Copied from cornerstone3D jumpToSlice\_getImageSliceData()
let numberOfSlices = 0;
if (viewport instanceof StackViewport) {
numberOfSlices = viewport.getImageIds().length;
} else if (viewport instanceof VolumeViewport) {
numberOfSlices = csUtils.getImageSliceDataForVolumeViewport(viewport)
.numberOfSlices;
} else {
throw new Error('Unsupported viewport type');
}
// Set slice to last slice
const options = { imageIndex: numberOfSlices - 1 };
cstUtils.jumpToSlice(viewport.element, options);
},
scroll: ({ direction }) => { scroll: ({ direction }) => {
const enabledElement = _getActiveViewportEnabledElement(); const enabledElement = _getActiveViewportEnabledElement();
@ -331,7 +375,7 @@ const commandsModule = ({ servicesManager }) => {
colormap, colormap,
immediate = false, immediate = false,
}) => { }) => {
const viewport = CornerstoneViewportService.getCornerstoneViewportByIndex( const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
viewportIndex viewportIndex
); );
@ -350,18 +394,18 @@ const commandsModule = ({ servicesManager }) => {
} }
}, },
incrementActiveViewport: () => { incrementActiveViewport: () => {
const { activeViewportIndex, viewports } = ViewportGridService.getState(); const { activeViewportIndex, viewports } = viewportGridService.getState();
const nextViewportIndex = (activeViewportIndex + 1) % viewports.length; const nextViewportIndex = (activeViewportIndex + 1) % viewports.length;
ViewportGridService.setActiveViewportIndex(nextViewportIndex); viewportGridService.setActiveViewportIndex(nextViewportIndex);
}, },
decrementActiveViewport: () => { decrementActiveViewport: () => {
const { activeViewportIndex, viewports } = ViewportGridService.getState(); const { activeViewportIndex, viewports } = viewportGridService.getState();
const nextViewportIndex = const nextViewportIndex =
(activeViewportIndex - 1 + viewports.length) % viewports.length; (activeViewportIndex - 1 + viewports.length) % viewports.length;
ViewportGridService.setActiveViewportIndex(nextViewportIndex); viewportGridService.setActiveViewportIndex(nextViewportIndex);
}, },
setHangingProtocol: ({ protocolId }) => { setHangingProtocol: ({ protocolId }) => {
HangingProtocolService.setProtocol(protocolId); hangingProtocolService.setProtocol(protocolId);
}, },
toggleMPR: ({ toggledState }) => { toggleMPR: ({ toggledState }) => {
toggleMPRHangingProtocol({ toggleMPRHangingProtocol({
@ -378,13 +422,13 @@ const commandsModule = ({ servicesManager }) => {
}); });
}, },
toggleReferenceLines: ({ toggledState }) => { toggleReferenceLines: ({ toggledState }) => {
const { activeViewportIndex } = ViewportGridService.getState(); const { activeViewportIndex } = viewportGridService.getState();
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex( const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
activeViewportIndex activeViewportIndex
); );
const viewportId = viewportInfo.getViewportId(); const viewportId = viewportInfo.getViewportId();
const toolGroup = ToolGroupService.getToolGroupForViewport(viewportId); const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toggledState) { if (!toggledState) {
toolGroup.setToolDisabled(ReferenceLinesTool.toolName); toolGroup.setToolDisabled(ReferenceLinesTool.toolName);
@ -475,6 +519,16 @@ const commandsModule = ({ servicesManager }) => {
storeContexts: [], storeContexts: [],
options: { direction: -1 }, options: { direction: -1 },
}, },
firstImage: {
commandFn: actions.firstImage,
storeContexts: [],
options: {},
},
lastImage: {
commandFn: actions.lastImage,
storeContexts: [],
options: {},
},
showDownloadViewportModal: { showDownloadViewportModal: {
commandFn: actions.showDownloadViewportModal, commandFn: actions.showDownloadViewportModal,
storeContexts: [], storeContexts: [],

View File

@ -8,6 +8,7 @@ const mpr = {
availableTo: {}, availableTo: {},
editableBy: {}, editableBy: {},
protocolMatchingRules: [], protocolMatchingRules: [],
imageLoadStrategy: 'nth',
displaySetSelectors: { displaySetSelectors: {
mprDisplaySet: { mprDisplaySet: {
seriesMatchingRules: [ seriesMatchingRules: [

View File

@ -7,6 +7,8 @@ import {
imageRetrievalPoolManager, imageRetrievalPoolManager,
} from '@cornerstonejs/core'; } from '@cornerstonejs/core';
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools'; import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
import { Types } from '@ohif/core';
import init from './init'; import init from './init';
import commandsModule from './commandsModule'; import commandsModule from './commandsModule';
import getHangingProtocolModule from './getHangingProtocolModule'; import getHangingProtocolModule from './getHangingProtocolModule';
@ -23,6 +25,7 @@ import { registerColormap } from './utils/colormap/transferFunctionHelpers';
import { id } from './id'; import { id } from './id';
import * as csWADOImageLoader from './initWADOImageLoader.js'; import * as csWADOImageLoader from './initWADOImageLoader.js';
import { measurementMappingUtils } from './utils/measurementServiceMappings';
const Component = React.lazy(() => { const Component = React.lazy(() => {
return import( return import(
@ -41,7 +44,7 @@ const OHIFCornerstoneViewport = props => {
/** /**
* *
*/ */
const cornerstoneExtension = { const cornerstoneExtension: Types.Extensions.Extension = {
/** /**
* Only required property. Should be a unique value across all extensions. * Only required property. Should be a unique value across all extensions.
*/ */
@ -60,10 +63,9 @@ const cornerstoneExtension = {
}, },
/** /**
* Register the Cornerstone 3D services and set them up for use.
* *
* * @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools`
* @param {object} [configuration={}]
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
*/ */
async preRegistration({ async preRegistration({
servicesManager, servicesManager,
@ -71,13 +73,18 @@ const cornerstoneExtension = {
configuration = {}, configuration = {},
appConfig, appConfig,
}) { }) {
// Todo: we should be consistent with how services get registered. Use REGISTRATION static method for all
servicesManager.registerService( servicesManager.registerService(
CornerstoneViewportService(servicesManager) CornerstoneViewportService(servicesManager)
); );
servicesManager.registerService(ToolGroupService(servicesManager)); servicesManager.registerService(
ToolGroupService.REGISTRATION(servicesManager)
);
servicesManager.registerService(SyncGroupService(servicesManager)); servicesManager.registerService(SyncGroupService(servicesManager));
servicesManager.registerService(SegmentationService(servicesManager)); servicesManager.registerService(SegmentationService(servicesManager));
servicesManager.registerService(CornerstoneCacheService(servicesManager)); servicesManager.registerService(
CornerstoneCacheService.REGISTRATION(servicesManager)
);
await init({ servicesManager, commandsManager, configuration, appConfig }); await init({ servicesManager, commandsManager, configuration, appConfig });
}, },
@ -87,12 +94,12 @@ const cornerstoneExtension = {
// const onNewImageHandler = jumpData => { // const onNewImageHandler = jumpData => {
// commandsManager.runCommand('jumpToImage', jumpData); // commandsManager.runCommand('jumpToImage', jumpData);
// }; // };
const { ToolBarService } = servicesManager.services; const { ToolbarService } = servicesManager.services;
return ( return (
<OHIFCornerstoneViewport <OHIFCornerstoneViewport
{...props} {...props}
ToolBarService={ToolBarService} ToolbarService={ToolbarService}
servicesManager={servicesManager} servicesManager={servicesManager}
commandsManager={commandsManager} commandsManager={commandsManager}
/> />
@ -144,3 +151,4 @@ const cornerstoneExtension = {
}; };
export default cornerstoneExtension; export default cornerstoneExtension;
export { measurementMappingUtils };

View File

@ -10,15 +10,12 @@ import {
EVENTS, EVENTS,
metaData, metaData,
volumeLoader, volumeLoader,
imageLoader,
imageLoadPoolManager, imageLoadPoolManager,
Settings, Settings,
utilities as csUtilities,
} from '@cornerstonejs/core'; } from '@cornerstonejs/core';
import { Enums, utilities, ReferenceLinesTool } from '@cornerstonejs/tools'; import { Enums, utilities, ReferenceLinesTool } from '@cornerstonejs/tools';
import { import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
cornerstoneStreamingImageVolumeLoader,
sharedArrayBufferImageLoader,
} from '@cornerstonejs/streaming-image-volume-loader';
import initWADOImageLoader from './initWADOImageLoader'; import initWADOImageLoader from './initWADOImageLoader';
import initCornerstoneTools from './initCornerstoneTools'; import initCornerstoneTools from './initCornerstoneTools';
@ -27,6 +24,7 @@ import { connectToolsToMeasurementService } from './initMeasurementService';
import callInputDialog from './utils/callInputDialog'; import callInputDialog from './utils/callInputDialog';
import initCineService from './initCineService'; import initCineService from './initCineService';
import interleaveCenterLoader from './utils/interleaveCenterLoader'; import interleaveCenterLoader from './utils/interleaveCenterLoader';
import nthLoader from './utils/nthLoader';
import interleaveTopToBottom from './utils/interleaveTopToBottom'; import interleaveTopToBottom from './utils/interleaveTopToBottom';
const cs3DToolsEvents = Enums.Events; const cs3DToolsEvents = Enums.Events;
@ -65,23 +63,26 @@ export default async function init({
); );
const { const {
UserAuthenticationService, userAuthenticationService,
MeasurementService, measurementService,
DisplaySetService, displaySetService,
UIDialogService, uiDialogService,
UIModalService, uiModalService,
UINotificationService, uiNotificationService,
CineService, cineService,
CornerstoneViewportService, cornerstoneViewportService,
HangingProtocolService, hangingProtocolService,
ToolGroupService, toolGroupService,
ViewportGridService, viewportGridService,
} = servicesManager.services; } = servicesManager.services;
window.services = servicesManager.services; window.services = servicesManager.services;
if (!window.crossOriginIsolated) { if (
UINotificationService.show({ appConfig.showWarningMessageForCrossOrigin &&
!window.crossOriginIsolated
) {
uiNotificationService.show({
title: 'Cross Origin Isolation', title: 'Cross Origin Isolation',
message: message:
'Cross Origin Isolation is not enabled, volume rendering will not work (e.g., MPR)', 'Cross Origin Isolation is not enabled, volume rendering will not work (e.g., MPR)',
@ -89,8 +90,11 @@ export default async function init({
}); });
} }
if (cornerstone.getShouldUseCPURendering()) { if (
_showCPURenderingModal(UIModalService, HangingProtocolService); appConfig.showCPUFallbackMessage &&
cornerstone.getShouldUseCPURendering()
) {
_showCPURenderingModal(uiModalService, hangingProtocolService);
} }
const labelmapRepresentation = const labelmapRepresentation =
@ -113,20 +117,22 @@ export default async function init({
cornerstoneStreamingImageVolumeLoader cornerstoneStreamingImageVolumeLoader
); );
HangingProtocolService.registerImageLoadStrategy( hangingProtocolService.registerImageLoadStrategy(
'interleaveCenter', 'interleaveCenter',
interleaveCenterLoader interleaveCenterLoader
); );
HangingProtocolService.registerImageLoadStrategy( hangingProtocolService.registerImageLoadStrategy(
'interleaveTopToBottom', 'interleaveTopToBottom',
interleaveTopToBottom interleaveTopToBottom
); );
hangingProtocolService.registerImageLoadStrategy('nth', nthLoader);
imageLoader.registerImageLoader( // add metadata providers
'streaming-wadors', metaData.addProvider(
sharedArrayBufferImageLoader csUtilities.calibratedPixelSpacingMetadataProvider.get.bind(
); csUtilities.calibratedPixelSpacingMetadataProvider
)
); // this provider is required for Calibration tool
metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999); metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999);
imageLoadPoolManager.maxNumRequests = { imageLoadPoolManager.maxNumRequests = {
@ -135,16 +141,14 @@ export default async function init({
prefetch: appConfig?.maxNumRequests?.prefetch || 10, prefetch: appConfig?.maxNumRequests?.prefetch || 10,
}; };
initWADOImageLoader(UserAuthenticationService, appConfig); initWADOImageLoader(userAuthenticationService, appConfig);
/* Measurement Service */ /* Measurement Service */
const measurementServiceSource = connectToolsToMeasurementService( const measurementServiceSource = connectToolsToMeasurementService(
MeasurementService, servicesManager
DisplaySetService,
CornerstoneViewportService
); );
initCineService(CineService); initCineService(cineService);
const _getDefaultPosition = event => ({ const _getDefaultPosition = event => ({
x: (event && event.currentPoints.client[0]) || 0, x: (event && event.currentPoints.client[0]) || 0,
@ -152,7 +156,7 @@ export default async function init({
}); });
const onRightClick = event => { const onRightClick = event => {
if (!UIDialogService) { if (!uiDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.'); console.warn('Unable to show dialog; no UI Dialog Service available.');
return; return;
} }
@ -179,15 +183,15 @@ export default async function init({
CONTEXT_MENU_OPEN = true; CONTEXT_MENU_OPEN = true;
UIDialogService.dismiss({ id: 'context-menu' }); uiDialogService.dismiss({ id: 'context-menu' });
UIDialogService.create({ uiDialogService.create({
id: 'context-menu', id: 'context-menu',
isDraggable: false, isDraggable: false,
preservePosition: false, preservePosition: false,
defaultPosition: _getDefaultPosition(event.detail), defaultPosition: _getDefaultPosition(event.detail),
content: ContextMenuMeasurements, content: ContextMenuMeasurements,
onClickOutside: () => { onClickOutside: () => {
UIDialogService.dismiss({ id: 'context-menu' }); uiDialogService.dismiss({ id: 'context-menu' });
CONTEXT_MENU_OPEN = false; CONTEXT_MENU_OPEN = false;
}, },
contentProps: { contentProps: {
@ -207,15 +211,15 @@ export default async function init({
}, },
onClose: () => { onClose: () => {
CONTEXT_MENU_OPEN = false; CONTEXT_MENU_OPEN = false;
UIDialogService.dismiss({ id: 'context-menu' }); uiDialogService.dismiss({ id: 'context-menu' });
}, },
onSetLabel: item => { onSetLabel: item => {
const { annotationUID } = item.value; const { annotationUID } = item.value;
const measurement = MeasurementService.getMeasurement(annotationUID); const measurement = measurementService.getMeasurement(annotationUID);
callInputDialog( callInputDialog(
UIDialogService, uiDialogService,
measurement, measurement,
(label, actionId) => { (label, actionId) => {
if (actionId === 'cancel') { if (actionId === 'cancel') {
@ -226,7 +230,7 @@ export default async function init({
label, label,
}); });
MeasurementService.update( measurementService.update(
updatedMeasurement.uid, updatedMeasurement.uid,
updatedMeasurement, updatedMeasurement,
true true
@ -242,27 +246,27 @@ export default async function init({
}; };
const resetContextMenu = () => { const resetContextMenu = () => {
if (!UIDialogService) { if (!uiDialogService) {
console.warn('Unable to show dialog; no UI Dialog Service available.'); console.warn('Unable to show dialog; no UI Dialog Service available.');
return; return;
} }
CONTEXT_MENU_OPEN = false; CONTEXT_MENU_OPEN = false;
UIDialogService.dismiss({ id: 'context-menu' }); uiDialogService.dismiss({ id: 'context-menu' });
}; };
// When a custom image load is performed, update the relevant viewports // When a custom image load is performed, update the relevant viewports
HangingProtocolService.subscribe( hangingProtocolService.subscribe(
HangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED, hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,
volumeInputArrayMap => { volumeInputArrayMap => {
for (const entry of volumeInputArrayMap.entries()) { for (const entry of volumeInputArrayMap.entries()) {
const [viewportId, volumeInputArray] = entry; const [viewportId, volumeInputArray] = entry;
const viewport = CornerstoneViewportService.getCornerstoneViewport( const viewport = cornerstoneViewportService.getCornerstoneViewport(
viewportId viewportId
); );
CornerstoneViewportService.setVolumesForViewport( cornerstoneViewportService.setVolumesForViewport(
viewport, viewport,
volumeInputArray volumeInputArray
); );
@ -363,11 +367,11 @@ export default async function init({
elementDisabledHandler.bind(null) elementDisabledHandler.bind(null)
); );
ViewportGridService.subscribe( viewportGridService.subscribe(
ViewportGridService.EVENTS.ACTIVE_VIEWPORT_INDEX_CHANGED, viewportGridService.EVENTS.ACTIVE_VIEWPORT_INDEX_CHANGED,
({ viewportIndex }) => { ({ viewportIndex }) => {
const viewportId = `viewport-${viewportIndex}`; const viewportId = `viewport-${viewportIndex}`;
const toolGroup = ToolGroupService.getToolGroupForViewport(viewportId); const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup || !toolGroup._toolInstances?.['ReferenceLines']) { if (!toolGroup || !toolGroup._toolInstances?.['ReferenceLines']) {
return; return;
@ -410,10 +414,10 @@ function CPUModal() {
); );
} }
function _showCPURenderingModal(UIModalService, HangingProtocolService) { function _showCPURenderingModal(uiModalService, hangingProtocolService) {
const callback = progress => { const callback = progress => {
if (progress === 100) { if (progress === 100) {
UIModalService.show({ uiModalService.show({
content: CPUModal, content: CPUModal,
title: 'OHIF Fell Back to CPU Rendering', title: 'OHIF Fell Back to CPU Rendering',
}); });
@ -422,8 +426,8 @@ function _showCPURenderingModal(UIModalService, HangingProtocolService) {
} }
}; };
const { unsubscribe } = HangingProtocolService.subscribe( const { unsubscribe } = hangingProtocolService.subscribe(
HangingProtocolService.EVENTS.HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT, hangingProtocolService.EVENTS.HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT,
({ progress }) => { ({ progress }) => {
const done = callback(progress); const done = callback(progress);

View File

@ -1,6 +1,6 @@
import { utilities } from '@cornerstonejs/tools'; import { utilities } from '@cornerstonejs/tools';
function initCineService(CineService) { function initCineService(cineService) {
const playClip = (element, playClipOptions) => { const playClip = (element, playClipOptions) => {
return utilities.cine.playClip(element, playClipOptions); return utilities.cine.playClip(element, playClipOptions);
}; };
@ -9,7 +9,7 @@ function initCineService(CineService) {
return utilities.cine.stopClip(element); return utilities.cine.stopClip(element);
}; };
CineService.setServiceImplementation({ playClip, stopClip }); cineService.setServiceImplementation({ playClip, stopClip });
} }
export default initCineService; export default initCineService;

View File

@ -14,6 +14,8 @@ import {
DragProbeTool, DragProbeTool,
ProbeTool, ProbeTool,
AngleTool, AngleTool,
CobbAngleTool,
PlanarFreehandROITool,
MagnifyTool, MagnifyTool,
CrosshairsTool, CrosshairsTool,
SegmentationDisplayTool, SegmentationDisplayTool,
@ -23,6 +25,8 @@ import {
ReferenceLinesTool, ReferenceLinesTool,
} from '@cornerstonejs/tools'; } from '@cornerstonejs/tools';
import CalibrationLineTool from './tools/CalibrationLineTool';
export default function initCornerstoneTools(configuration = {}) { export default function initCornerstoneTools(configuration = {}) {
init(configuration); init(configuration);
addTool(PanTool); addTool(PanTool);
@ -40,10 +44,13 @@ export default function initCornerstoneTools(configuration = {}) {
addTool(ArrowAnnotateTool); addTool(ArrowAnnotateTool);
addTool(DragProbeTool); addTool(DragProbeTool);
addTool(AngleTool); addTool(AngleTool);
addTool(CobbAngleTool);
addTool(PlanarFreehandROITool);
addTool(MagnifyTool); addTool(MagnifyTool);
addTool(CrosshairsTool); addTool(CrosshairsTool);
addTool(SegmentationDisplayTool); addTool(SegmentationDisplayTool);
addTool(ReferenceLinesTool); addTool(ReferenceLinesTool);
addTool(CalibrationLineTool);
// Modify annotation tools to use dashed lines on SR // Modify annotation tools to use dashed lines on SR
const annotationStyle = { const annotationStyle = {
@ -76,10 +83,13 @@ const toolNames = {
EllipticalROI: EllipticalROITool.toolName, EllipticalROI: EllipticalROITool.toolName,
Bidirectional: BidirectionalTool.toolName, Bidirectional: BidirectionalTool.toolName,
Angle: AngleTool.toolName, Angle: AngleTool.toolName,
CobbAngle: CobbAngleTool.toolName,
PlanarFreehandROI: PlanarFreehandROITool.toolName,
Magnify: MagnifyTool.toolName, Magnify: MagnifyTool.toolName,
Crosshairs: CrosshairsTool.toolName, Crosshairs: CrosshairsTool.toolName,
SegmentationDisplay: SegmentationDisplayTool.toolName, SegmentationDisplay: SegmentationDisplayTool.toolName,
ReferenceLines: ReferenceLinesTool.toolName, ReferenceLines: ReferenceLinesTool.toolName,
CalibrationLine: CalibrationLineTool.toolName,
}; };
export { toolNames }; export { toolNames };

View File

@ -1,6 +1,8 @@
import { eventTarget } from '@cornerstonejs/core'; import { eventTarget } from '@cornerstonejs/core';
import { Enums, annotation } from '@cornerstonejs/tools'; import { Enums, annotation } from '@cornerstonejs/tools';
import { DicomMetadataStore } from '@ohif/core'; import { DicomMetadataStore } from '@ohif/core';
import { toolNames } from './initCornerstoneTools';
import { onCompletedCalibrationLine } from './tools/CalibrationLineTool';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory'; import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes'; import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
@ -13,9 +15,9 @@ const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1'; const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const initMeasurementService = ( const initMeasurementService = (
MeasurementService, measurementService,
DisplaySetService, displaySetService,
CornerstoneViewportService cornerstoneViewportService
) => { ) => {
/* Initialization */ /* Initialization */
const { const {
@ -23,18 +25,22 @@ const initMeasurementService = (
Bidirectional, Bidirectional,
EllipticalROI, EllipticalROI,
ArrowAnnotate, ArrowAnnotate,
Angle,
CobbAngle,
RectangleROI,
PlanarFreehandROI,
} = measurementServiceMappingsFactory( } = measurementServiceMappingsFactory(
MeasurementService, measurementService,
DisplaySetService, displaySetService,
CornerstoneViewportService cornerstoneViewportService
); );
const csTools3DVer1MeasurementSource = MeasurementService.createSource( const csTools3DVer1MeasurementSource = measurementService.createSource(
CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION CORNERSTONE_3D_TOOLS_SOURCE_VERSION
); );
/* Mappings */ /* Mappings */
MeasurementService.addMapping( measurementService.addMapping(
csTools3DVer1MeasurementSource, csTools3DVer1MeasurementSource,
'Length', 'Length',
Length.matchingCriteria, Length.matchingCriteria,
@ -42,7 +48,7 @@ const initMeasurementService = (
Length.toMeasurement Length.toMeasurement
); );
MeasurementService.addMapping( measurementService.addMapping(
csTools3DVer1MeasurementSource, csTools3DVer1MeasurementSource,
'Bidirectional', 'Bidirectional',
Bidirectional.matchingCriteria, Bidirectional.matchingCriteria,
@ -50,7 +56,7 @@ const initMeasurementService = (
Bidirectional.toMeasurement Bidirectional.toMeasurement
); );
MeasurementService.addMapping( measurementService.addMapping(
csTools3DVer1MeasurementSource, csTools3DVer1MeasurementSource,
'EllipticalROI', 'EllipticalROI',
EllipticalROI.matchingCriteria, EllipticalROI.matchingCriteria,
@ -58,7 +64,7 @@ const initMeasurementService = (
EllipticalROI.toMeasurement EllipticalROI.toMeasurement
); );
MeasurementService.addMapping( measurementService.addMapping(
csTools3DVer1MeasurementSource, csTools3DVer1MeasurementSource,
'ArrowAnnotate', 'ArrowAnnotate',
ArrowAnnotate.matchingCriteria, ArrowAnnotate.matchingCriteria,
@ -66,22 +72,65 @@ const initMeasurementService = (
ArrowAnnotate.toMeasurement ArrowAnnotate.toMeasurement
); );
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'CobbAngle',
CobbAngle.matchingCriteria,
CobbAngle.toAnnotation,
CobbAngle.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'Angle',
Angle.matchingCriteria,
Angle.toAnnotation,
Angle.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'RectangleROI',
RectangleROI.matchingCriteria,
RectangleROI.toAnnotation,
RectangleROI.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'PlanarFreehandROI',
PlanarFreehandROI.matchingCriteria,
PlanarFreehandROI.toAnnotation,
PlanarFreehandROI.toMeasurement
);
// On the UI side, the Calibration Line tool will work almost the same as the
// Length tool
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'CalibrationLine',
Length.matchingCriteria,
Length.toAnnotation,
Length.toMeasurement
);
return csTools3DVer1MeasurementSource; return csTools3DVer1MeasurementSource;
}; };
const connectToolsToMeasurementService = ( const connectToolsToMeasurementService = servicesManager => {
MeasurementService, const {
DisplaySetService, measurementService,
CornerstoneViewportService displaySetService,
) => { cornerstoneViewportService,
} = servicesManager.services;
const csTools3DVer1MeasurementSource = initMeasurementService( const csTools3DVer1MeasurementSource = initMeasurementService(
MeasurementService, measurementService,
DisplaySetService, displaySetService,
CornerstoneViewportService cornerstoneViewportService
); );
connectMeasurementServiceToTools( connectMeasurementServiceToTools(
MeasurementService, measurementService,
CornerstoneViewportService, cornerstoneViewportService,
csTools3DVer1MeasurementSource csTools3DVer1MeasurementSource
); );
const { annotationToMeasurement, remove } = csTools3DVer1MeasurementSource; const { annotationToMeasurement, remove } = csTools3DVer1MeasurementSource;
@ -95,15 +144,38 @@ const connectToolsToMeasurementService = (
} = annotationAddedEventDetail; } = annotationAddedEventDetail;
const { toolName } = metadata; const { toolName } = metadata;
// To force the measurementUID be the same as the annotationUID if (
// Todo: this should be changed when a measurement can include multiple annotations csToolsEvent.type === completedEvt &&
// in the future toolName === toolNames.CalibrationLine
annotationAddedEventDetail.uid = annotationUID; ) {
annotationToMeasurement(toolName, annotationAddedEventDetail); // show modal to input the measurement (mm)
onCompletedCalibrationLine(servicesManager, csToolsEvent)
.then(
() => {
console.log('calibration applied');
},
() => true
)
.finally(() => {
// we don't need the calibration line lingering around, remove the
// annotation from the display
removeAnnotation(annotationUID);
removeMeasurement(csToolsEvent);
// this will ensure redrawing of annotations
cornerstoneViewportService.resize();
});
} else {
// To force the measurementUID be the same as the annotationUID
// Todo: this should be changed when a measurement can include multiple annotations
// in the future
annotationAddedEventDetail.uid = annotationUID;
annotationToMeasurement(toolName, annotationAddedEventDetail);
}
} catch (error) { } catch (error) {
console.warn('Failed to update measurement:', error); console.warn('Failed to update measurement:', error);
} }
} }
function updateMeasurement(csToolsEvent) { function updateMeasurement(csToolsEvent) {
try { try {
const annotationModifiedEventDetail = csToolsEvent.detail; const annotationModifiedEventDetail = csToolsEvent.detail;
@ -113,7 +185,7 @@ const connectToolsToMeasurementService = (
} = annotationModifiedEventDetail; } = annotationModifiedEventDetail;
// If the measurement hasn't been added, don't modify it // If the measurement hasn't been added, don't modify it
const measurement = MeasurementService.getMeasurement(annotationUID); const measurement = measurementService.getMeasurement(annotationUID);
if (!measurement) { if (!measurement) {
return; return;
@ -121,11 +193,36 @@ const connectToolsToMeasurementService = (
const { toolName } = metadata; const { toolName } = metadata;
annotationModifiedEventDetail.uid = annotationUID; annotationModifiedEventDetail.uid = annotationUID;
annotationToMeasurement(toolName, annotationModifiedEventDetail); // Passing true to indicate this is an update and NOT a annotation (start) completion.
annotationToMeasurement(toolName, annotationModifiedEventDetail, true);
} catch (error) { } catch (error) {
console.warn('Failed to update measurement:', error); console.warn('Failed to update measurement:', error);
} }
} }
function selectMeasurement(csToolsEvent) {
try {
const annotationSelectionEventDetail = csToolsEvent.detail;
const {
added: addedSelectedAnnotationUIDs,
removed: removedSelectedAnnotationUIDs,
} = annotationSelectionEventDetail;
if (removedSelectedAnnotationUIDs) {
removedSelectedAnnotationUIDs.forEach(annotationUID =>
measurementService.setMeasurementSelected(annotationUID, false)
);
}
if (addedSelectedAnnotationUIDs) {
addedSelectedAnnotationUIDs.forEach(annotationUID =>
measurementService.setMeasurementSelected(annotationUID, true)
);
}
} catch (error) {
console.warn('Failed to select and unselect measurements:', error);
}
}
/** /**
* When csTools fires a removed event, remove the same measurement * When csTools fires a removed event, remove the same measurement
@ -141,7 +238,7 @@ const connectToolsToMeasurementService = (
annotation: { annotationUID }, annotation: { annotationUID },
} = annotationRemovedEventDetail; } = annotationRemovedEventDetail;
const measurement = MeasurementService.getMeasurement(annotationUID); const measurement = measurementService.getMeasurement(annotationUID);
if (measurement) { if (measurement) {
console.log('~~ removeEvt', csToolsEvent); console.log('~~ removeEvt', csToolsEvent);
@ -157,20 +254,24 @@ const connectToolsToMeasurementService = (
// on display sets added, check if there are any measurements in measurement service that need to be // on display sets added, check if there are any measurements in measurement service that need to be
// put into cornerstone tools // put into cornerstone tools
const addedEvt = csToolsEvents.ANNOTATION_ADDED;
const completedEvt = csToolsEvents.ANNOTATION_COMPLETED; const completedEvt = csToolsEvents.ANNOTATION_COMPLETED;
const updatedEvt = csToolsEvents.ANNOTATION_MODIFIED; const updatedEvt = csToolsEvents.ANNOTATION_MODIFIED;
const removedEvt = csToolsEvents.ANNOTATION_REMOVED; const removedEvt = csToolsEvents.ANNOTATION_REMOVED;
const selectionEvt = csToolsEvents.ANNOTATION_SELECTION_CHANGE;
eventTarget.addEventListener(addedEvt, addMeasurement);
eventTarget.addEventListener(completedEvt, addMeasurement); eventTarget.addEventListener(completedEvt, addMeasurement);
eventTarget.addEventListener(updatedEvt, updateMeasurement); eventTarget.addEventListener(updatedEvt, updateMeasurement);
eventTarget.addEventListener(removedEvt, removeMeasurement); eventTarget.addEventListener(removedEvt, removeMeasurement);
eventTarget.addEventListener(selectionEvt, selectMeasurement);
return csTools3DVer1MeasurementSource; return csTools3DVer1MeasurementSource;
}; };
const connectMeasurementServiceToTools = ( const connectMeasurementServiceToTools = (
MeasurementService, measurementService,
CornerstoneViewportService, cornerstoneViewportService,
measurementSource measurementSource
) => { ) => {
const { const {
@ -178,14 +279,14 @@ const connectMeasurementServiceToTools = (
MEASUREMENTS_CLEARED, MEASUREMENTS_CLEARED,
MEASUREMENT_UPDATED, MEASUREMENT_UPDATED,
RAW_MEASUREMENT_ADDED, RAW_MEASUREMENT_ADDED,
} = MeasurementService.EVENTS; } = measurementService.EVENTS;
const csTools3DVer1MeasurementSource = MeasurementService.getSource( const csTools3DVer1MeasurementSource = measurementService.getSource(
CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION CORNERSTONE_3D_TOOLS_SOURCE_VERSION
); );
MeasurementService.subscribe(MEASUREMENTS_CLEARED, ({ measurements }) => { measurementService.subscribe(MEASUREMENTS_CLEARED, ({ measurements }) => {
if (!Object.keys(measurements).length) { if (!Object.keys(measurements).length) {
return; return;
} }
@ -200,7 +301,7 @@ const connectMeasurementServiceToTools = (
} }
}); });
MeasurementService.subscribe( measurementService.subscribe(
MEASUREMENT_UPDATED, MEASUREMENT_UPDATED,
({ source, measurement, notYetUpdatedAtSource }) => { ({ source, measurement, notYetUpdatedAtSource }) => {
if (source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) { if (source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) {
@ -234,7 +335,7 @@ const connectMeasurementServiceToTools = (
} }
); );
MeasurementService.subscribe( measurementService.subscribe(
RAW_MEASUREMENT_ADDED, RAW_MEASUREMENT_ADDED,
({ source, measurement, data, dataSource }) => { ({ source, measurement, data, dataSource }) => {
if (source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) { if (source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) {
@ -265,7 +366,7 @@ const connectMeasurementServiceToTools = (
imageId = dataSource.getImageIdsForInstance({ instance }); imageId = dataSource.getImageIdsForInstance({ instance });
} }
const annotationManager = annotation.state.getDefaultAnnotationManager(); const annotationManager = annotation.state.getAnnotationManager();
annotationManager.addAnnotation({ annotationManager.addAnnotation({
annotationUID: measurement.uid, annotationUID: measurement.uid,
highlighted: false, highlighted: false,
@ -287,14 +388,14 @@ const connectMeasurementServiceToTools = (
} }
); );
MeasurementService.subscribe( measurementService.subscribe(
MEASUREMENT_REMOVED, MEASUREMENT_REMOVED,
({ source, measurement: removedMeasurementId }) => { ({ source, measurement: removedMeasurementId }) => {
if (source?.name && source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) { if (source?.name && source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) {
return; return;
} }
removeAnnotation(removedMeasurementId); removeAnnotation(removedMeasurementId);
const renderingEngine = CornerstoneViewportService.getRenderingEngine(); const renderingEngine = cornerstoneViewportService.getRenderingEngine();
// Note: We could do a better job by triggering the render on the // Note: We could do a better job by triggering the render on the
// viewport itself, but the removeAnnotation does not include that info... // viewport itself, but the removeAnnotation does not include that info...
renderingEngine.render(); renderingEngine.render();

View File

@ -34,7 +34,7 @@ function initWebWorkers(appConfig) {
} }
export default function initWADOImageLoader( export default function initWADOImageLoader(
UserAuthenticationService, userAuthenticationService,
appConfig appConfig
) { ) {
cornerstoneWADOImageLoader.external.cornerstone = cornerstone; cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
@ -55,7 +55,7 @@ export default function initWADOImageLoader(
convertFloatPixelDataToInt: false, convertFloatPixelDataToInt: false,
}, },
beforeSend: function(xhr) { beforeSend: function(xhr) {
const headers = UserAuthenticationService.getAuthorizationHeader(); const headers = userAuthenticationService.getAuthorizationHeader();
// Request: // Request:
// JPEG-LS Lossless (1.2.840.10008.1.2.4.80) if available, otherwise accept // JPEG-LS Lossless (1.2.840.10008.1.2.4.80) if available, otherwise accept

View File

@ -1,5 +1,5 @@
import { ServicesManager } from '@ohif/core';
import { cache as cs3DCache, Enums, volumeLoader } from '@cornerstonejs/core'; import { cache as cs3DCache, Enums, volumeLoader } from '@cornerstonejs/core';
import { utils } from '@ohif/core';
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType'; import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
import { import {
@ -7,10 +7,19 @@ import {
VolumeViewportData, VolumeViewportData,
} from '../../types/CornerstoneCacheService'; } from '../../types/CornerstoneCacheService';
const VOLUME_IMAGE_LOADER_SCHEME = 'streaming-wadors';
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume'; const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
class CornerstoneCacheService { class CornerstoneCacheService {
static REGISTRATION = (serviceManager: ServicesManager) => {
return {
name: 'cornerstoneCacheService',
altName: 'CornerstoneCacheService',
create: ({ configuration = {} }) => {
return new CornerstoneCacheService(serviceManager);
},
};
};
stackImageIds: Map<string, string[]> = new Map(); stackImageIds: Map<string, string[]> = new Map();
volumeImageIds: Map<string, string[]> = new Map(); volumeImageIds: Map<string, string[]> = new Map();
@ -70,7 +79,7 @@ class CornerstoneCacheService {
viewportData: VolumeViewportData, viewportData: VolumeViewportData,
invalidatedDisplaySetInstanceUID: string, invalidatedDisplaySetInstanceUID: string,
dataSource, dataSource,
DisplaySetService displaySetService
) { ) {
if (viewportData.viewportType === Enums.ViewportType.STACK) { if (viewportData.viewportType === Enums.ViewportType.STACK) {
throw new Error('Invalidation of StackViewport is not supported yet'); throw new Error('Invalidation of StackViewport is not supported yet');
@ -86,7 +95,7 @@ class CornerstoneCacheService {
} }
const displaySets = viewportData.data.map(({ displaySetInstanceUID }) => const displaySets = viewportData.data.map(({ displaySetInstanceUID }) =>
DisplaySetService.getDisplaySetByUID(displaySetInstanceUID) displaySetService.getDisplaySetByUID(displaySetInstanceUID)
); );
const newViewportData = await this._getVolumeViewportData( const newViewportData = await this._getVolumeViewportData(
@ -148,8 +157,8 @@ class CornerstoneCacheService {
// getSOPClassHandler method // getSOPClassHandler method
if (displaySet.load && displaySet.load instanceof Function) { if (displaySet.load && displaySet.load instanceof Function) {
const { UserAuthenticationService } = this.servicesManager.services; const { userAuthenticationService } = this.servicesManager.services;
const headers = UserAuthenticationService.getAuthorizationHeader(); const headers = userAuthenticationService.getAuthorizationHeader();
await displaySet.load({ headers }); await displaySet.load({ headers });
volumeData.push({ volumeData.push({
@ -204,7 +213,7 @@ class CornerstoneCacheService {
} }
private _shouldRenderSegmentation(displaySets) { private _shouldRenderSegmentation(displaySets) {
const { SegmentationService } = this.servicesManager.services; const { segmentationService } = this.servicesManager.services;
const viewportDisplaySetInstanceUIDs = displaySets.map( const viewportDisplaySetInstanceUIDs = displaySets.map(
({ displaySetInstanceUID }) => displaySetInstanceUID ({ displaySetInstanceUID }) => displaySetInstanceUID
@ -212,12 +221,12 @@ class CornerstoneCacheService {
// check inside segmentations if any of them are referencing the displaySets // check inside segmentations if any of them are referencing the displaySets
// that are about to be displayed // that are about to be displayed
const segmentations = SegmentationService.getSegmentations(); const segmentations = segmentationService.getSegmentations();
for (const segmentation of segmentations) { for (const segmentation of segmentations) {
const segDisplaySetInstanceUID = segmentation.displaySetInstanceUID; const segDisplaySetInstanceUID = segmentation.displaySetInstanceUID;
const shouldDisplaySeg = SegmentationService.shouldRenderSegmentation( const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
viewportDisplaySetInstanceUIDs, viewportDisplaySetInstanceUIDs,
segDisplaySetInstanceUID segDisplaySetInstanceUID
); );
@ -238,14 +247,7 @@ class CornerstoneCacheService {
dataSource dataSource
); );
if (stackImageIds[0].startsWith('dicomfile')) { return stackImageIds;
return stackImageIds;
}
return stackImageIds.map(imageId => {
const imageURI = utils.imageIdToURI(imageId);
return `${VOLUME_IMAGE_LOADER_SCHEME}:${imageURI}`;
});
} }
} }

View File

@ -1,10 +1,3 @@
import CornerstoneCacheService from './CornerstoneCacheService'; import CornerstoneCacheService from './CornerstoneCacheService';
export default function ExtendedCornerstoneCacheService(serviceManager) { export default CornerstoneCacheService;
return {
name: 'CornerstoneCacheService',
create: ({ configuration = {} }) => {
return new CornerstoneCacheService(serviceManager);
},
};
}

View File

@ -738,7 +738,7 @@ class SegmentationService {
highlightHideOthers = false, highlightHideOthers = false,
highlightFunctionType: 'ease-in-out' // todo: make animation functions configurable from outside highlightFunctionType: 'ease-in-out' // todo: make animation functions configurable from outside
): void { ): void {
const { ToolGroupService } = this.servicesManager.services; const { toolGroupService } = this.servicesManager.services;
const center = this._getSegmentCenter(segmentationId, segmentIndex); const center = this._getSegmentCenter(segmentationId, segmentIndex);
const { world } = center; const { world } = center;
@ -751,10 +751,10 @@ class SegmentationService {
if (Array.isArray(toolGroupId)) { if (Array.isArray(toolGroupId)) {
toolGroupId.forEach(toolGroup => { toolGroupId.forEach(toolGroup => {
toolGroups.push(ToolGroupService.getToolGroup(toolGroup)); toolGroups.push(toolGroupService.getToolGroup(toolGroup));
}); });
} else { } else {
toolGroups.push(ToolGroupService.getToolGroup(toolGroupId)); toolGroups.push(toolGroupService.getToolGroup(toolGroupId));
} }
toolGroups.forEach(toolGroup => { toolGroups.forEach(toolGroup => {
@ -1240,9 +1240,9 @@ class SegmentationService {
} }
// if (brushSize !== undefined) { // if (brushSize !== undefined) {
// const { ToolGroupService } = this.servicesManager.services; // const { toolGroupService } = this.servicesManager.services;
// const toolGroupIds = ToolGroupService.getToolGroupIds(); // const toolGroupIds = toolGroupService.getToolGroupIds();
// toolGroupIds.forEach(toolGroupId => { // toolGroupIds.forEach(toolGroupId => {
// cstUtils.segmentation.setBrushSizeForToolGroup(toolGroupId, brushSize); // cstUtils.segmentation.setBrushSizeForToolGroup(toolGroupId, brushSize);
@ -1250,9 +1250,9 @@ class SegmentationService {
// } // }
// if (brushThresholdGate !== undefined) { // if (brushThresholdGate !== undefined) {
// const { ToolGroupService } = this.servicesManager.services; // const { toolGroupService } = this.servicesManager.services;
// const toolGroupIds = ToolGroupService.getFirstToolGroupIds(); // const toolGroupIds = toolGroupService.getFirstToolGroupIds();
// toolGroupIds.forEach(toolGroupId => { // toolGroupIds.forEach(toolGroupId => {
// cstUtils.segmentation.setBrushThresholdForToolGroup( // cstUtils.segmentation.setBrushThresholdForToolGroup(
@ -1324,11 +1324,11 @@ class SegmentationService {
return false; return false;
} }
const { DisplaySetService } = this.servicesManager.services; const { displaySetService } = this.servicesManager.services;
let shouldDisplaySeg = false; let shouldDisplaySeg = false;
const segDisplaySet = DisplaySetService.getDisplaySetByUID( const segDisplaySet = displaySetService.getDisplaySetByUID(
segDisplaySetInstanceUID segDisplaySetInstanceUID
); );
@ -1337,7 +1337,7 @@ class SegmentationService {
viewportDisplaySetInstanceUIDs.forEach(displaySetInstanceUID => { viewportDisplaySetInstanceUIDs.forEach(displaySetInstanceUID => {
// check if the displaySet is sharing the same frameOfReferenceUID // check if the displaySet is sharing the same frameOfReferenceUID
// with the new segmentation // with the new segmentation
const displaySet = DisplaySetService.getDisplaySetByUID( const displaySet = displaySetService.getDisplaySetByUID(
displaySetInstanceUID displaySetInstanceUID
); );
@ -1710,7 +1710,7 @@ class SegmentationService {
} }
private _setLabelmapConfigValue = (property, value) => { private _setLabelmapConfigValue = (property, value) => {
const { CornerstoneViewportService } = this.servicesManager.services; const { cornerstoneViewportService } = this.servicesManager.services;
const config = cstSegmentation.config.getGlobalConfig(); const config = cstSegmentation.config.getGlobalConfig();
@ -1719,8 +1719,8 @@ class SegmentationService {
// Todo: add non global (representation specific config as well) // Todo: add non global (representation specific config as well)
cstSegmentation.config.setGlobalConfig(config); cstSegmentation.config.setGlobalConfig(config);
const renderingEngine = CornerstoneViewportService.getRenderingEngine(); const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const viewportIds = CornerstoneViewportService.getViewportIds(); const viewportIds = cornerstoneViewportService.getViewportIds();
renderingEngine.renderViewports(viewportIds); renderingEngine.renderViewports(viewportIds);
}; };
@ -1953,8 +1953,8 @@ class SegmentationService {
} }
private _getFirstToolGroupId = () => { private _getFirstToolGroupId = () => {
const { ToolGroupService } = this.servicesManager.services; const { toolGroupService } = this.servicesManager.services;
const toolGroupIds = ToolGroupService.getToolGroupIds(); const toolGroupIds = toolGroupService.getToolGroupIds();
return toolGroupIds[0]; return toolGroupIds[0];
}; };

View File

@ -2,7 +2,8 @@ import SegmentationService from './SegmentationService';
export default function ExtendedSegmentationService(servicesManager) { export default function ExtendedSegmentationService(servicesManager) {
return { return {
name: 'SegmentationService', name: 'segmentationService',
altName: 'SegmentationService',
create: ({ configuration = {} }) => { create: ({ configuration = {} }) => {
return new SegmentationService({ servicesManager }); return new SegmentationService({ servicesManager });
}, },

View File

@ -2,7 +2,8 @@ import SyncGroupService from './SyncGroupService';
export default function ExtendedSyncGroupService(serviceManager) { export default function ExtendedSyncGroupService(serviceManager) {
return { return {
name: 'SyncGroupService', altName: 'SyncGroupService',
name: 'syncGroupService',
create: ({ configuration = {} }) => { create: ({ configuration = {} }) => {
return new SyncGroupService(serviceManager); return new SyncGroupService(serviceManager);
}, },

View File

@ -20,6 +20,16 @@ type Tools = {
}; };
export default class ToolGroupService { export default class ToolGroupService {
public static REGISTRATION = serviceManager => {
return {
name: 'toolGroupService',
altName: 'ToolGroupService',
create: ({ configuration = {} }) => {
return new ToolGroupService(serviceManager);
},
};
};
serviceManager: any; serviceManager: any;
private toolGroupIds: Set<string> = new Set(); private toolGroupIds: Set<string> = new Set();
/** /**
@ -29,8 +39,8 @@ export default class ToolGroupService {
EVENTS: { [key: string]: string }; EVENTS: { [key: string]: string };
constructor(serviceManager) { constructor(serviceManager) {
const { CornerstoneViewportService } = serviceManager.services; const { cornerstoneViewportService } = serviceManager.services;
this.CornerstoneViewportService = CornerstoneViewportService; this.cornerstoneViewportService = cornerstoneViewportService;
this.listeners = {}; this.listeners = {};
this.EVENTS = EVENTS; this.EVENTS = EVENTS;
Object.assign(this, pubSubServiceInterface); Object.assign(this, pubSubServiceInterface);
@ -51,7 +61,7 @@ export default class ToolGroupService {
} }
public getToolGroupForViewport(viewportId: string): Types.IToolGroup | void { public getToolGroupForViewport(viewportId: string): Types.IToolGroup | void {
const renderingEngine = this.CornerstoneViewportService.getRenderingEngine(); const renderingEngine = this.cornerstoneViewportService.getRenderingEngine();
return ToolGroupManager.getToolGroupForViewport( return ToolGroupManager.getToolGroupForViewport(
viewportId, viewportId,
renderingEngine.id renderingEngine.id

View File

@ -1,10 +1,3 @@
import ToolGroupService from './ToolGroupService'; import ToolGroupService from './ToolGroupService';
export default function ExtendedToolGroupService(serviceManager) { export default ToolGroupService;
return {
name: 'ToolGroupService',
create: ({ configuration = {} }) => {
return new ToolGroupService(serviceManager);
},
};
}

View File

@ -136,7 +136,11 @@ class CornerstoneViewportService implements IViewportService {
public destroy() { public destroy() {
this._removeResizeObserver(); this._removeResizeObserver();
this.viewportGridResizeObserver = null; this.viewportGridResizeObserver = null;
this.renderingEngine?.destroy?.(); try {
this.renderingEngine?.destroy?.();
} catch (e) {
console.warn('Rendering engine not destroyed', e);
}
this.viewportsDisplaySets.clear(); this.viewportsDisplaySets.clear();
this.renderingEngine = null; this.renderingEngine = null;
cache.purgeCache(); cache.purgeCache();
@ -408,7 +412,7 @@ class CornerstoneViewportService implements IViewportService {
// (This call may or may not create sub-requests for series metadata) // (This call may or may not create sub-requests for series metadata)
const volumeInputArray = []; const volumeInputArray = [];
const displaySetOptionsArray = viewportInfo.getDisplaySetOptions(); const displaySetOptionsArray = viewportInfo.getDisplaySetOptions();
const { HangingProtocolService } = this.servicesManager.services; const { hangingProtocolService } = this.servicesManager.services;
const volumeToLoad = []; const volumeToLoad = [];
const displaySetInstanceUIDs = []; const displaySetInstanceUIDs = [];
@ -445,11 +449,11 @@ class CornerstoneViewportService implements IViewportService {
this.viewportsDisplaySets.set(viewport.id, displaySetInstanceUIDs); this.viewportsDisplaySets.set(viewport.id, displaySetInstanceUIDs);
if ( if (
HangingProtocolService.hasCustomImageLoadStrategy() && hangingProtocolService.hasCustomImageLoadStrategy() &&
!HangingProtocolService.customImageLoadPerformed !hangingProtocolService.customImageLoadPerformed
) { ) {
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy // delegate the volume loading to the hanging protocol service if it has a custom image load strategy
return HangingProtocolService.runImageLoadStrategy({ return hangingProtocolService.runImageLoadStrategy({
viewportId: viewport.id, viewportId: viewport.id,
volumeInputArray, volumeInputArray,
}); });
@ -465,9 +469,9 @@ class CornerstoneViewportService implements IViewportService {
public async setVolumesForViewport(viewport, volumeInputArray) { public async setVolumesForViewport(viewport, volumeInputArray) {
const { const {
DisplaySetService, displaySetService,
SegmentationService, segmentationService,
ToolGroupService, toolGroupService,
} = this.servicesManager.services; } = this.servicesManager.services;
await viewport.setVolumes(volumeInputArray); await viewport.setVolumes(volumeInputArray);
@ -476,7 +480,7 @@ class CornerstoneViewportService implements IViewportService {
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id); const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
const segDisplaySet = displaySetInstanceUIDs const segDisplaySet = displaySetInstanceUIDs
.map(DisplaySetService.getDisplaySetByUID) .map(displaySetService.getDisplaySetByUID)
.find(displaySet => displaySet && displaySet.Modality === 'SEG'); .find(displaySet => displaySet && displaySet.Modality === 'SEG');
if (segDisplaySet) { if (segDisplaySet) {
@ -484,18 +488,18 @@ class CornerstoneViewportService implements IViewportService {
const referencedVolume = cache.getVolume(referencedVolumeId); const referencedVolume = cache.getVolume(referencedVolumeId);
const segmentationId = segDisplaySet.displaySetInstanceUID; const segmentationId = segDisplaySet.displaySetInstanceUID;
const toolGroup = ToolGroupService.getToolGroupForViewport(viewport.id); const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
if (referencedVolume) { if (referencedVolume) {
SegmentationService.addSegmentationRepresentationToToolGroup( segmentationService.addSegmentationRepresentationToToolGroup(
toolGroup.id, toolGroup.id,
segmentationId segmentationId
); );
} }
} else { } else {
const toolGroup = ToolGroupService.getToolGroupForViewport(viewport.id); const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
const toolGroupSegmentationRepresentations = const toolGroupSegmentationRepresentations =
SegmentationService.getSegmentationRepresentationsForToolGroup( segmentationService.getSegmentationRepresentationsForToolGroup(
toolGroup.id toolGroup.id
) || []; ) || [];
@ -504,7 +508,7 @@ class CornerstoneViewportService implements IViewportService {
// and we can look into hydrated segmentations to check if any of them are // and we can look into hydrated segmentations to check if any of them are
// associated with the primary displaySet // associated with the primary displaySet
// get segmentations only returns the hydrated segmentations // get segmentations only returns the hydrated segmentations
const segmentations = SegmentationService.getSegmentations(); const segmentations = segmentationService.getSegmentations();
for (const segmentation of segmentations) { for (const segmentation of segmentations) {
// if there is already a segmentation representation for this segmentation // if there is already a segmentation representation for this segmentation
@ -539,11 +543,11 @@ class CornerstoneViewportService implements IViewportService {
} }
if (shouldDisplaySeg) { if (shouldDisplaySeg) {
const toolGroup = ToolGroupService.getToolGroupForViewport( const toolGroup = toolGroupService.getToolGroupForViewport(
viewport.id viewport.id
); );
SegmentationService.addSegmentationRepresentationToToolGroup( segmentationService.addSegmentationRepresentationToToolGroup(
toolGroup.id, toolGroup.id,
segmentation.id segmentation.id
); );
@ -553,7 +557,7 @@ class CornerstoneViewportService implements IViewportService {
const viewportInfo = this.getViewportInfo(viewport.id); const viewportInfo = this.getViewportInfo(viewport.id);
const toolGroup = ToolGroupService.getToolGroupForViewport(viewport.id); const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id); csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
const initialImageOptions = viewportInfo.getInitialImageOptions(); const initialImageOptions = viewportInfo.getInitialImageOptions();
@ -729,8 +733,8 @@ class CornerstoneViewportService implements IViewportService {
} }
_getFrameOfReferenceUID(displaySetInstanceUID) { _getFrameOfReferenceUID(displaySetInstanceUID) {
const { DisplaySetService } = this.servicesManager.services; const { displaySetService } = this.servicesManager.services;
const displaySet = DisplaySetService.getDisplaySetByUID( const displaySet = displaySetService.getDisplaySetByUID(
displaySetInstanceUID displaySetInstanceUID
); );
@ -756,7 +760,8 @@ class CornerstoneViewportService implements IViewportService {
export default function ExtendedCornerstoneViewportService(serviceManager) { export default function ExtendedCornerstoneViewportService(serviceManager) {
return { return {
name: 'CornerstoneViewportService', name: 'cornerstoneViewportService',
altName: 'CornerstoneViewportService',
create: ({ configuration = {} }) => { create: ({ configuration = {} }) => {
return new CornerstoneViewportService(serviceManager); return new CornerstoneViewportService(serviceManager);
}, },

View File

@ -12,7 +12,7 @@ import {
*/ */
export interface IViewportService { export interface IViewportService {
servicesManager: unknown; servicesManager: unknown;
HangingProtocolService: unknown; hangingProtocolService: unknown;
renderingEngine: unknown; renderingEngine: unknown;
viewportGridResizeObserver: unknown; viewportGridResizeObserver: unknown;
viewportsInfo: unknown; viewportsInfo: unknown;

View File

@ -7,7 +7,7 @@ import { SyncGroup } from '../SyncGroupService/SyncGroupService';
import { import {
StackViewportData, StackViewportData,
VolumeViewportData, VolumeViewportData,
} from './CornerstoneCacheService'; } from '../../types/CornerstoneCacheService';
export type InitialImageOptions = { export type InitialImageOptions = {
index?: number; index?: number;
@ -23,6 +23,11 @@ export type ViewportOptions = {
syncGroups?: SyncGroup[]; syncGroups?: SyncGroup[];
initialImageOptions?: InitialImageOptions; initialImageOptions?: InitialImageOptions;
customViewportProps?: Record<string, unknown>; customViewportProps?: Record<string, unknown>;
/*
* Allows drag and drop of display sets not matching viewport options, but
* doesn't show them initially. Displays initially blank if no required match
*/
allowUnmatchedView?: boolean;
}; };
export type PublicViewportOptions = { export type PublicViewportOptions = {
@ -34,6 +39,7 @@ export type PublicViewportOptions = {
syncGroups?: SyncGroup[]; syncGroups?: SyncGroup[];
initialImageOptions?: InitialImageOptions; initialImageOptions?: InitialImageOptions;
customViewportProps?: Record<string, unknown>; customViewportProps?: Record<string, unknown>;
allowUnmatchedView?: boolean;
}; };
export type PublicDisplaySetOptions = { export type PublicDisplaySetOptions = {

View File

@ -0,0 +1,134 @@
import { metaData } from '@cornerstonejs/core';
import { LengthTool } from '@cornerstonejs/tools';
import { calibrateImageSpacing } from '@cornerstonejs/tools/dist/esm/utilities';
import callInputDialog from '../utils/callInputDialog';
import getActiveViewportEnabledElement from '../utils/getActiveViewportEnabledElement';
/**
* Calibration Line tool works almost the same as the
*/
class CalibrationLineTool extends LengthTool {
static toolName = 'CalibrationLine';
_renderingViewport: any;
_lengthToolRenderAnnotation = this.renderAnnotation;
renderAnnotation = (enabledElement, svgDrawingHelper) => {
const { viewport } = enabledElement;
this._renderingViewport = viewport;
return this._lengthToolRenderAnnotation(enabledElement, svgDrawingHelper);
};
_getTextLines(data, targetId) {
const [canvasPoint1, canvasPoint2] = data.handles.points.map(p =>
this._renderingViewport.worldToCanvas(p)
);
// for display, round to 2 decimal points
const lengthPx =
Math.round(calculateLength2(canvasPoint1, canvasPoint2) * 100) / 100;
const textLines = [`${lengthPx}px`];
return textLines;
}
}
function calculateLength2(point1, point2) {
const dx = point1[0] - point2[0];
const dy = point1[1] - point2[1];
return Math.sqrt(dx * dx + dy * dy);
}
function calculateLength3(pos1, pos2) {
const dx = pos1[0] - pos2[0];
const dy = pos1[1] - pos2[1];
const dz = pos1[2] - pos2[2];
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
export default CalibrationLineTool;
export function onCompletedCalibrationLine(servicesManager, csToolsEvent) {
const { uiDialogService, viewportGridService } = servicesManager.services;
// calculate length (mm) with the current Pixel Spacing
const annotationAddedEventDetail = csToolsEvent.detail;
const {
annotation: { metadata, data: annotationData },
} = annotationAddedEventDetail;
const { referencedImageId: imageId } = metadata;
const enabledElement = getActiveViewportEnabledElement(viewportGridService);
const { viewport } = enabledElement;
const length =
Math.round(
calculateLength3(
annotationData.handles.points[0],
annotationData.handles.points[1]
) * 100
) / 100;
// calculate the currently applied pixel spacing on the viewport
const calibratedPixelSpacing = metaData.get(
'calibratedPixelSpacing',
imageId
);
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
const currentRowPixelSpacing =
calibratedPixelSpacing?.[0] || imagePlaneModule?.rowPixelSpacing || 1;
const currentColumnPixelSpacing =
calibratedPixelSpacing?.[1] || imagePlaneModule?.columnPixelSpacing || 1;
const adjustCalibration = newLength => {
const spacingScale = newLength / length;
const rowSpacing = spacingScale * currentRowPixelSpacing;
const colSpacing = spacingScale * currentColumnPixelSpacing;
// trigger resize of the viewport to adjust the world/pixel mapping
calibrateImageSpacing(
imageId,
viewport.getRenderingEngine(),
rowSpacing,
colSpacing
);
};
return new Promise((resolve, reject) => {
if (!uiDialogService) {
reject('UIDialogService is not initiated');
return;
}
callInputDialog(
uiDialogService,
{
text: '',
label: `${length}`,
},
(value, id) => {
if (id === 'save') {
adjustCalibration(Number.parseFloat(value));
resolve(true);
} else {
reject('cancel');
}
},
false,
{
dialogTitle: 'Calibration',
inputLabel: 'Actual Physical distance (mm)',
// the input value must be a number
validateFunc: val => {
try {
const v = Number.parseFloat(val);
return !isNaN(v) && v !== 0.0;
} catch {
return false;
}
},
}
);
});
}

View File

@ -21,7 +21,7 @@ const VIEWPORT_ID = 'cornerstone-viewport-download-form';
const CornerstoneViewportDownloadForm = ({ const CornerstoneViewportDownloadForm = ({
onClose, onClose,
activeViewportIndex, activeViewportIndex,
CornerstoneViewportService, cornerstoneViewportService,
}) => { }) => {
const enabledElement = OHIFgetEnabledElement(activeViewportIndex); const enabledElement = OHIFgetEnabledElement(activeViewportIndex);
const activeViewportElement = enabledElement?.element; const activeViewportElement = enabledElement?.element;
@ -154,7 +154,7 @@ const CornerstoneViewportDownloadForm = ({
const { viewport } = activeViewportEnabledElement; const { viewport } = activeViewportEnabledElement;
const renderingEngine = CornerstoneViewportService.getRenderingEngine(); const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const downloadViewport = renderingEngine.getViewport(VIEWPORT_ID); const downloadViewport = renderingEngine.getViewport(VIEWPORT_ID);
if (downloadViewport instanceof StackViewport) { if (downloadViewport instanceof StackViewport) {

View File

@ -9,44 +9,56 @@ import { Input, Dialog } from '@ohif/ui';
* @param {*} event * @param {*} event
* @param {*} callback * @param {*} callback
* @param {*} isArrowAnnotateInputDialog * @param {*} isArrowAnnotateInputDialog
* @param {*} dialogConfig
* @param {string?} dialogConfig.dialogTitle - title of the input dialog
* @param {string?} dialogConfig.inputLabel - show label above the input
*/ */
function callInputDialog( function callInputDialog(
UIDialogService, uiDialogService,
data, data,
callback, callback,
isArrowAnnotateInputDialog = true isArrowAnnotateInputDialog = true,
dialogConfig: any = {}
) { ) {
const dialogId = 'enter-annotation'; const dialogId = 'dialog-enter-annotation';
const label = data const label = data
? isArrowAnnotateInputDialog ? isArrowAnnotateInputDialog
? data.text ? data.text
: data.label : data.label
: ''; : '';
const {
dialogTitle = 'Enter your annotation',
inputLabel = '',
validateFunc = value => true,
} = dialogConfig;
const onSubmitHandler = ({ action, value }) => { const onSubmitHandler = ({ action, value }) => {
switch (action.id) { switch (action.id) {
case 'save': case 'save':
if (typeof validateFunc === 'function' && !validateFunc(value.label))
return;
callback(value.label, action.id); callback(value.label, action.id);
break; break;
case 'cancel': case 'cancel':
callback('', action.id); callback('', action.id);
break; break;
} }
UIDialogService.dismiss({ id: dialogId }); uiDialogService.dismiss({ id: dialogId });
}; };
if (UIDialogService) { if (uiDialogService) {
UIDialogService.create({ uiDialogService.create({
id: dialogId, id: dialogId,
centralize: true, centralize: true,
isDraggable: false, isDraggable: false,
showOverlay: true, showOverlay: true,
content: Dialog, content: Dialog,
contentProps: { contentProps: {
title: 'Enter your annotation', title: dialogTitle,
value: { label }, value: { label },
noCloseButton: true, noCloseButton: true,
onClose: () => UIDialogService.dismiss({ id: dialogId }), onClose: () => uiDialogService.dismiss({ id: dialogId }),
actions: [ actions: [
{ id: 'cancel', text: 'Cancel', type: 'primary' }, { id: 'cancel', text: 'Cancel', type: 'primary' },
{ id: 'save', text: 'Save', type: 'secondary' }, { id: 'save', text: 'Save', type: 'secondary' },
@ -59,7 +71,10 @@ function callInputDialog(
autoFocus autoFocus
className="mt-2 bg-black border-primary-main" className="mt-2 bg-black border-primary-main"
type="text" type="text"
id="annotation"
containerClassName="mr-2" containerClassName="mr-2"
label={inputLabel}
labelClassName="text-primary-light"
value={value.label} value={value.label}
onChange={event => { onChange={event => {
event.persist(); event.persist();

View File

@ -0,0 +1,13 @@
import { getEnabledElement } from '@cornerstonejs/core';
import { IEnabledElement } from '@cornerstonejs/core/dist/esm/types';
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
export default function getActiveViewportEnabledElement(
viewportGridService
): IEnabledElement {
const { activeViewportIndex } = viewportGridService.getState();
const { element } = OHIFgetEnabledElement(activeViewportIndex) || {};
const enabledElement = getEnabledElement(element);
return enabledElement;
}

View File

@ -17,7 +17,6 @@ export default function getCornerstoneOrientation(
case CORONAL: case CORONAL:
return Enums.OrientationAxis.CORONAL; return Enums.OrientationAxis.CORONAL;
default: default:
log.wanr('Choosing acquisition plane orientation');
return Enums.OrientationAxis.ACQUISITION; return Enums.OrientationAxis.ACQUISITION;
} }
} }

View File

@ -0,0 +1,44 @@
/**
* Returns a re-ordered array consisting of, in order:
* 1. First few objects
* 2. Center objects
* 3. Last few objects
* 4. nth Objects (n=7), set 2
* 5. nth Objects set 5,
* 6. Remaining objects
* What this does is return the first/center/start objects, as those
* are often used first, then a selection of objects scattered over the
* instances in order to allow making requests over a set of image instances.
*
* @param {[]} imageIds
* @returns [] reordered to be an nth selection
*/
export default function getNthFrames(imageIds) {
const frames = [[], [], [], [], []];
const centerStart = imageIds.length / 2 - 3;
const centerEnd = centerStart + 6;
for (let i = 0; i < imageIds.length; i++) {
if (
i < 2 ||
i > imageIds.length - 4 ||
(i > centerStart && i < centerEnd)
) {
frames[0].push(imageIds[i]);
} else if (i % 7 === 2) {
frames[1].push(imageIds[i]);
} else if (i % 7 === 5) {
frames[2].push(imageIds[i]);
} else {
frames[(i % 2) + 3].push(imageIds[i]);
}
}
const ret = [
...frames[0],
...frames[1],
...frames[2],
...frames[3],
...frames[4],
];
return ret;
}

View File

@ -0,0 +1,26 @@
/**
* Interleave the items from all the lists so that the first items are first
* in the returned list, the second items are next etc.
* Does this in a O(n) fashion, and return lists[0] if there is only one list.
*
* @param {[]} lists
* @returns [] reordered to be breadth first traversal of lists
*/
export default function interleave(lists) {
if (!lists || !lists.length) return [];
if (lists.length === 1) return lists[0];
console.time('interleave');
const useLists = [...lists];
const ret = [];
for (let i = 0; useLists.length > 0; i++) {
for (const list of useLists) {
if (i >= list.length) {
useLists.splice(useLists.indexOf(list), 1);
continue;
}
ret.push(list[i]);
}
}
console.timeEnd('interleave');
return ret;
}

View File

@ -0,0 +1,210 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
const Angle = {
toAnnotation: measurement => { },
/**
* Maps cornerstone annotation event data to measurement service format.
*
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
*/
toMeasurement: (
csToolsEventDetail,
displaySetService,
CornerstoneViewportService,
getValueTypeFromToolType
) => {
const { annotation, viewportId } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
if (!metadata || !data) {
console.warn('Length tool: Missing metadata or data');
return null;
}
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
const validToolType = SUPPORTED_TOOLS.includes(toolName);
if (!validToolType) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(
referencedImageId,
CornerstoneViewportService,
viewportId
);
let displaySet;
if (SOPInstanceUID) {
displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const mappedAnnotations = getMappedAnnotations(
annotation,
displaySetService
);
const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
return {
uid: annotationUID,
SOPInstanceUID,
FrameOfReferenceUID,
points,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
frameNumber: mappedAnnotations?.[0]?.frameNumber || 1,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport,
};
},
};
function getMappedAnnotations(annotation, DisplaySetService) {
const { metadata, data } = annotation;
const { cachedStats } = data;
const { referencedImageId } = metadata;
const targets = Object.keys(cachedStats);
if (!targets.length) {
return;
}
const annotations = [];
Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId];
if (!referencedImageId) {
throw new Error(
'Non-acquisition plane measurement mapping not supported'
);
}
const {
SOPInstanceUID,
SeriesInstanceUID,
frameNumber,
} = getSOPInstanceAttributes(referencedImageId);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID,
frameNumber
);
const { SeriesNumber } = displaySet;
const { angle } = targetStats;
const unit = '\u00B0';
annotations.push({
SeriesInstanceUID,
SOPInstanceUID,
SeriesNumber,
frameNumber,
unit,
angle,
});
});
return annotations;
}
/*
This function is used to convert the measurement data to a format that is
suitable for the report generation (e.g. for the csv report). The report
returns a list of columns and corresponding values.
*/
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
const columns = [];
const values = [];
// Add Type
columns.push('AnnotationType');
values.push('Cornerstone:Angle');
mappedAnnotations.forEach(annotation => {
const { angle, unit } = annotation;
columns.push(`Angle (${unit})`);
values.push(angle);
});
if (FrameOfReferenceUID) {
columns.push('FrameOfReferenceUID');
values.push(FrameOfReferenceUID);
}
if (points) {
columns.push('points');
// points has the form of [[x1, y1, z1], [x2, y2, z2], ...]
// convert it to string of [[x1 y1 z1];[x2 y2 z2];...]
// so that it can be used in the csv report
values.push(points.map(p => p.join(' ')).join(';'));
}
return {
columns,
values,
};
}
function getDisplayText(mappedAnnotations, displaySet) {
if (!mappedAnnotations || !mappedAnnotations.length) {
return '';
}
const displayText = [];
// Area is the same for all series
const {
angle,
unit,
SeriesNumber,
SOPInstanceUID,
frameNumber,
} = mappedAnnotations[0];
const instance = displaySet.images.find(
image => image.SOPInstanceUID === SOPInstanceUID
);
let InstanceNumber;
if (instance) {
InstanceNumber = instance.InstanceNumber;
}
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
if (angle === undefined) return displayText;
const roundedAngle = utils.roundNumber(angle, 2);
displayText.push(
`${roundedAngle} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})`
);
return displayText;
}
export default Angle;

View File

@ -12,8 +12,8 @@ const Length = {
*/ */
toMeasurement: ( toMeasurement: (
csToolsEventDetail, csToolsEventDetail,
DisplaySetService, displaySetService,
CornerstoneViewportService, cornerstoneViewportService,
getValueTypeFromToolType getValueTypeFromToolType
) => { ) => {
const { annotation, viewportId } = csToolsEventDetail; const { annotation, viewportId } = csToolsEventDetail;
@ -37,26 +37,26 @@ const Length = {
StudyInstanceUID, StudyInstanceUID,
} = getSOPInstanceAttributes( } = getSOPInstanceAttributes(
referencedImageId, referencedImageId,
CornerstoneViewportService, cornerstoneViewportService,
viewportId viewportId
); );
let displaySet; let displaySet;
if (SOPInstanceUID) { if (SOPInstanceUID) {
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID, SOPInstanceUID,
SeriesInstanceUID SeriesInstanceUID
); );
} else { } else {
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID); displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
} }
const { points } = data.handles; const { points } = data.handles;
const mappedAnnotations = getMappedAnnotations( const mappedAnnotations = getMappedAnnotations(
annotation, annotation,
DisplaySetService displaySetService
); );
const displayText = getDisplayText(mappedAnnotations, displaySet); const displayText = getDisplayText(mappedAnnotations, displaySet);
@ -84,7 +84,7 @@ const Length = {
}, },
}; };
function getMappedAnnotations(annotation, DisplaySetService) { function getMappedAnnotations(annotation, displaySetService) {
const { metadata, data } = annotation; const { metadata, data } = annotation;
const { text } = data; const { text } = data;
const { referencedImageId } = metadata; const { referencedImageId } = metadata;
@ -97,7 +97,7 @@ function getMappedAnnotations(annotation, DisplaySetService) {
frameNumber, frameNumber,
} = getSOPInstanceAttributes(referencedImageId); } = getSOPInstanceAttributes(referencedImageId);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID, SOPInstanceUID,
SeriesInstanceUID, SeriesInstanceUID,
frameNumber frameNumber

View File

@ -8,8 +8,8 @@ const Bidirectional = {
toAnnotation: measurement => {}, toAnnotation: measurement => {},
toMeasurement: ( toMeasurement: (
csToolsEventDetail, csToolsEventDetail,
DisplaySetService, displaySetService,
CornerstoneViewportService, cornerstoneViewportService,
getValueTypeFromToolType getValueTypeFromToolType
) => { ) => {
const { annotation, viewportId } = csToolsEventDetail; const { annotation, viewportId } = csToolsEventDetail;
@ -33,26 +33,26 @@ const Bidirectional = {
StudyInstanceUID, StudyInstanceUID,
} = getSOPInstanceAttributes( } = getSOPInstanceAttributes(
referencedImageId, referencedImageId,
CornerstoneViewportService, cornerstoneViewportService,
viewportId viewportId
); );
let displaySet; let displaySet;
if (SOPInstanceUID) { if (SOPInstanceUID) {
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID, SOPInstanceUID,
SeriesInstanceUID SeriesInstanceUID
); );
} else { } else {
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID); displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
} }
const { points } = data.handles; const { points } = data.handles;
const mappedAnnotations = getMappedAnnotations( const mappedAnnotations = getMappedAnnotations(
annotation, annotation,
DisplaySetService displaySetService
); );
const displayText = getDisplayText(mappedAnnotations, displaySet); const displayText = getDisplayText(mappedAnnotations, displaySet);
@ -79,14 +79,14 @@ const Bidirectional = {
}, },
}; };
function getMappedAnnotations(annotation, DisplaySetService) { function getMappedAnnotations(annotation, displaySetService) {
const { metadata, data } = annotation; const { metadata, data } = annotation;
const { cachedStats } = data; const { cachedStats } = data;
const { referencedImageId, referencedSeriesInstanceUID } = metadata; const { referencedImageId, referencedSeriesInstanceUID } = metadata;
const targets = Object.keys(cachedStats); const targets = Object.keys(cachedStats);
if (!targets.length) { if (!targets.length) {
return; return [];
} }
const annotations = []; const annotations = [];
@ -105,7 +105,7 @@ function getMappedAnnotations(annotation, DisplaySetService) {
frameNumber, frameNumber,
} = getSOPInstanceAttributes(referencedImageId); } = getSOPInstanceAttributes(referencedImageId);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID, SOPInstanceUID,
SeriesInstanceUID, SeriesInstanceUID,
frameNumber frameNumber

View File

@ -0,0 +1,210 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
const CobbAngle = {
toAnnotation: measurement => { },
/**
* Maps cornerstone annotation event data to measurement service format.
*
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
*/
toMeasurement: (
csToolsEventDetail,
displaySetService,
CornerstoneViewportService,
getValueTypeFromToolType
) => {
const { annotation, viewportId } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
if (!metadata || !data) {
console.warn('Cobb Angle tool: Missing metadata or data');
return null;
}
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
const validToolType = SUPPORTED_TOOLS.includes(toolName);
if (!validToolType) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(
referencedImageId,
CornerstoneViewportService,
viewportId
);
let displaySet;
if (SOPInstanceUID) {
displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const mappedAnnotations = getMappedAnnotations(
annotation,
displaySetService
);
const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
return {
uid: annotationUID,
SOPInstanceUID,
FrameOfReferenceUID,
points,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
frameNumber: mappedAnnotations?.[0]?.frameNumber || 1,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport,
};
},
};
function getMappedAnnotations(annotation, DisplaySetService) {
const { metadata, data } = annotation;
const { cachedStats } = data;
const { referencedImageId } = metadata;
const targets = Object.keys(cachedStats);
if (!targets.length) {
return;
}
const annotations = [];
Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId];
if (!referencedImageId) {
throw new Error(
'Non-acquisition plane measurement mapping not supported'
);
}
const {
SOPInstanceUID,
SeriesInstanceUID,
frameNumber,
} = getSOPInstanceAttributes(referencedImageId);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID,
frameNumber
);
const { SeriesNumber } = displaySet;
const { angle } = targetStats;
const unit = '\u00B0';
annotations.push({
SeriesInstanceUID,
SOPInstanceUID,
SeriesNumber,
frameNumber,
unit,
angle,
});
});
return annotations;
}
/*
This function is used to convert the measurement data to a format that is
suitable for the report generation (e.g. for the csv report). The report
returns a list of columns and corresponding values.
*/
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
const columns = [];
const values = [];
// Add Type
columns.push('AnnotationType');
values.push('Cornerstone:CobbAngle');
mappedAnnotations.forEach(annotation => {
const { angle, unit } = annotation;
columns.push(`Angle (${unit})`);
values.push(angle);
});
if (FrameOfReferenceUID) {
columns.push('FrameOfReferenceUID');
values.push(FrameOfReferenceUID);
}
if (points) {
columns.push('points');
// points has the form of [[x1, y1, z1], [x2, y2, z2], ...]
// convert it to string of [[x1 y1 z1];[x2 y2 z2];...]
// so that it can be used in the csv report
values.push(points.map(p => p.join(' ')).join(';'));
}
return {
columns,
values,
};
}
function getDisplayText(mappedAnnotations, displaySet) {
if (!mappedAnnotations || !mappedAnnotations.length) {
return '';
}
const displayText = [];
// Area is the same for all series
const {
angle,
unit,
SeriesNumber,
SOPInstanceUID,
frameNumber,
} = mappedAnnotations[0];
const instance = displaySet.images.find(
image => image.SOPInstanceUID === SOPInstanceUID
);
let InstanceNumber;
if (instance) {
InstanceNumber = instance.InstanceNumber;
}
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
if (angle === undefined) return displayText;
const roundedAngle = utils.roundNumber(angle, 2);
displayText.push(
`${roundedAngle} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})`
);
return displayText;
}
export default CobbAngle;

View File

@ -7,8 +7,8 @@ const EllipticalROI = {
toAnnotation: measurement => {}, toAnnotation: measurement => {},
toMeasurement: ( toMeasurement: (
csToolsEventDetail, csToolsEventDetail,
DisplaySetService, displaySetService,
CornerstoneViewportService, cornerstoneViewportService,
getValueTypeFromToolType getValueTypeFromToolType
) => { ) => {
const { annotation, viewportId } = csToolsEventDetail; const { annotation, viewportId } = csToolsEventDetail;
@ -32,26 +32,26 @@ const EllipticalROI = {
StudyInstanceUID, StudyInstanceUID,
} = getSOPInstanceAttributes( } = getSOPInstanceAttributes(
referencedImageId, referencedImageId,
CornerstoneViewportService, cornerstoneViewportService,
viewportId viewportId
); );
let displaySet; let displaySet;
if (SOPInstanceUID) { if (SOPInstanceUID) {
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID, SOPInstanceUID,
SeriesInstanceUID SeriesInstanceUID
); );
} else { } else {
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID); displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
} }
const { points } = data.handles; const { points } = data.handles;
const mappedAnnotations = getMappedAnnotations( const mappedAnnotations = getMappedAnnotations(
annotation, annotation,
DisplaySetService displaySetService
); );
const displayText = getDisplayText(mappedAnnotations, displaySet); const displayText = getDisplayText(mappedAnnotations, displaySet);
@ -78,14 +78,14 @@ const EllipticalROI = {
}, },
}; };
function getMappedAnnotations(annotation, DisplaySetService) { function getMappedAnnotations(annotation, displaySetService) {
const { metadata, data } = annotation; const { metadata, data } = annotation;
const { cachedStats } = data; const { cachedStats } = data;
const { referencedImageId } = metadata; const { referencedImageId } = metadata;
const targets = Object.keys(cachedStats); const targets = Object.keys(cachedStats);
if (!targets.length) { if (!targets.length) {
return; return [];
} }
const annotations = []; const annotations = [];
@ -105,7 +105,7 @@ function getMappedAnnotations(annotation, DisplaySetService) {
frameNumber, frameNumber,
} = getSOPInstanceAttributes(referencedImageId); } = getSOPInstanceAttributes(referencedImageId);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID, SOPInstanceUID,
SeriesInstanceUID, SeriesInstanceUID,
frameNumber frameNumber

View File

@ -1,6 +1,6 @@
import SUPPORTED_TOOLS from './constants/supportedTools'; import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { DisplaySetService, utils } from '@ohif/core'; import { displaySetService, utils } from '@ohif/core';
const Length = { const Length = {
toAnnotation: measurement => {}, toAnnotation: measurement => {},
@ -13,8 +13,8 @@ const Length = {
*/ */
toMeasurement: ( toMeasurement: (
csToolsEventDetail, csToolsEventDetail,
DisplaySetService, displaySetService,
CornerstoneViewportService, cornerstoneViewportService,
getValueTypeFromToolType getValueTypeFromToolType
) => { ) => {
const { annotation, viewportId } = csToolsEventDetail; const { annotation, viewportId } = csToolsEventDetail;
@ -38,26 +38,26 @@ const Length = {
StudyInstanceUID, StudyInstanceUID,
} = getSOPInstanceAttributes( } = getSOPInstanceAttributes(
referencedImageId, referencedImageId,
CornerstoneViewportService, cornerstoneViewportService,
viewportId viewportId
); );
let displaySet; let displaySet;
if (SOPInstanceUID) { if (SOPInstanceUID) {
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID, SOPInstanceUID,
SeriesInstanceUID SeriesInstanceUID
); );
} else { } else {
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID); displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
} }
const { points } = data.handles; const { points } = data.handles;
const mappedAnnotations = getMappedAnnotations( const mappedAnnotations = getMappedAnnotations(
annotation, annotation,
DisplaySetService displaySetService
); );
const displayText = getDisplayText(mappedAnnotations, displaySet); const displayText = getDisplayText(mappedAnnotations, displaySet);
@ -84,14 +84,14 @@ const Length = {
}, },
}; };
function getMappedAnnotations(annotation, DisplaySetService) { function getMappedAnnotations(annotation, displaySetService) {
const { metadata, data } = annotation; const { metadata, data } = annotation;
const { cachedStats } = data; const { cachedStats } = data;
const { referencedImageId } = metadata; const { referencedImageId } = metadata;
const targets = Object.keys(cachedStats); const targets = Object.keys(cachedStats);
if (!targets.length) { if (!targets.length) {
return; return [];
} }
const annotations = []; const annotations = [];
@ -110,7 +110,7 @@ function getMappedAnnotations(annotation, DisplaySetService) {
frameNumber, frameNumber,
} = getSOPInstanceAttributes(referencedImageId); } = getSOPInstanceAttributes(referencedImageId);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID, SOPInstanceUID,
SeriesInstanceUID, SeriesInstanceUID,
frameNumber frameNumber

View File

@ -0,0 +1,148 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
const PlanarFreehandROI = {
toAnnotation: measurement => { },
/**
* Maps cornerstone annotation event data to measurement service format.
*
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
*/
toMeasurement: (
csToolsEventDetail,
DisplaySetService,
CornerstoneViewportService,
getValueTypeFromToolType
) => {
const { annotation, viewportId } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
if (!metadata || !data) {
console.warn('PlanarFreehandROI tool: Missing metadata or data');
return null;
}
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
const validToolType = SUPPORTED_TOOLS.includes(toolName);
if (!validToolType) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(
referencedImageId,
CornerstoneViewportService,
viewportId
);
let displaySet;
if (SOPInstanceUID) {
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const mappedAnnotations = getMappedAnnotations(
annotation,
DisplaySetService
);
const displayText = getDisplayText(mappedAnnotations);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
return {
uid: annotationUID,
SOPInstanceUID,
FrameOfReferenceUID,
points,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
displayText: displayText,
data: { ...data, ...data.cachedStats },
type: getValueTypeFromToolType(toolName),
getReport,
};
},
};
/**
* It maps an imaging library annotation to a list of simplified annotation properties.
*
* @param {Object} annotationData
* @param {Object} DisplaySetService
* @returns
*/
function getMappedAnnotations(annotationData, DisplaySetService) {
const { metadata, data } = annotationData;
const { label } = data;
const { referencedImageId } = metadata;
const annotations = [];
const {
SOPInstanceUID: _SOPInstanceUID,
SeriesInstanceUID: _SeriesInstanceUID,
} = getSOPInstanceAttributes(referencedImageId) || {};
if (!_SOPInstanceUID || !_SeriesInstanceUID) {
return annotations;
}
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
_SOPInstanceUID,
_SeriesInstanceUID
);
const { SeriesNumber, SeriesInstanceUID } = displaySet;
annotations.push({
SeriesInstanceUID,
SeriesNumber,
label,
data,
});
return annotations;
}
/**
* TBD
* This function is used to convert the measurement data to a format that is suitable for the report generation (e.g. for the csv report).
* The report returns a list of columns and corresponding values.
* @param {*} mappedAnnotations
* @param {*} points
* @param {*} FrameOfReferenceUID
* @returns Object representing the report's content for this tool.
*/
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
const columns = [];
const values = [];
return {
columns,
values,
};
}
function getDisplayText(mappedAnnotations) {
return '';
}
export default PlanarFreehandROI;

View File

@ -0,0 +1,228 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import getModalityUnit from './utils/getModalityUnit';
import { utils } from '@ohif/core';
const RectangleROI = {
toAnnotation: measurement => { },
toMeasurement: (
csToolsEventDetail,
DisplaySetService,
CornerstoneViewportService,
getValueTypeFromToolType
) => {
const { annotation, viewportId } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
if (!metadata || !data) {
console.warn('Rectangle ROI tool: Missing metadata or data');
return null;
}
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
const validToolType = SUPPORTED_TOOLS.includes(toolName);
if (!validToolType) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(
referencedImageId,
CornerstoneViewportService,
viewportId
);
let displaySet;
if (SOPInstanceUID) {
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const mappedAnnotations = getMappedAnnotations(
annotation,
DisplaySetService
);
const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
return {
uid: annotationUID,
SOPInstanceUID,
FrameOfReferenceUID,
points,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport,
};
},
};
function getMappedAnnotations(annotation, DisplaySetService) {
const { metadata, data } = annotation;
const { cachedStats } = data;
const { referencedImageId } = metadata;
const targets = Object.keys(cachedStats);
if (!targets.length) {
return [];
}
const annotations = [];
Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId];
if (!referencedImageId) {
// Todo: Non-acquisition plane measurement mapping not supported yet
throw new Error(
'Non-acquisition plane measurement mapping not supported'
);
}
const {
SOPInstanceUID,
SeriesInstanceUID,
frameNumber,
} = getSOPInstanceAttributes(referencedImageId);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID,
frameNumber
);
const { SeriesNumber } = displaySet;
const { mean, stdDev, max, area, Modality } = targetStats;
const unit = getModalityUnit(Modality);
annotations.push({
SeriesInstanceUID,
SOPInstanceUID,
SeriesNumber,
frameNumber,
Modality,
unit,
mean,
stdDev,
max,
area,
});
});
return annotations;
}
/*
This function is used to convert the measurement data to a format that is
suitable for the report generation (e.g. for the csv report). The report
returns a list of columns and corresponding values.
*/
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
const columns = [];
const values = [];
// Add Type
columns.push('AnnotationType');
values.push('Cornerstone:EllipticalROI');
mappedAnnotations.forEach(annotation => {
const { mean, stdDev, max, area, unit } = annotation;
if (!mean || !unit || !max || !area) {
return;
}
columns.push(
`max (${unit})`,
`mean (${unit})`,
`std (${unit})`,
`area (mm2)`
);
values.push(max, mean, stdDev, area);
});
if (FrameOfReferenceUID) {
columns.push('FrameOfReferenceUID');
values.push(FrameOfReferenceUID);
}
if (points) {
columns.push('points');
// points has the form of [[x1, y1, z1], [x2, y2, z2], ...]
// convert it to string of [[x1 y1 z1];[x2 y2 z2];...]
// so that it can be used in the csv report
values.push(points.map(p => p.join(' ')).join(';'));
}
return {
columns,
values,
};
}
function getDisplayText(mappedAnnotations, displaySet) {
if (!mappedAnnotations || !mappedAnnotations.length) {
return '';
}
const displayText = [];
// Area is the same for all series
const { area, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
const instance = displaySet.images.find(
image => image.SOPInstanceUID === SOPInstanceUID
);
let InstanceNumber;
if (instance) {
InstanceNumber = instance.InstanceNumber;
}
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
// Area sometimes becomes undefined if `preventHandleOutsideImage` is off.
const roundedArea = utils.roundNumber(area || 0, 2);
displayText.push(`${roundedArea} mm<sup>2</sup>`);
// Todo: we need a better UI for displaying all these information
mappedAnnotations.forEach(mappedAnnotation => {
const { unit, max, SeriesNumber } = mappedAnnotation;
let maxStr = '';
if (max) {
const roundedMax = utils.roundNumber(max, 2);
maxStr = `Max: ${roundedMax} <small>${unit}</small> `;
}
const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`;
if (!displayText.includes(str)) {
displayText.push(str);
}
});
return displayText;
}
export default RectangleROI;

View File

@ -1 +1,11 @@
export default ['Length', 'EllipticalROI', 'Bidirectional', 'ArrowAnnotate']; export default [
'Length',
'EllipticalROI',
'Bidirectional',
'ArrowAnnotate',
'Angle',
'CobbAngle',
'Probe',
'RectangleROI',
'PlanarFreehandROI',
];

View File

@ -0,0 +1,3 @@
import * as measurementMappingUtils from './utils';
export { measurementMappingUtils };

View File

@ -1,19 +1,24 @@
import { MeasurementService } from '@ohif/core';
import Length from './Length'; import Length from './Length';
import Bidirectional from './Bidirectional'; import Bidirectional from './Bidirectional';
import EllipticalROI from './EllipticalROI'; import EllipticalROI from './EllipticalROI';
import ArrowAnnotate from './ArrowAnnotate'; import ArrowAnnotate from './ArrowAnnotate';
import CobbAngle from './CobbAngle';
import Angle from './Angle';
import PlanarFreehandROI from './PlanarFreehandROI';
import RectangleROI from './RectangleROI';
const measurementServiceMappingsFactory = ( const measurementServiceMappingsFactory = (
MeasurementService, measurementService: MeasurementService,
DisplaySetService, displaySetService,
CornerstoneViewportService cornerstoneViewportService
) => { ) => {
/** /**
* Maps measurement service format object to cornerstone annotation object. * Maps measurement service format object to cornerstone annotation object.
* *
* @param {Measurement} measurement The measurement instance * @param measurement The measurement instance
* @param {string} definition The source definition * @param definition The source definition
* @return {Object} Cornerstone annotation data * @return Cornerstone annotation data
*/ */
const _getValueTypeFromToolType = toolType => { const _getValueTypeFromToolType = toolType => {
@ -23,6 +28,7 @@ const measurementServiceMappingsFactory = (
RECTANGLE, RECTANGLE,
BIDIRECTIONAL, BIDIRECTIONAL,
POINT, POINT,
ANGLE,
} = MeasurementService.VALUE_TYPES; } = MeasurementService.VALUE_TYPES;
// TODO -> I get why this was attempted, but its not nearly flexible enough. // TODO -> I get why this was attempted, but its not nearly flexible enough.
@ -32,21 +38,24 @@ const measurementServiceMappingsFactory = (
Length: POLYLINE, Length: POLYLINE,
EllipticalROI: ELLIPSE, EllipticalROI: ELLIPSE,
RectangleROI: RECTANGLE, RectangleROI: RECTANGLE,
PlanarFreehandROI: POLYLINE,
Bidirectional: BIDIRECTIONAL, Bidirectional: BIDIRECTIONAL,
ArrowAnnotate: POINT, ArrowAnnotate: POINT,
CobbAngle: ANGLE,
Angle: ANGLE,
}; };
return TOOL_TYPE_TO_VALUE_TYPE[toolType]; return TOOL_TYPE_TO_VALUE_TYPE[toolType];
}; };
return { const factories = {
Length: { Length: {
toAnnotation: Length.toAnnotation, toAnnotation: Length.toAnnotation,
toMeasurement: csToolsAnnotation => toMeasurement: csToolsAnnotation =>
Length.toMeasurement( Length.toMeasurement(
csToolsAnnotation, csToolsAnnotation,
DisplaySetService, displaySetService,
CornerstoneViewportService, cornerstoneViewportService,
_getValueTypeFromToolType _getValueTypeFromToolType
), ),
matchingCriteria: [ matchingCriteria: [
@ -61,8 +70,8 @@ const measurementServiceMappingsFactory = (
toMeasurement: csToolsAnnotation => toMeasurement: csToolsAnnotation =>
Bidirectional.toMeasurement( Bidirectional.toMeasurement(
csToolsAnnotation, csToolsAnnotation,
DisplaySetService, displaySetService,
CornerstoneViewportService, cornerstoneViewportService,
_getValueTypeFromToolType _getValueTypeFromToolType
), ),
matchingCriteria: [ matchingCriteria: [
@ -78,13 +87,14 @@ const measurementServiceMappingsFactory = (
}, },
], ],
}, },
EllipticalROI: { EllipticalROI: {
toAnnotation: EllipticalROI.toAnnotation, toAnnotation: EllipticalROI.toAnnotation,
toMeasurement: csToolsAnnotation => toMeasurement: csToolsAnnotation =>
EllipticalROI.toMeasurement( EllipticalROI.toMeasurement(
csToolsAnnotation, csToolsAnnotation,
DisplaySetService, displaySetService,
CornerstoneViewportService, cornerstoneViewportService,
_getValueTypeFromToolType _getValueTypeFromToolType
), ),
matchingCriteria: [ matchingCriteria: [
@ -93,13 +103,46 @@ const measurementServiceMappingsFactory = (
}, },
], ],
}, },
RectangleROI: {
toAnnotation: RectangleROI.toAnnotation,
toMeasurement: csToolsAnnotation =>
RectangleROI.toMeasurement(
csToolsAnnotation,
displaySetService,
cornerstoneViewportService,
_getValueTypeFromToolType
),
matchingCriteria: [
{
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
},
],
},
PlanarFreehandROI: {
toAnnotation: PlanarFreehandROI.toAnnotation,
toMeasurement: csToolsAnnotation =>
PlanarFreehandROI.toMeasurement(
csToolsAnnotation,
displaySetService,
cornerstoneViewportService,
_getValueTypeFromToolType
),
matchingCriteria: [
{
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
},
],
},
ArrowAnnotate: { ArrowAnnotate: {
toAnnotation: ArrowAnnotate.toAnnotation, toAnnotation: ArrowAnnotate.toAnnotation,
toMeasurement: csToolsAnnotation => toMeasurement: csToolsAnnotation =>
ArrowAnnotate.toMeasurement( ArrowAnnotate.toMeasurement(
csToolsAnnotation, csToolsAnnotation,
DisplaySetService, displaySetService,
CornerstoneViewportService, cornerstoneViewportService,
_getValueTypeFromToolType _getValueTypeFromToolType
), ),
matchingCriteria: [ matchingCriteria: [
@ -109,7 +152,41 @@ const measurementServiceMappingsFactory = (
}, },
], ],
}, },
CobbAngle: {
toAnnotation: CobbAngle.toAnnotation,
toMeasurement: csToolsAnnotation =>
CobbAngle.toMeasurement(
csToolsAnnotation,
displaySetService,
cornerstoneViewportService,
_getValueTypeFromToolType
),
matchingCriteria: [
{
valueType: MeasurementService.VALUE_TYPES.ANGLE,
},
],
},
Angle: {
toAnnotation: Angle.toAnnotation,
toMeasurement: csToolsAnnotation =>
Angle.toMeasurement(
csToolsAnnotation,
displaySetService,
cornerstoneViewportService,
_getValueTypeFromToolType
),
matchingCriteria: [
{
valueType: MeasurementService.VALUE_TYPES.ANGLE,
},
],
},
}; };
return factories;
}; };
export default measurementServiceMappingsFactory; export default measurementServiceMappingsFactory;

View File

@ -13,7 +13,7 @@ import * as cornerstone from '@cornerstonejs/core';
*/ */
export default function getSOPInstanceAttributes( export default function getSOPInstanceAttributes(
imageId, imageId,
CornerstoneViewportService, cornerstoneViewportService,
viewportId viewportId
) { ) {
if (imageId) { if (imageId) {
@ -23,7 +23,7 @@ export default function getSOPInstanceAttributes(
// Todo: implement for volume viewports and use the referencedSeriesInstanceUID // Todo: implement for volume viewports and use the referencedSeriesInstanceUID
// if no imageId => measurement is not in the acquisition plane // if no imageId => measurement is not in the acquisition plane
// const metadata = getUIDFromScene(CornerstoneViewportService, viewportId); // const metadata = getUIDFromScene(cornerstoneViewportService, viewportId);
// if (!metadata) { // if (!metadata) {
// throw new Error('Not viewport with imageId found'); // throw new Error('Not viewport with imageId found');
@ -49,8 +49,8 @@ function _getUIDFromImageID(imageId) {
}; };
} }
// function getUIDFromScene(CornerstoneViewportService) { // function getUIDFromScene(cornerstoneViewportService) {
// const renderingEngine = CornerstoneViewportService.getRenderingEngine(); // const renderingEngine = cornerstoneViewportService.getRenderingEngine();
// const scene = renderingEngine.getScene(sceneUID); // const scene = renderingEngine.getScene(sceneUID);
// const viewportUIDs = scene.getViewportIds(); // const viewportUIDs = scene.getViewportIds();

View File

@ -0,0 +1,17 @@
import getHandlesFromPoints from './getHandlesFromPoints';
import {
isAnnotationSelected,
setAnnotationSelected,
getFirstAnnotationSelected,
} from './selection';
import getModalityUnit from './getModalityUnit';
import getSOPInstanceAttributes from './getSOPInstanceAttributes';
export {
getModalityUnit,
getHandlesFromPoints,
getSOPInstanceAttributes,
isAnnotationSelected,
setAnnotationSelected,
getFirstAnnotationSelected,
};

View File

@ -0,0 +1,41 @@
import { annotation as cs3dToolAnnotationUtils } from '@cornerstonejs/tools';
/**
* Check whether an annotation from imaging library is selected or not.
* @param {string} annotationUID uid of imaging library annotation
* @returns boolean
*/
function isAnnotationSelected(annotationUID: string): boolean {
return cs3dToolAnnotationUtils.selection.isAnnotationSelected(annotationUID);
}
/**
* Change an annotation from imaging library's selected property.
* @param annotationUID - uid of imaging library annotation
* @param selected - new value for selected
*/
function setAnnotationSelected(annotationUID: string, selected: boolean): void {
const isCurrentSelected = isAnnotationSelected(annotationUID);
// branch cut, avoid invoking imaging library unnecessarily.
if (isCurrentSelected !== selected) {
cs3dToolAnnotationUtils.selection.setAnnotationSelected(
annotationUID,
selected
);
}
}
function getFirstAnnotationSelected(element) {
const [selectedAnnotationUID] =
cs3dToolAnnotationUtils.selection.getAnnotationsSelected() || [];
if (selectedAnnotationUID) {
return cs3dToolAnnotationUtils.state.getAnnotation(selectedAnnotationUID);
}
}
export {
isAnnotationSelected,
setAnnotationSelected,
getFirstAnnotationSelected,
};

View File

@ -39,34 +39,39 @@ export default function toggleMPRHangingProtocol({
getToolGroup, getToolGroup,
}) { }) {
const { const {
UINotificationService, uiNotificationService,
HangingProtocolService, hangingProtocolService,
ViewportGridService, viewportGridService,
ToolBarService, toolbarService,
} = servicesManager.services; } = servicesManager.services;
// TODO Introduce a service to persist the state of the current hanging protocol/app.
// So all of the code to persist the state here will no longer be needed. Perhaps
// just the id of the current hanging protocol to toggle MPR off is needed.
const { const {
activeViewportIndex, activeViewportIndex,
viewports, viewports,
numRows, numRows,
numCols, numCols,
} = ViewportGridService.getState(); } = viewportGridService.getState();
const viewportDisplaySetInstanceUIDs = const viewportDisplaySetInstanceUIDs =
viewports[activeViewportIndex].displaySetInstanceUIDs; viewports[activeViewportIndex].displaySetInstanceUIDs;
const errorCallback = error => { // What is the current active protocol and stage number to restore later
UINotificationService.show({ const { protocol, stage } = hangingProtocolService.getActiveProtocol();
const restoreErrorCallback = error => {
console.error(error);
uiNotificationService.show({
title: 'Multiplanar reconstruction (MPR) ', title: 'Multiplanar reconstruction (MPR) ',
message: message:
'Cannot create MPR for this DisplaySet since it is not reconstructable.', 'Something went wrong while trying to restore the previous layout.',
type: 'info', type: 'info',
duration: 3000, duration: 3000,
}); });
}; };
// What is the current active protocol and stage number to restore later
const { protocol, stage } = HangingProtocolService.getActiveProtocol();
if (toggledState) { if (toggledState) {
resetCachedState(); resetCachedState();
@ -98,7 +103,24 @@ export default function toggleMPRHangingProtocol({
getToolGroup getToolGroup
); );
HangingProtocolService.setProtocol( const errorCallback = error => {
// Unable to create MPR, so be sure to return to the cached/original protocol.
hangingProtocolService.setProtocol(
cachedState.protocol.id,
viewportMatchDetails,
restoreErrorCallback
);
uiNotificationService.show({
title: 'Multiplanar reconstruction (MPR) ',
message:
'Cannot create MPR for this DisplaySet since it is not reconstructable.',
type: 'info',
duration: 3000,
});
};
hangingProtocolService.setProtocol(
MPR_TOOLGROUP_ID, MPR_TOOLGROUP_ID,
matchDetails, matchDetails,
errorCallback errorCallback
@ -106,16 +128,6 @@ export default function toggleMPRHangingProtocol({
return; return;
} }
const restoreErrorCallback = error => {
UINotificationService.show({
title: 'Multiplanar reconstruction (MPR) ',
message:
'Something went wrong while trying to restore the previous layout.',
type: 'info',
duration: 3000,
});
};
_disableCrosshairs([MPR_TOOLGROUP_ID], getToolGroup); _disableCrosshairs([MPR_TOOLGROUP_ID], getToolGroup);
const { layoutType, properties } = cachedState.viewportStructure; const { layoutType, properties } = cachedState.viewportStructure;
@ -126,7 +138,7 @@ export default function toggleMPRHangingProtocol({
// cached protocol and stage. However, for the default protocol, we need // cached protocol and stage. However, for the default protocol, we need
// to also apply the layout type and properties. // to also apply the layout type and properties.
if (cachedState.protocol.id !== 'default') { if (cachedState.protocol.id !== 'default') {
HangingProtocolService.setProtocol( hangingProtocolService.setProtocol(
cachedState.protocol.id, cachedState.protocol.id,
viewportMatchDetails, viewportMatchDetails,
restoreErrorCallback restoreErrorCallback
@ -135,14 +147,14 @@ export default function toggleMPRHangingProtocol({
return; return;
} }
HangingProtocolService.setProtocol( hangingProtocolService.setProtocol(
'default', 'default',
viewportMatchDetails, viewportMatchDetails,
restoreErrorCallback restoreErrorCallback
); );
if (numRows !== properties.rows || numCols !== properties.columns) { if (numRows !== properties.rows || numCols !== properties.columns) {
ViewportGridService.setLayout({ viewportGridService.setLayout({
numRows: properties.rows, numRows: properties.rows,
numCols: properties.columns, numCols: properties.columns,
layoutType, layoutType,
@ -165,7 +177,7 @@ export default function toggleMPRHangingProtocol({
viewportOptions, viewportOptions,
displaySetsInfo, displaySetsInfo,
} = viewportMatchDetailsForViewport; } = viewportMatchDetailsForViewport;
ViewportGridService.setDisplaySetsForViewport({ viewportGridService.setDisplaySetsForViewport({
viewportIndex, viewportIndex,
displaySetInstanceUIDs: displaySetsInfo.map( displaySetInstanceUIDs: displaySetsInfo.map(
displaySetInfo => displaySetInfo.displaySetInstanceUID displaySetInfo => displaySetInfo.displaySetInstanceUID
@ -173,7 +185,7 @@ export default function toggleMPRHangingProtocol({
viewportOptions, viewportOptions,
}); });
} else { } else {
ViewportGridService.setDisplaySetsForViewport({ viewportGridService.setDisplaySetsForViewport({
viewportIndex, viewportIndex,
displaySetInstanceUIDs: [], displaySetInstanceUIDs: [],
viewportOptions: {}, viewportOptions: {},
@ -181,7 +193,7 @@ export default function toggleMPRHangingProtocol({
} }
}); });
ToolBarService.recordInteraction({ toolbarService.recordInteraction({
groupId: 'WindowLevel', groupId: 'WindowLevel',
itemId: 'WindowLevel', itemId: 'WindowLevel',
interactionType: 'tool', interactionType: 'tool',
@ -213,15 +225,15 @@ function _disableCrosshairs(toolGroupIds, getToolGroup) {
function _getViewportsInfo({ protocol, stage, viewports, servicesManager }) { function _getViewportsInfo({ protocol, stage, viewports, servicesManager }) {
// here we need to use the viewports and try to map it into the // here we need to use the viewports and try to map it into the
// viewportMatchDetails and displaySetMatch that HangingProtocolService // viewportMatchDetails and displaySetMatch that hangingProtocolService
// expects // expects
const { const {
ViewportGridService, viewportGridService,
HangingProtocolService, hangingProtocolService,
ToolGroupService, toolGroupService,
} = servicesManager.services; } = servicesManager.services;
const { numRows, numCols } = ViewportGridService.getState(); const { numRows, numCols } = viewportGridService.getState();
let viewportMatchDetails = new Map(); let viewportMatchDetails = new Map();
@ -257,7 +269,7 @@ function _getViewportsInfo({ protocol, stage, viewports, servicesManager }) {
} }
}); });
} else { } else {
({ viewportMatchDetails } = HangingProtocolService.getMatchDetails()); ({ viewportMatchDetails } = hangingProtocolService.getMatchDetails());
} }
// get the toolGroup state for viewports // get the toolGroup state for viewports
@ -272,13 +284,17 @@ function _getViewportsInfo({ protocol, stage, viewports, servicesManager }) {
.filter(Boolean); .filter(Boolean);
if (viewportIds.length) { if (viewportIds.length) {
toolOptions = viewportIds.map(viewportId => { toolOptions = viewportIds
const toolGroup = ToolGroupService.getToolGroupForViewport(viewportId); .map(viewportId => {
return { const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
toolGroupId: toolGroup.id, return toolGroup
toolOptions: toolGroup.toolOptions, ? {
}; toolGroupId: toolGroup.id,
}); toolOptions: toolGroup.toolOptions,
}
: null;
})
.filter(Boolean);
} }
return { viewportMatchDetails, viewportStructure, toolOptions }; return { viewportMatchDetails, viewportStructure, toolOptions };

View File

@ -0,0 +1,121 @@
import { cache, imageLoadPoolManager, Enums } from '@cornerstonejs/core';
import getNthFrames from './getNthFrames';
import interleave from './interleave';
// Map of volumeId and SeriesInstanceId
const volumeIdMapsToLoad = new Map<string, string>();
const viewportIdVolumeInputArrayMap = new Map<string, unknown[]>();
/**
* This function caches the volumeUIDs until all the volumes inside the
* hanging protocol are initialized. Then it goes through the requests and
* chooses a sub-selection starting the the first few objects, center objects
* and last objects, and then the remaining nth images until all instances are
* retrieved. This causes the image to have a progressive load order and looks
* visually much better.
* @param {Object} props image loading properties from Cornerstone ViewportService
*/
export default function interleaveNthLoader({
data: { viewportId, volumeInputArray },
displaySetsMatchDetails,
viewportMatchDetails: matchDetails,
}) {
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);
// Based on the volumeInputs store the volumeIds and SeriesInstanceIds
// to keep track of the volumes being loaded
for (const volumeInput of volumeInputArray) {
const { volumeId } = volumeInput;
const volume = cache.getVolume(volumeId);
if (!volume) {
return;
}
// if the volumeUID is not in the volumeUIDs array, add it
if (!volumeIdMapsToLoad.has(volumeId)) {
const { metadata } = volume;
volumeIdMapsToLoad.set(volumeId, metadata.SeriesInstanceUID);
}
}
/**
* The following is checking if all the viewports that were matched in the HP has been
* successfully created their cornerstone viewport or not. Todo: This can be
* improved by not checking it, and as soon as the matched DisplaySets have their
* volume loaded, we start the loading, but that comes at the cost of viewports
* not being created yet (e.g., in a 10 viewport ptCT fusion, when one ct viewport and one
* pt viewport are created we have a guarantee that the volumes are created in the cache
* but the rest of the viewports (fusion, mip etc.) are not created yet. So
* we can't initiate setting the volumes for those viewports. One solution can be
* to add an event when a viewport is created (not enabled element event) and then
* listen to it and as the other viewports are created we can set the volumes for them
* since volumes are already started loading.
*/
if (matchDetails.size !== viewportIdVolumeInputArrayMap.size) {
return;
}
// Check if all the matched volumes are loaded
for (const [_, details] of displaySetsMatchDetails.entries()) {
const { SeriesInstanceUID } = details;
// HangingProtocol has matched, but don't have all the volumes created yet, so return
if (!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)) {
return;
}
}
const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice();
// get volumes from cache
const volumes = volumeIds.map(volumeId => {
return cache.getVolume(volumeId);
});
// iterate over all volumes, and get their imageIds, and interleave
// the imageIds and save them in AllRequests for later use
const originalRequests = volumes
.map(volume => volume.getImageLoadRequests())
.filter(requests => requests?.[0]?.imageId);
const orderedRequests = originalRequests.map(request =>
getNthFrames(request)
);
// set the finalRequests to the imageLoadPoolManager
const finalRequests = interleave(orderedRequests);
const requestType = Enums.RequestType.Prefetch;
const priority = 0;
finalRequests.forEach(
({ callLoadImage, additionalDetails, imageId, imageIdIndex, options }) => {
const callLoadImageBound = callLoadImage.bind(
null,
imageId,
imageIdIndex,
options
);
imageLoadPoolManager.addRequest(
callLoadImageBound,
requestType,
additionalDetails,
priority
);
}
);
// clear the volumeIdMapsToLoad
volumeIdMapsToLoad.clear();
// copy the viewportIdVolumeInputArrayMap
const viewportIdVolumeInputArrayMapCopy = new Map(
viewportIdVolumeInputArrayMap
);
// reset the viewportIdVolumeInputArrayMap
viewportIdVolumeInputArrayMap.clear();
return viewportIdVolumeInputArrayMapCopy;
}

View File

@ -12,10 +12,10 @@ export default function toggleStackImageSync({
getEnabledElement, getEnabledElement,
}) { }) {
const { const {
SyncGroupService, syncGroupService,
ViewportGridService, viewportGridService,
DisplaySetService, displaySetService,
CornerstoneViewportService, cornerstoneViewportService,
} = servicesManager.services; } = servicesManager.services;
if (!toggledState) { if (!toggledState) {
@ -23,7 +23,7 @@ export default function toggleStackImageSync({
const { viewports, synchronizerId } = syncGroupInfo; const { viewports, synchronizerId } = syncGroupInfo;
viewports.forEach(({ viewportId, renderingEngineId }) => { viewports.forEach(({ viewportId, renderingEngineId }) => {
SyncGroupService.removeViewportFromSyncGroup( syncGroupService.removeViewportFromSyncGroup(
viewportId, viewportId,
renderingEngineId, renderingEngineId,
synchronizerId synchronizerId
@ -37,7 +37,7 @@ export default function toggleStackImageSync({
STACK_IMAGE_SYNC_GROUPS_INFO = []; STACK_IMAGE_SYNC_GROUPS_INFO = [];
// create synchronization groups and add viewports // create synchronization groups and add viewports
let { viewports } = ViewportGridService.getState(); let { viewports } = viewportGridService.getState();
// filter empty viewports // filter empty viewports
viewports = viewports.filter( viewports = viewports.filter(
@ -50,7 +50,7 @@ export default function toggleStackImageSync({
const { displaySetInstanceUIDs } = viewport; const { displaySetInstanceUIDs } = viewport;
for (const displaySetInstanceUID of displaySetInstanceUIDs) { for (const displaySetInstanceUID of displaySetInstanceUIDs) {
const displaySet = DisplaySetService.getDisplaySetByUID( const displaySet = displaySetService.getDisplaySetByUID(
displaySetInstanceUID displaySetInstanceUID
); );
@ -70,7 +70,7 @@ export default function toggleStackImageSync({
return acc; return acc;
} }
const { element } = CornerstoneViewportService.getViewportInfo(viewportId); const { element } = cornerstoneViewportService.getViewportInfo(viewportId);
const { viewport: csViewport, renderingEngineId } = getEnabledElement( const { viewport: csViewport, renderingEngineId } = getEnabledElement(
element element
); );
@ -99,7 +99,7 @@ export default function toggleStackImageSync({
calculateViewportRegistrations(viewports); calculateViewportRegistrations(viewports);
viewports.forEach(({ viewportId, renderingEngineId }) => { viewports.forEach(({ viewportId, renderingEngineId }) => {
SyncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, { syncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, {
type: 'stackimage', type: 'stackimage',
id: synchronizerId, id: synchronizerId,
source: true, source: true,

View File

@ -32,8 +32,8 @@
"peerDependencies": { "peerDependencies": {
"@ohif/core": "^3.0.0", "@ohif/core": "^3.0.0",
"@ohif/i18n": "^1.0.0", "@ohif/i18n": "^1.0.0",
"dcmjs": "^0.28.3", "dcmjs": "^0.29.4",
"dicomweb-client": "^0.6.0", "dicomweb-client": "^0.8.4",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
@ -42,7 +42,7 @@
"webpack-merge": "^5.7.3" "webpack-merge": "^5.7.3"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.16.3", "@babel/runtime": "^7.20.13",
"@cornerstonejs/calculate-suv": "^1.0.3" "@cornerstonejs/calculate-suv": "^1.0.3"
} }
} }

View File

@ -0,0 +1,73 @@
import React from 'react';
import { DicomMetadataStore } from '@ohif/core';
/**
*
* @param {*} servicesManager
* @param {*} dataSource
* @param {*} measurements
* @param {*} options
* @returns {string[]} displaySetInstanceUIDs
*/
async function createReportAsync(
servicesManager,
commandsManager,
dataSource,
measurements,
options
) {
const {
displaySetService,
uiNotificationService,
uiDialogService,
} = servicesManager.services;
const loadingDialogId = uiDialogService.create({
showOverlay: true,
isDraggable: false,
centralize: true,
// TODO: Create a loading indicator component + zeplin design?
content: Loading,
});
try {
const naturalizedReport = await commandsManager.runCommand(
'storeMeasurements',
{
measurementData: measurements,
dataSource,
additionalFindingTypes: ['ArrowAnnotate'],
options,
},
'CORNERSTONE_STRUCTURED_REPORT'
);
// The "Mode" route listens for DicomMetadataStore changes
// When a new instance is added, it listens and
// automatically calls makeDisplaySets
DicomMetadataStore.addInstances([naturalizedReport], true);
const displaySetInstanceUID = displaySetService.getMostRecentDisplaySet();
uiNotificationService.show({
title: 'Create Report',
message: 'Measurements saved successfully',
type: 'success',
});
return [displaySetInstanceUID];
} catch (error) {
uiNotificationService.show({
title: 'Create Report',
message: error.message || 'Failed to store measurements',
type: 'error',
});
} finally {
uiDialogService.dismiss({ id: loadingDialogId });
}
}
function Loading() {
return <div className="text-primary-active">Loading...</div>;
}
export default createReportAsync;

View File

@ -206,8 +206,10 @@ function createDicomJSONApi(dicomJsonConfig) {
url: instance.url, url: instance.url,
imageId: instance.url, imageId: instance.url,
...series, ...series,
...study,
}; };
delete obj.instances; delete obj.instances;
delete obj.series;
return obj; return obj;
}); });
storeInstances(instances); storeInstances(instances);

View File

@ -5,19 +5,37 @@ import dcmjs from 'dcmjs';
const metadataProvider = OHIF.classes.MetadataProvider; const metadataProvider = OHIF.classes.MetadataProvider;
const { EVENTS } = DicomMetadataStore; const { EVENTS } = DicomMetadataStore;
// Sorting SR modalities to be at the end of series list const END_MODALITIES = {
function customSort(seriesA, seriesB) { SR: true,
const modalityA = seriesA.instances[0].Modality; SEG: true,
const modalityB = seriesB.instances[0].Modality; DOC: true,
};
if (modalityA === 'SR') { const compareValue = (v1, v2, def = 0) => {
return +1; if (v1 === v2) return def;
if (v1 < v2) return -1;
return 1;
};
// Sorting SR modalities to be at the end of series list
const customSort = (seriesA, seriesB) => {
const instanceA = seriesA.instances[0];
const instanceB = seriesB.instances[0];
const modalityA = instanceA.Modality;
const modalityB = instanceB.Modality;
const isEndA = END_MODALITIES[modalityA];
const isEndB = END_MODALITIES[modalityB];
if (isEndA && isEndB) {
// Compare by series date
return compareValue(instanceA.SeriesNumber, instanceB.SeriesNumber);
} }
if (modalityB === 'SR') { if (!isEndA && !isEndB) {
return -1; return compareValue(instanceB.SeriesNumber, instanceA.SeriesNumber);
} }
return 0; return isEndA ? -1 : 1;
} };
function createDicomLocalApi(dicomLocalConfig) { function createDicomLocalApi(dicomLocalConfig) {
const { name } = dicomLocalConfig; const { name } = dicomLocalConfig;

View File

@ -49,7 +49,7 @@ const metadataProvider = classes.MetadataProvider;
* @param {bool} lazyLoadStudy - "enableStudyLazyLoad"; Request series meta async instead of blocking * @param {bool} lazyLoadStudy - "enableStudyLazyLoad"; Request series meta async instead of blocking
* @param {string|bool} singlepart - indicates of the retrieves can fetch singlepart. Options are bulkdata, video, image or boolean true * @param {string|bool} singlepart - indicates of the retrieves can fetch singlepart. Options are bulkdata, video, image or boolean true
*/ */
function createDicomWebApi(dicomWebConfig, UserAuthenticationService) { function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
const { const {
qidoRoot, qidoRoot,
wadoRoot, wadoRoot,
@ -65,7 +65,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
url: qidoRoot, url: qidoRoot,
staticWado, staticWado,
singlepart, singlepart,
headers: UserAuthenticationService.getAuthorizationHeader(), headers: userAuthenticationService.getAuthorizationHeader(),
errorInterceptor: errorHandler.getHTTPErrorHandler(), errorInterceptor: errorHandler.getHTTPErrorHandler(),
}; };
@ -73,7 +73,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
url: wadoRoot, url: wadoRoot,
staticWado, staticWado,
singlepart, singlepart,
headers: UserAuthenticationService.getAuthorizationHeader(), headers: userAuthenticationService.getAuthorizationHeader(),
errorInterceptor: errorHandler.getHTTPErrorHandler(), errorInterceptor: errorHandler.getHTTPErrorHandler(),
}; };
@ -105,7 +105,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
studies: { studies: {
mapParams: mapParams.bind(), mapParams: mapParams.bind(),
search: async function (origParams) { search: async function (origParams) {
const headers = UserAuthenticationService.getAuthorizationHeader(); const headers = userAuthenticationService.getAuthorizationHeader();
if (headers) { if (headers) {
qidoDicomWebClient.headers = headers; qidoDicomWebClient.headers = headers;
} }
@ -130,7 +130,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
series: { series: {
// mapParams: mapParams.bind(), // mapParams: mapParams.bind(),
search: async function (studyInstanceUid) { search: async function (studyInstanceUid) {
const headers = UserAuthenticationService.getAuthorizationHeader(); const headers = userAuthenticationService.getAuthorizationHeader();
if (headers) { if (headers) {
qidoDicomWebClient.headers = headers; qidoDicomWebClient.headers = headers;
} }
@ -146,7 +146,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
}, },
instances: { instances: {
search: (studyInstanceUid, queryParameters) => { search: (studyInstanceUid, queryParameters) => {
const headers = UserAuthenticationService.getAuthorizationHeader(); const headers = userAuthenticationService.getAuthorizationHeader();
if (headers) { if (headers) {
qidoDicomWebClient.headers = headers; qidoDicomWebClient.headers = headers;
} }
@ -184,7 +184,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
sortFunction, sortFunction,
madeInClient = false, madeInClient = false,
} = {}) => { } = {}) => {
const headers = UserAuthenticationService.getAuthorizationHeader(); const headers = userAuthenticationService.getAuthorizationHeader();
if (headers) { if (headers) {
wadoDicomWebClient.headers = headers; wadoDicomWebClient.headers = headers;
} }
@ -218,7 +218,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
store: { store: {
dicom: async dataset => { dicom: async dataset => {
const headers = UserAuthenticationService.getAuthorizationHeader(); const headers = userAuthenticationService.getAuthorizationHeader();
if (headers) { if (headers) {
wadoDicomWebClient.headers = headers; wadoDicomWebClient.headers = headers;
} }

View File

@ -23,6 +23,7 @@
* | offset | {number} | * | offset | {number} |
*/ */
import { DICOMWeb, utils } from '@ohif/core'; import { DICOMWeb, utils } from '@ohif/core';
import { sortStudySeries } from '@ohif/core/src/utils/sortStudy';
const { getString, getName, getModalities } = DICOMWeb; const { getString, getName, getModalities } = DICOMWeb;
@ -90,6 +91,8 @@ export function processSeriesResults(qidoSeries) {
); );
} }
sortStudySeries(series);
return series; return series;
} }

View File

@ -1,5 +1,8 @@
import dcmjs from 'dcmjs'; import dcmjs from 'dcmjs';
import { sortStudySeries, sortingCriteria } from '../utils/sortStudy'; import {
sortStudySeries,
sortingCriteria,
} from '@ohif/core/src/utils/sortStudy';
import RetrieveMetadataLoader from './retrieveMetadataLoader'; import RetrieveMetadataLoader from './retrieveMetadataLoader';
/** /**

View File

@ -13,16 +13,10 @@ function ActionButtons({ onExportClick, onCreateReportClick }) {
<Button className="px-2 py-2 text-base" onClick={onExportClick}> <Button className="px-2 py-2 text-base" onClick={onExportClick}>
{t('Export CSV')} {t('Export CSV')}
</Button> </Button>
<Button className="px-2 py-2 text-base" onClick={onCreateReportClick}>
{t('Create Report')}
</Button>
</ButtonGroup> </ButtonGroup>
{/* <Button
className="ml-2 text-base"
variant="outlined"
size="small"
color="black"
onClick={onCreateReportClick}
>
{t('Create Report')}
</Button> */}
</React.Fragment> </React.Fragment>
); );
} }

View File

@ -0,0 +1,56 @@
import React from 'react';
import classnames from 'classnames';
import { useNavigate } from 'react-router-dom';
import { useAppConfig } from '@state';
import { Button } from '@ohif/ui';
function DataSourceSelector() {
const [appConfig] = useAppConfig();
const navigate = useNavigate();
// This is frowned upon, but the raw config is needed here to provide
// the selector
const dsConfigs = appConfig.dataSources;
return (
<div style={{ width: '100%', height: '100%' }}>
<div className="h-screen w-screen flex justify-center items-center ">
<div className="py-8 px-8 mx-auto bg-secondary-dark drop-shadow-md space-y-2 rounded-lg">
<img
className="block mx-auto h-14"
src="./ohif-logo.svg"
alt="OHIF"
/>
<div className="text-center space-y-2 pt-4">
{dsConfigs
.filter(
it =>
it.sourceName !== 'dicomjson' &&
it.sourceName !== 'dicomlocal'
)
.map(ds => (
<div key={ds.sourceName}>
<h1 className="text-white">{ds.friendlyName}</h1>
<Button
className={classnames('font-bold', 'ml-2')}
onClick={() => {
navigate({
pathname: '/',
search: `datasources=${ds.sourceName}`,
});
}}
>
{ds.sourceName}
</Button>
<br />
</div>
))}
</div>
</div>
</div>
</div>
);
}
export default DataSourceSelector;

View File

@ -1,19 +1,31 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { utils, ServicesManager } from '@ohif/core';
import { MeasurementTable, Dialog, Input, useViewportGrid } from '@ohif/ui'; import { MeasurementTable, Dialog, Input, useViewportGrid } from '@ohif/ui';
import ActionButtons from './ActionButtons'; import ActionButtons from './ActionButtons';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import { utils } from '@ohif/core'; import createReportDialogPrompt, {
CREATE_REPORT_DIALOG_RESPONSE,
} from './createReportDialogPrompt';
import createReportAsync from '../Actions/createReportAsync';
import findSRWithSameSeriesDescription from '../utils/findSRWithSameSeriesDescription';
const { downloadCSVReport } = utils; const { downloadCSVReport } = utils;
export default function PanelMeasurementTable({ export default function PanelMeasurementTable({
servicesManager, servicesManager,
commandsManager, commandsManager,
}) { extensionManager,
}): React.FunctionComponent {
const [viewportGrid, viewportGridService] = useViewportGrid(); const [viewportGrid, viewportGridService] = useViewportGrid();
const { MeasurementService, UIDialogService } = servicesManager.services; const { activeViewportIndex, viewports } = viewportGrid;
const {
measurementService,
uiDialogService,
uiNotificationService,
displaySetService,
} = (servicesManager as ServicesManager).services;
const [displayMeasurements, setDisplayMeasurements] = useState([]); const [displayMeasurements, setDisplayMeasurements] = useState([]);
useEffect(() => { useEffect(() => {
@ -22,21 +34,21 @@ export default function PanelMeasurementTable({
100 100
); );
// ~~ Initial // ~~ Initial
setDisplayMeasurements(_getMappedMeasurements(MeasurementService)); setDisplayMeasurements(_getMappedMeasurements(measurementService));
// ~~ Subscription // ~~ Subscription
const added = MeasurementService.EVENTS.MEASUREMENT_ADDED; const added = measurementService.EVENTS.MEASUREMENT_ADDED;
const addedRaw = MeasurementService.EVENTS.RAW_MEASUREMENT_ADDED; const addedRaw = measurementService.EVENTS.RAW_MEASUREMENT_ADDED;
const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED; const updated = measurementService.EVENTS.MEASUREMENT_UPDATED;
const removed = MeasurementService.EVENTS.MEASUREMENT_REMOVED; const removed = measurementService.EVENTS.MEASUREMENT_REMOVED;
const cleared = MeasurementService.EVENTS.MEASUREMENTS_CLEARED; const cleared = measurementService.EVENTS.MEASUREMENTS_CLEARED;
const subscriptions = []; const subscriptions = [];
[added, addedRaw, updated, removed, cleared].forEach(evt => { [added, addedRaw, updated, removed, cleared].forEach(evt => {
subscriptions.push( subscriptions.push(
MeasurementService.subscribe(evt, () => { measurementService.subscribe(evt, () => {
debouncedSetDisplayMeasurements( debouncedSetDisplayMeasurements(
_getMappedMeasurements(MeasurementService) _getMappedMeasurements(measurementService)
); );
}).unsubscribe }).unsubscribe
); );
@ -51,26 +63,84 @@ export default function PanelMeasurementTable({
}, []); }, []);
async function exportReport() { async function exportReport() {
const measurements = MeasurementService.getMeasurements(); const measurements = measurementService.getMeasurements();
downloadCSVReport(measurements, MeasurementService); downloadCSVReport(measurements, measurementService);
}
async function clearMeasurements() {
measurementService.clearMeasurements();
}
async function createReport(): Promise<any> {
// filter measurements that are added to the active study
const activeViewport = viewports[activeViewportIndex];
const measurements = measurementService.getMeasurements();
const displaySet = displaySetService.getDisplaySetByUID(
activeViewport.displaySetInstanceUIDs[0]
);
const trackedMeasurements = measurements.filter(
m => displaySet.StudyInstanceUID === m.referenceStudyUID
);
if (trackedMeasurements.length <= 0) {
uiNotificationService.show({
title: 'No Measurements',
message: 'No Measurements are added to the current Study.',
type: 'info',
duration: 3000,
});
return;
}
const promptResult = await createReportDialogPrompt(uiDialogService, {
extensionManager,
});
if (promptResult.action === CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT) {
const dataSources = extensionManager.getDataSources(
promptResult.dataSourceName
);
const dataSource = dataSources[0];
const SeriesDescription =
// isUndefinedOrEmpty
promptResult.value === undefined || promptResult.value === ''
? 'Research Derived Series' // default
: promptResult.value; // provided value
// Re-use an existing series having the same series description to avoid
// creating too many series instances.
const options = findSRWithSameSeriesDescription(
SeriesDescription,
displaySetService
);
return createReportAsync(
servicesManager,
commandsManager,
dataSource,
trackedMeasurements,
options
);
}
} }
const jumpToImage = ({ uid, isActive }) => { const jumpToImage = ({ uid, isActive }) => {
MeasurementService.jumpToMeasurement(viewportGrid.activeViewportIndex, uid); measurementService.jumpToMeasurement(viewportGrid.activeViewportIndex, uid);
onMeasurementItemClickHandler({ uid, isActive }); onMeasurementItemClickHandler({ uid, isActive });
}; };
const onMeasurementItemEditHandler = ({ uid, isActive }) => { const onMeasurementItemEditHandler = ({ uid, isActive }) => {
const measurement = MeasurementService.getMeasurement(uid); const measurement = measurementService.getMeasurement(uid);
//Todo: why we are jumping to image? //Todo: why we are jumping to image?
// jumpToImage({ id, isActive }); // jumpToImage({ id, isActive });
const onSubmitHandler = ({ action, value }) => { const onSubmitHandler = ({ action, value }) => {
switch (action.id) { switch (action.id) {
case 'save': { case 'save': {
MeasurementService.update( measurementService.update(
uid, uid,
{ {
...measurement, ...measurement,
@ -80,10 +150,10 @@ export default function PanelMeasurementTable({
); );
} }
} }
UIDialogService.dismiss({ id: 'enter-annotation' }); uiDialogService.dismiss({ id: 'enter-annotation' });
}; };
UIDialogService.create({ uiDialogService.create({
id: 'enter-annotation', id: 'enter-annotation',
centralize: true, centralize: true,
isDraggable: false, isDraggable: false,
@ -108,6 +178,7 @@ export default function PanelMeasurementTable({
<div className="p-4 bg-primary-dark"> <div className="p-4 bg-primary-dark">
<Input <Input
autoFocus autoFocus
id="annotation"
className="mt-2 bg-black border-primary-main" className="mt-2 bg-black border-primary-main"
type="text" type="text"
containerClassName="mr-2" containerClassName="mr-2"
@ -142,7 +213,7 @@ export default function PanelMeasurementTable({
return ( return (
<> <>
<div <div
className="overflow-x-hidden overflow-y-auto invisible-scrollbar" className="overflow-x-hidden overflow-y-auto ohif-scrollbar"
data-cy={'measurements-panel'} data-cy={'measurements-panel'}
> >
<MeasurementTable <MeasurementTable
@ -155,7 +226,8 @@ export default function PanelMeasurementTable({
<div className="flex justify-center p-4"> <div className="flex justify-center p-4">
<ActionButtons <ActionButtons
onExportClick={exportReport} onExportClick={exportReport}
onCreateReportClick={() => {}} onClearMeasurementsClick={clearMeasurements}
onCreateReportClick={createReport}
/> />
</div> </div>
</> </>
@ -163,37 +235,27 @@ export default function PanelMeasurementTable({
} }
PanelMeasurementTable.propTypes = { PanelMeasurementTable.propTypes = {
servicesManager: PropTypes.shape({ servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
services: PropTypes.shape({
MeasurementService: PropTypes.shape({
getMeasurements: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,
EVENTS: PropTypes.object.isRequired,
VALUE_TYPES: PropTypes.object.isRequired,
}).isRequired,
}).isRequired,
}).isRequired,
}; };
function _getMappedMeasurements(MeasurementService) { function _getMappedMeasurements(measurementService) {
const measurements = MeasurementService.getMeasurements(); const measurements = measurementService.getMeasurements();
const mappedMeasurements = measurements.map((m, index) => const mappedMeasurements = measurements.map((m, index) =>
_mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES) _mapMeasurementToDisplay(m, index, measurementService.VALUE_TYPES)
); );
return mappedMeasurements; return mappedMeasurements;
} }
function _mapMeasurementToDisplay(measurement, index, types) { function _mapMeasurementToDisplay(measurement, index, types) {
const { displayText, uid, label, type } = measurement; const { displayText, uid, label, type, selected } = measurement;
return { return {
uid, uid,
label: label || '(empty)', label: label || '(empty)',
measurementType: type, measurementType: type,
displayText: displayText || [], displayText: displayText || [],
// TODO: handle one layer down isActive: selected,
isActive: false, // activeMeasurementItem === i + 1,
}; };
} }

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { StudyBrowser, useImageViewer, useViewportGrid } from '@ohif/ui'; import { StudyBrowser, useImageViewer, useViewportGrid } from '@ohif/ui';
import { utils } from '@ohif/core'; import { utils } from '@ohif/core';
const { formatDate } = utils; const { sortStudyInstances, formatDate } = utils;
/** /**
* *
@ -17,9 +17,9 @@ function PanelStudyBrowser({
dataSource, dataSource,
}) { }) {
const { const {
HangingProtocolService, hangingProtocolService,
DisplaySetService, displaySetService,
UINotificationService, uiNotificationService,
} = servicesManager.services; } = servicesManager.services;
// Normally you nest the components so the tree isn't so deep, and the data // Normally you nest the components so the tree isn't so deep, and the data
// doesn't have to have such an intense shape. This works well enough for now. // doesn't have to have such an intense shape. This works well enough for now.
@ -42,13 +42,13 @@ function PanelStudyBrowser({
let updatedViewports = []; let updatedViewports = [];
const viewportIndex = activeViewportIndex; const viewportIndex = activeViewportIndex;
try { try {
updatedViewports = HangingProtocolService.getViewportsRequireUpdate( updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportIndex, viewportIndex,
displaySetInstanceUID displaySetInstanceUID
); );
} catch (error) { } catch (error) {
console.warn(error); console.warn(error);
UINotificationService.show({ uiNotificationService.show({
title: 'Thumbnail Double Click', title: 'Thumbnail Double Click',
message: message:
'The selected display sets could not be added to the viewport due to a mismatch in the Hanging Protocol rules.', 'The selected display sets could not be added to the viewport due to a mismatch in the Hanging Protocol rules.',
@ -103,10 +103,10 @@ function PanelStudyBrowser({
// // ~~ Initial Thumbnails // // ~~ Initial Thumbnails
useEffect(() => { useEffect(() => {
const currentDisplaySets = DisplaySetService.activeDisplaySets; const currentDisplaySets = displaySetService.activeDisplaySets;
currentDisplaySets.forEach(async dSet => { currentDisplaySets.forEach(async dSet => {
const newImageSrcEntry = {}; const newImageSrcEntry = {};
const displaySet = DisplaySetService.getDisplaySetByUID( const displaySet = displaySetService.getDisplaySetByUID(
dSet.displaySetInstanceUID dSet.displaySetInstanceUID
); );
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
@ -134,11 +134,12 @@ function PanelStudyBrowser({
// ~~ displaySets // ~~ displaySets
useEffect(() => { useEffect(() => {
// TODO: Are we sure `activeDisplaySets` will always be accurate? // TODO: Are we sure `activeDisplaySets` will always be accurate?
const currentDisplaySets = DisplaySetService.activeDisplaySets; const currentDisplaySets = displaySetService.activeDisplaySets;
const mappedDisplaySets = _mapDisplaySets( const mappedDisplaySets = _mapDisplaySets(
currentDisplaySets, currentDisplaySets,
thumbnailImageSrcMap thumbnailImageSrcMap
); );
sortStudyInstances(mappedDisplaySets);
setDisplaySets(mappedDisplaySets); setDisplaySets(mappedDisplaySets);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -147,13 +148,13 @@ function PanelStudyBrowser({
// ~~ subscriptions --> displaySets // ~~ subscriptions --> displaySets
useEffect(() => { useEffect(() => {
// DISPLAY_SETS_ADDED returns an array of DisplaySets that were added // DISPLAY_SETS_ADDED returns an array of DisplaySets that were added
const SubscriptionDisplaySetsAdded = DisplaySetService.subscribe( const SubscriptionDisplaySetsAdded = displaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED, displaySetService.EVENTS.DISPLAY_SETS_ADDED,
data => { data => {
const { displaySetsAdded } = data; const { displaySetsAdded } = data;
displaySetsAdded.forEach(async dSet => { displaySetsAdded.forEach(async dSet => {
const newImageSrcEntry = {}; const newImageSrcEntry = {};
const displaySet = DisplaySetService.getDisplaySetByUID( const displaySet = displaySetService.getDisplaySetByUID(
dSet.displaySetInstanceUID dSet.displaySetInstanceUID
); );
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
@ -178,8 +179,8 @@ function PanelStudyBrowser({
// TODO: Will this always hold _all_ the displaySets we care about? // TODO: Will this always hold _all_ the displaySets we care about?
// DISPLAY_SETS_CHANGED returns `DisplaySerService.activeDisplaySets` // DISPLAY_SETS_CHANGED returns `DisplaySerService.activeDisplaySets`
const SubscriptionDisplaySetsChanged = DisplaySetService.subscribe( const SubscriptionDisplaySetsChanged = displaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_CHANGED, displaySetService.EVENTS.DISPLAY_SETS_CHANGED,
changedDisplaySets => { changedDisplaySets => {
const mappedDisplaySets = _mapDisplaySets( const mappedDisplaySets = _mapDisplaySets(
changedDisplaySets, changedDisplaySets,
@ -221,7 +222,7 @@ function PanelStudyBrowser({
if (!shouldCollapseStudy) { if (!shouldCollapseStudy) {
const madeInClient = true; const madeInClient = true;
requestDisplaySetCreationForStudy( requestDisplaySetCreationForStudy(
DisplaySetService, displaySetService,
StudyInstanceUID, StudyInstanceUID,
madeInClient madeInClient
); );
@ -300,6 +301,7 @@ function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
seriesNumber: ds.SeriesNumber, seriesNumber: ds.SeriesNumber,
modality: ds.Modality, modality: ds.Modality,
seriesDate: ds.SeriesDate, seriesDate: ds.SeriesDate,
seriesTime: ds.SeriesTime,
numInstances: ds.numImageFrames, numInstances: ds.numImageFrames,
StudyInstanceUID: ds.StudyInstanceUID, StudyInstanceUID: ds.StudyInstanceUID,
componentType, componentType,

View File

@ -0,0 +1,143 @@
/* eslint-disable react/display-name */
import React from 'react';
import { Dialog, Input, Select } from '@ohif/ui';
export const CREATE_REPORT_DIALOG_RESPONSE = {
CANCEL: 0,
CREATE_REPORT: 1,
};
export default function createReportDialogPrompt(
uiDialogService,
{ extensionManager }
) {
return new Promise(function (resolve, reject) {
let dialogId = undefined;
const _handleClose = () => {
// Dismiss dialog
uiDialogService.dismiss({ id: dialogId });
// Notify of cancel action
resolve({
action: CREATE_REPORT_DIALOG_RESPONSE.CANCEL,
value: undefined,
dataSourceName: undefined,
});
};
/**
*
* @param {string} param0.action - value of action performed
* @param {string} param0.value - value from input field
*/
const _handleFormSubmit = ({ action, value }) => {
uiDialogService.dismiss({ id: dialogId });
switch (action.id) {
case 'save':
resolve({
action: CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT,
value: value.label,
dataSourceName: value.dataSourceName,
});
break;
case 'cancel':
resolve({
action: CREATE_REPORT_DIALOG_RESPONSE.CANCEL,
value: undefined,
dataSourceName: undefined,
});
break;
}
};
const dataSourcesOpts = Object.keys(extensionManager.dataSourceMap)
.filter(ds => {
const configuration =
extensionManager.dataSourceDefs[ds]?.configuration;
const supportsStow =
configuration?.supportsStow ?? configuration?.wadoRoot;
return supportsStow;
})
.map(ds => {
return {
value: ds,
label: ds,
placeHolder: ds,
};
});
dialogId = uiDialogService.create({
centralize: true,
isDraggable: false,
content: Dialog,
useLastPosition: false,
showOverlay: true,
contentProps: {
title: 'Provide a name for your report',
value: {
label: '',
dataSourceName: extensionManager.activeDataSource,
},
noCloseButton: true,
onClose: _handleClose,
actions: [
{ id: 'cancel', text: 'Cancel', type: 'primary' },
{ id: 'save', text: 'Save', type: 'secondary' },
],
// TODO: Should be on button press...
onSubmit: _handleFormSubmit,
body: ({ value, setValue }) => {
const onChangeHandler = event => {
event.persist();
setValue(value => ({ ...value, label: event.target.value }));
};
const onKeyPressHandler = event => {
if (event.key === 'Enter') {
uiDialogService.dismiss({ id: dialogId });
resolve({
action: CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT,
value: value.label,
});
}
};
return (
<>
<div className="p-4 bg-primary-dark">
{dataSourcesOpts.length > 1 && (
<Select
closeMenuOnSelect={true}
className="mr-2 bg-black border-primary-main"
options={dataSourcesOpts}
placeholder={
dataSourcesOpts.find(
option => option.value === value.dataSourceName
).placeHolder
}
value={value.dataSourceName}
onChange={evt => {
setValue(v => ({ ...v, dataSourceName: evt.value }));
}}
isClearable={false}
/>
)}
</div>
<div className="p-4 bg-primary-dark">
<Input
autoFocus
className="mt-2 bg-black border-primary-main"
type="text"
placeholder="Enter Report Name"
containerClassName="mr-2"
value={value.label}
onChange={onChangeHandler}
onKeyPress={onKeyPressHandler}
required
/>
</div>
</>
);
},
},
});
});
}

View File

@ -1,12 +1,12 @@
function requestDisplaySetCreationForStudy( function requestDisplaySetCreationForStudy(
dataSource, dataSource,
DisplaySetService, displaySetService,
StudyInstanceUID, StudyInstanceUID,
madeInClient madeInClient
) { ) {
// TODO: is this already short-circuited by the map of Retrieve promises? // TODO: is this already short-circuited by the map of Retrieve promises?
if ( if (
DisplaySetService.activeDisplaySets.some( displaySetService.activeDisplaySets.some(
displaySet => displaySet.StudyInstanceUID === StudyInstanceUID displaySet => displaySet.StudyInstanceUID === StudyInstanceUID
) )
) { ) {

View File

@ -1,7 +1,8 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import classnames from 'classnames';
export default function Toolbar({ servicesManager }) { export default function Toolbar({ servicesManager }) {
const { ToolBarService } = servicesManager.services; const { toolbarService } = servicesManager.services;
const [toolbarButtons, setToolbarButtons] = useState([]); const [toolbarButtons, setToolbarButtons] = useState([]);
const [buttonState, setButtonState] = useState({ const [buttonState, setButtonState] = useState({
primaryToolId: '', primaryToolId: '',
@ -11,20 +12,20 @@ export default function Toolbar({ servicesManager }) {
// Could track buttons and state separately...? // Could track buttons and state separately...?
useEffect(() => { useEffect(() => {
const { unsubscribe: unsub1 } = ToolBarService.subscribe( const { unsubscribe: unsub1 } = toolbarService.subscribe(
ToolBarService.EVENTS.TOOL_BAR_MODIFIED, toolbarService.EVENTS.TOOL_BAR_MODIFIED,
() => setToolbarButtons(ToolBarService.getButtonSection('primary')) () => setToolbarButtons(toolbarService.getButtonSection('primary'))
); );
const { unsubscribe: unsub2 } = ToolBarService.subscribe( const { unsubscribe: unsub2 } = toolbarService.subscribe(
ToolBarService.EVENTS.TOOL_BAR_STATE_MODIFIED, toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
() => setButtonState({ ...ToolBarService.state }) () => setButtonState({ ...toolbarService.state })
); );
return () => { return () => {
unsub1(); unsub1();
unsub2(); unsub2();
}; };
}, [ToolBarService]); }, [toolbarService]);
return ( return (
<> <>
@ -46,15 +47,18 @@ export default function Toolbar({ servicesManager }) {
// These can... Trigger toolbar events based on updates? // These can... Trigger toolbar events based on updates?
// Then sync using useEffect, or simply modify the state here? // Then sync using useEffect, or simply modify the state here?
return ( return (
<Component // The margin for separating the tools on the toolbar should go here and NOT in each individual component (button) item.
key={id} // This allows for the individual items to be included in other UI components where perhaps alternative margins are desired.
id={id} <div key={id} className={classnames('mr-1')}>
{...componentProps} <Component
bState={buttonState} id={id}
isActive={isActive} {...componentProps}
onInteraction={args => ToolBarService.recordInteraction(args)} bState={buttonState}
servicesManager={servicesManager} isActive={isActive}
/> onInteraction={args => toolbarService.recordInteraction(args)}
servicesManager={servicesManager}
/>
</div>
); );
})} })}
</> </>

View File

@ -6,12 +6,23 @@ import {
useViewportGrid, useViewportGrid,
} from '@ohif/ui'; } from '@ohif/ui';
function LayoutSelector({ rows, columns, servicesManager }) { import { ServicesManager } from '@ohif/core';
function LayoutSelector({
rows,
columns,
className,
servicesManager,
...rest
}) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [disableSelector, setDisableSelector] = useState(false); const [disableSelector, setDisableSelector] = useState(false);
const [viewportGridState, viewportGridService] = useViewportGrid(); const [viewportGridState, viewportGridService] = useViewportGrid();
const { HangingProtocolService } = servicesManager.services; const {
hangingProtocolService,
toolbarService,
} = (servicesManager as ServicesManager).services;
const closeOnOutsideClick = () => { const closeOnOutsideClick = () => {
if (isOpen) { if (isOpen) {
@ -20,23 +31,17 @@ function LayoutSelector({ rows, columns, servicesManager }) {
}; };
useEffect(() => { useEffect(() => {
const { unsubscribe } = HangingProtocolService.subscribe( const { unsubscribe } = hangingProtocolService.subscribe(
HangingProtocolService.EVENTS.PROTOCOL_CHANGED, hangingProtocolService.EVENTS.PROTOCOL_CHANGED,
evt => { evt => {
const { protocol } = evt; const { protocol } = evt;
if (protocol.id === 'mpr') {
setDisableSelector(true);
} else {
setDisableSelector(false);
}
} }
); );
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}, [HangingProtocolService]); }, [hangingProtocolService]);
useEffect(() => { useEffect(() => {
window.addEventListener('click', closeOnOutsideClick); window.addEventListener('click', closeOnOutsideClick);
@ -55,20 +60,50 @@ function LayoutSelector({ rows, columns, servicesManager }) {
const onInteractionHandler = () => setIsOpen(!isOpen); const onInteractionHandler = () => setIsOpen(!isOpen);
const DropdownContent = isOpen ? OHIFLayoutSelector : null; const DropdownContent = isOpen ? OHIFLayoutSelector : null;
const onSelectionHandler = ({ numRows, numCols }) => {
// TODO Introduce a service to persist the state of the current hanging protocol/app.
// TODO Here the layout change will amount to a change of hanging protocol as specified by the extension for this layout selector tool
// followed by the change of the grid itself.
if (hangingProtocolService.getActiveProtocol().protocol.id === 'mpr') {
toolbarService.recordInteraction({
groupId: 'MPR',
itemId: 'MPR',
interactionType: 'toggle',
commands: [
{
commandName: 'toggleMPR',
commandOptions: {},
context: 'CORNERSTONE',
},
],
});
}
// When a new layout is selected, keep any extra/offscreen viewports
// so that if any of those viewports were populated via the UI then they
// will be maintained in case those viewports are redisplayed later.
viewportGridService.setLayout({
numRows,
numCols,
keepExtraViewports: true,
});
};
return ( return (
<ToolbarButton <ToolbarButton
id="Layout" id="Layout"
label="Grid Layout" label="Grid Layout"
icon="tool-layout" icon="tool-layout"
onInteraction={onInteractionHandler} onInteraction={onInteractionHandler}
className={className}
rounded={rest.rounded}
dropdownContent={ dropdownContent={
DropdownContent !== null && ( DropdownContent !== null && (
<DropdownContent <DropdownContent
rows={rows} rows={rows}
columns={columns} columns={columns}
onSelection={({ numRows, numCols }) => { onSelection={onSelectionHandler}
viewportGridService.setLayout({ numRows, numCols });
}}
/> />
) )
} }
@ -82,6 +117,7 @@ LayoutSelector.propTypes = {
rows: PropTypes.number, rows: PropTypes.number,
columns: PropTypes.number, columns: PropTypes.number,
onLayoutChange: PropTypes.func, onLayoutChange: PropTypes.func,
servicesManager: PropTypes.instanceOf(ServicesManager),
}; };
LayoutSelector.defaultProps = { LayoutSelector.defaultProps = {

Some files were not shown because too many files have changed in this diff Show More