Merge remote-tracking branch 'upstream/v3-stable' into v3-stable
This commit is contained in:
commit
00099c0136
@ -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
|
||||||
|
|||||||
@ -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`,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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' },
|
||||||
|
|||||||
@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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');
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
@ -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 };
|
||||||
|
|||||||
@ -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: {},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
49
extensions/cornerstone-dicom-sr/src/init.ts
Normal file
49
extensions/cornerstone-dicom-sr/src/init.ts
Normal 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: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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: '',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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 =
|
||||||
|
|||||||
12
extensions/cornerstone-dicom-sr/src/utils/addToolInstance.ts
Normal file
12
extensions/cornerstone-dicom-sr/src/utils/addToolInstance.ts
Normal 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);
|
||||||
|
}
|
||||||
@ -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++) {
|
||||||
|
|||||||
@ -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
|
||||||
*
|
*
|
||||||
@ -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];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
98
extensions/cornerstone/src/Viewport/Overlays/utils.ts
Normal file
98
extensions/cornerstone/src/Viewport/Overlays/utils.ts
Normal 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';
|
||||||
|
}
|
||||||
@ -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: [],
|
||||||
|
|||||||
@ -8,6 +8,7 @@ const mpr = {
|
|||||||
availableTo: {},
|
availableTo: {},
|
||||||
editableBy: {},
|
editableBy: {},
|
||||||
protocolMatchingRules: [],
|
protocolMatchingRules: [],
|
||||||
|
imageLoadStrategy: 'nth',
|
||||||
displaySetSelectors: {
|
displaySetSelectors: {
|
||||||
mprDisplaySet: {
|
mprDisplaySet: {
|
||||||
seriesMatchingRules: [
|
seriesMatchingRules: [
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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}`;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 });
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
134
extensions/cornerstone/src/tools/CalibrationLineTool.ts
Normal file
134
extensions/cornerstone/src/tools/CalibrationLineTool.ts
Normal 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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -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) {
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
extensions/cornerstone/src/utils/getNthFrames.js
Normal file
44
extensions/cornerstone/src/utils/getNthFrames.js
Normal 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;
|
||||||
|
}
|
||||||
26
extensions/cornerstone/src/utils/interleave.js
Normal file
26
extensions/cornerstone/src/utils/interleave.js
Normal 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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -1 +1,11 @@
|
|||||||
export default ['Length', 'EllipticalROI', 'Bidirectional', 'ArrowAnnotate'];
|
export default [
|
||||||
|
'Length',
|
||||||
|
'EllipticalROI',
|
||||||
|
'Bidirectional',
|
||||||
|
'ArrowAnnotate',
|
||||||
|
'Angle',
|
||||||
|
'CobbAngle',
|
||||||
|
'Probe',
|
||||||
|
'RectangleROI',
|
||||||
|
'PlanarFreehandROI',
|
||||||
|
];
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
import * as measurementMappingUtils from './utils';
|
||||||
|
|
||||||
|
export { measurementMappingUtils };
|
||||||
@ -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;
|
||||||
@ -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();
|
||||||
|
|||||||
@ -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,
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
};
|
||||||
@ -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 };
|
||||||
|
|||||||
121
extensions/cornerstone/src/utils/nthLoader.ts
Normal file
121
extensions/cornerstone/src/utils/nthLoader.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
73
extensions/default/src/Actions/createReportAsync.tsx
Normal file
73
extensions/default/src/Actions/createReportAsync.tsx
Normal 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;
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
56
extensions/default/src/Panels/DataSourceSelector.tsx
Normal file
56
extensions/default/src/Panels/DataSourceSelector.tsx
Normal 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;
|
||||||
@ -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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
143
extensions/default/src/Panels/createReportDialogPrompt.tsx
Normal file
143
extensions/default/src/Panels/createReportDialogPrompt.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -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
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -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
Loading…
Reference in New Issue
Block a user