commit
5b7dc9e9c3
@ -2,7 +2,8 @@
|
|||||||
<!-- markdownlint-disable -->
|
<!-- markdownlint-disable -->
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<h1>OHIF Medical Imaging Viewer</h1>
|
<h1>OHIF Medical Imaging Viewer</h1>
|
||||||
<p><strong>The OHIF Viewer</strong> is a zero-footprint medical image viewer provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF)</a>. It is a configurable and extensible progressive web application with out-of-the-box support for image archives which support <a href="https://www.dicomstandard.org/dicomweb/">DICOMweb</a>.</p>
|
<p><strong>The OHIF Viewer</strong> is a zero-footprint medical image viewer
|
||||||
|
provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF)</a>. It is a configurable and extensible progressive web application with out-of-the-box support for image archives which support <a href="https://www.dicomstandard.org/dicomweb/">DICOMweb</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -31,8 +31,6 @@ async function _hydrateSEGDisplaySet({
|
|||||||
displaySetInstanceUID
|
displaySetInstanceUID
|
||||||
);
|
);
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
// check every viewport in the viewports to see if the displaySetInstanceUID
|
// check every viewport in the viewports to see if the displaySetInstanceUID
|
||||||
@ -50,7 +48,7 @@ async function _hydrateSEGDisplaySet({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (shouldDisplaySeg) {
|
if (shouldDisplaySeg) {
|
||||||
viewportGridService.setDisplaySetsForViewport({
|
updatedViewports.push({
|
||||||
viewportIndex: index,
|
viewportIndex: index,
|
||||||
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
|
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
|
||||||
viewportOptions: {
|
viewportOptions: {
|
||||||
@ -62,6 +60,9 @@ async function _hydrateSEGDisplaySet({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Do the entire update at once
|
||||||
|
viewportGridService.setDisplaySetsForViewports(updatedViewports);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -45,8 +45,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"@cornerstonejs/adapters": "^0.4.1",
|
"@cornerstonejs/adapters": "^0.6.0",
|
||||||
"@cornerstonejs/core": "^0.33.2",
|
"@cornerstonejs/core": "^0.40.0",
|
||||||
"@cornerstonejs/tools": "^0.50.2"
|
"@cornerstonejs/tools": "^0.60.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,7 +35,7 @@ function getFilteredCornerstoneToolState(
|
|||||||
);
|
);
|
||||||
const toolData = imageIdSpecificToolState[toolType].data;
|
const toolData = imageIdSpecificToolState[toolType].data;
|
||||||
|
|
||||||
let finding;
|
let { finding } = measurementDataI;
|
||||||
const findingSites = [];
|
const findingSites = [];
|
||||||
|
|
||||||
// NOTE -> We use the CORNERSTONEJS coding schemeDesignator which we have
|
// NOTE -> We use the CORNERSTONEJS coding schemeDesignator which we have
|
||||||
@ -56,6 +56,10 @@ function getFilteredCornerstoneToolState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (measurementDataI.findingSites) {
|
||||||
|
findingSites.push(...measurementDataI.findingSites);
|
||||||
|
}
|
||||||
|
|
||||||
const measurement = Object.assign({}, annotation, {
|
const measurement = Object.assign({}, annotation, {
|
||||||
finding,
|
finding,
|
||||||
findingSites,
|
findingSites,
|
||||||
@ -73,7 +77,7 @@ function getFilteredCornerstoneToolState(
|
|||||||
for (let i = 0; i < framesOfReference.length; i++) {
|
for (let i = 0; i < framesOfReference.length; i++) {
|
||||||
const frameOfReference = framesOfReference[i];
|
const frameOfReference = framesOfReference[i];
|
||||||
|
|
||||||
const frameOfReferenceAnnotations = annotationManager.getFrameOfReferenceAnnotations(
|
const frameOfReferenceAnnotations = annotationManager.getAnnotations(
|
||||||
frameOfReference
|
frameOfReference
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,25 @@ const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
|||||||
|
|
||||||
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
||||||
|
|
||||||
|
const convertCode = (codingValues, code) => {
|
||||||
|
if (!code || code.CodingSchemeDesignator === 'CORNERSTONEJS') return;
|
||||||
|
const ref = `${code.CodingSchemeDesignator}:${code.CodeValue}`;
|
||||||
|
const ret = { ...codingValues[ref], ref, ...code, text: code.CodeMeaning };
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertSites = (codingValues, sites) => {
|
||||||
|
if (!sites || !sites.length) return;
|
||||||
|
const ret = [];
|
||||||
|
// Do as a loop to convert away from Proxy instances
|
||||||
|
for (let i = 0; i < sites.length; i++) {
|
||||||
|
// Deal with irregular conversion from dcmjs
|
||||||
|
const site = convertCode(codingValues, sites[i][0] || sites[i]);
|
||||||
|
if (site) ret.push(site);
|
||||||
|
}
|
||||||
|
return (ret.length && ret) || undefined;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hydrates a structured report, for default viewports.
|
* Hydrates a structured report, for default viewports.
|
||||||
*
|
*
|
||||||
@ -20,8 +39,16 @@ export default function hydrateStructuredReport(
|
|||||||
displaySetInstanceUID
|
displaySetInstanceUID
|
||||||
) {
|
) {
|
||||||
const dataSource = extensionManager.getActiveDataSource()[0];
|
const dataSource = extensionManager.getActiveDataSource()[0];
|
||||||
const { measurementService, displaySetService } = servicesManager.services;
|
const {
|
||||||
|
measurementService,
|
||||||
|
displaySetService,
|
||||||
|
customizationService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
|
const codingValues = customizationService.getCustomization(
|
||||||
|
'codingValues',
|
||||||
|
{}
|
||||||
|
);
|
||||||
const displaySet = displaySetService.getDisplaySetByUID(
|
const displaySet = displaySetService.getDisplaySetByUID(
|
||||||
displaySetInstanceUID
|
displaySetInstanceUID
|
||||||
);
|
);
|
||||||
@ -171,6 +198,15 @@ export default function hydrateStructuredReport(
|
|||||||
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
|
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
|
||||||
);
|
);
|
||||||
annotation.data.label = getLabelFromDCMJSImportedToolData(toolData);
|
annotation.data.label = getLabelFromDCMJSImportedToolData(toolData);
|
||||||
|
annotation.data.finding = convertCode(
|
||||||
|
codingValues,
|
||||||
|
toolData.finding?.[0]
|
||||||
|
);
|
||||||
|
annotation.data.findingSites = convertSites(
|
||||||
|
codingValues,
|
||||||
|
toolData.findingSites
|
||||||
|
);
|
||||||
|
annotation.data.site = annotation.data.findingSites?.[0];
|
||||||
|
|
||||||
const matchingMapping = mappings.find(
|
const matchingMapping = mappings.find(
|
||||||
m => m.annotationType === annotationType
|
m => m.annotationType === annotationType
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
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 OHIF, { utils, ServicesManager, ExtensionManager } from '@ohif/core';
|
||||||
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -27,6 +27,7 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
dataSource,
|
dataSource,
|
||||||
displaySets,
|
displaySets,
|
||||||
viewportIndex,
|
viewportIndex,
|
||||||
|
viewportOptions,
|
||||||
viewportLabel,
|
viewportLabel,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
@ -215,6 +216,7 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
// override the activeImageDisplaySetData
|
// override the activeImageDisplaySetData
|
||||||
displaySets={[activeImageDisplaySetData]}
|
displaySets={[activeImageDisplaySetData]}
|
||||||
viewportOptions={{
|
viewportOptions={{
|
||||||
|
...viewportOptions,
|
||||||
toolGroupId: `${SR_TOOLGROUP_BASE_NAME}`,
|
toolGroupId: `${SR_TOOLGROUP_BASE_NAME}`,
|
||||||
}}
|
}}
|
||||||
onElementEnabled={onElementEnabled}
|
onElementEnabled={onElementEnabled}
|
||||||
@ -419,6 +421,9 @@ OHIFCornerstoneSRViewport.propTypes = {
|
|||||||
dataSource: PropTypes.object,
|
dataSource: PropTypes.object,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
customProps: PropTypes.object,
|
customProps: PropTypes.object,
|
||||||
|
viewportOptions: PropTypes.object,
|
||||||
|
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
|
||||||
|
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
OHIFCornerstoneSRViewport.defaultProps = {
|
OHIFCornerstoneSRViewport.defaultProps = {
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@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.13.0",
|
||||||
"dcmjs": "^0.29.4",
|
"dcmjs": "^0.29.4",
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
@ -43,10 +43,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^0.4.1",
|
"@cornerstonejs/adapters": "^0.6.0",
|
||||||
"@cornerstonejs/core": "^0.33.2",
|
"@cornerstonejs/core": "^0.40.0",
|
||||||
"@cornerstonejs/streaming-image-volume-loader": "^0.14.1",
|
"@cornerstonejs/streaming-image-volume-loader": "^0.16.0",
|
||||||
"@cornerstonejs/tools": "^0.50.2",
|
"@cornerstonejs/tools": "^0.60.1",
|
||||||
"@kitware/vtk.js": "26.5.6",
|
"@kitware/vtk.js": "26.5.6",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
|
|||||||
@ -128,6 +128,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
cornerstoneViewportService,
|
cornerstoneViewportService,
|
||||||
cornerstoneCacheService,
|
cornerstoneCacheService,
|
||||||
viewportGridService,
|
viewportGridService,
|
||||||
|
stateSyncService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
const cineHandler = () => {
|
const cineHandler = () => {
|
||||||
@ -211,6 +212,33 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
}
|
}
|
||||||
}, [elementRef]);
|
}, [elementRef]);
|
||||||
|
|
||||||
|
const storePresentation = () => {
|
||||||
|
const currentPresentation = cornerstoneViewportService.getPresentation(
|
||||||
|
viewportIndex
|
||||||
|
);
|
||||||
|
if (!currentPresentation || !currentPresentation.presentationIds) return;
|
||||||
|
const {
|
||||||
|
lutPresentationStore,
|
||||||
|
positionPresentationStore,
|
||||||
|
} = stateSyncService.getState();
|
||||||
|
const { presentationIds } = currentPresentation;
|
||||||
|
const { lutPresentationId, positionPresentationId } = presentationIds || {};
|
||||||
|
const storeState = {};
|
||||||
|
if (lutPresentationId) {
|
||||||
|
storeState.lutPresentationStore = {
|
||||||
|
...lutPresentationStore,
|
||||||
|
[lutPresentationId]: currentPresentation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (positionPresentationId) {
|
||||||
|
storeState.positionPresentationStore = {
|
||||||
|
...positionPresentationStore,
|
||||||
|
[positionPresentationId]: currentPresentation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
stateSyncService.store(storeState);
|
||||||
|
};
|
||||||
|
|
||||||
const cleanUpServices = useCallback(() => {
|
const cleanUpServices = useCallback(() => {
|
||||||
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
|
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
|
||||||
viewportIndex
|
viewportIndex
|
||||||
@ -288,6 +316,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
setImageScrollBarHeight();
|
setImageScrollBarHeight();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
storePresentation();
|
||||||
|
|
||||||
cleanUpServices();
|
cleanUpServices();
|
||||||
|
|
||||||
cornerstoneViewportService.disableElement(viewportIndex);
|
cornerstoneViewportService.disableElement(viewportIndex);
|
||||||
@ -360,11 +390,26 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
initialImageIndex
|
initialImageIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
|
storePresentation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
lutPresentationStore,
|
||||||
|
positionPresentationStore,
|
||||||
|
} = stateSyncService.getState();
|
||||||
|
const { presentationIds } = viewportOptions;
|
||||||
|
const presentations = {
|
||||||
|
positionPresentation:
|
||||||
|
positionPresentationStore[presentationIds?.positionPresentationId],
|
||||||
|
lutPresentation:
|
||||||
|
lutPresentationStore[presentationIds?.lutPresentationId],
|
||||||
|
};
|
||||||
|
|
||||||
cornerstoneViewportService.setViewportData(
|
cornerstoneViewportService.setViewportData(
|
||||||
viewportIndex,
|
viewportIndex,
|
||||||
viewportData,
|
viewportData,
|
||||||
viewportOptions,
|
viewportOptions,
|
||||||
displaySetOptions
|
displaySetOptions,
|
||||||
|
presentations
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -117,8 +117,11 @@ function CustomizableViewportOverlay({
|
|||||||
viewportIndex,
|
viewportIndex,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
}) {
|
}) {
|
||||||
const { toolbarService, cornerstoneViewportService, customizationService } =
|
const {
|
||||||
servicesManager.services;
|
toolbarService,
|
||||||
|
cornerstoneViewportService,
|
||||||
|
customizationService,
|
||||||
|
} = 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);
|
||||||
const [activeTools, setActiveTools] = useState([]);
|
const [activeTools, setActiveTools] = useState([]);
|
||||||
@ -202,10 +205,9 @@ function CustomizableViewportOverlay({
|
|||||||
previousCamera.parallelScale !== camera.parallelScale ||
|
previousCamera.parallelScale !== camera.parallelScale ||
|
||||||
previousCamera.scale !== camera.scale
|
previousCamera.scale !== camera.scale
|
||||||
) {
|
) {
|
||||||
const viewport =
|
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||||
cornerstoneViewportService.getCornerstoneViewportByIndex(
|
viewportIndex
|
||||||
viewportIndex
|
);
|
||||||
);
|
|
||||||
|
|
||||||
if (!viewport) {
|
if (!viewport) {
|
||||||
return;
|
return;
|
||||||
@ -283,7 +285,7 @@ function CustomizableViewportOverlay({
|
|||||||
} else if (item.customizationType === 'ohif.overlayItem.instanceNumber') {
|
} else if (item.customizationType === 'ohif.overlayItem.instanceNumber') {
|
||||||
return <InstanceNumberOverlayItem {...overlayItemProps} />;
|
return <InstanceNumberOverlayItem {...overlayItemProps} />;
|
||||||
} else {
|
} else {
|
||||||
const renderItem = customizationService.applyType(item);
|
const renderItem = customizationService.transform(item);
|
||||||
|
|
||||||
if (typeof renderItem.content === 'function') {
|
if (typeof renderItem.content === 'function') {
|
||||||
return renderItem.content(overlayItemProps);
|
return renderItem.content(overlayItemProps);
|
||||||
@ -450,8 +452,9 @@ function _getInstanceNumberFromVolume(
|
|||||||
const volume = volumes[0];
|
const volume = volumes[0];
|
||||||
const { direction, imageIds } = volume;
|
const { direction, imageIds } = volume;
|
||||||
|
|
||||||
const cornerstoneViewport =
|
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||||
cornerstoneViewportService.getCornerstoneViewportByIndex(viewportIndex);
|
viewportIndex
|
||||||
|
);
|
||||||
|
|
||||||
if (!cornerstoneViewport) {
|
if (!cornerstoneViewport) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -118,6 +118,10 @@ function ViewportOrientationMarkers({
|
|||||||
viewportIndex
|
viewportIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!ohifViewport) {
|
||||||
|
console.log('ViewportOrientationMarkers::No viewport');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const backgroundColor = ohifViewport.getViewportOptions().background;
|
const backgroundColor = ohifViewport.getViewportOptions().background;
|
||||||
|
|
||||||
// Todo: probably this can be done in a better way in which we identify bright
|
// Todo: probably this can be done in a better way in which we identify bright
|
||||||
|
|||||||
@ -15,11 +15,11 @@ import { ServicesManager } from '@ohif/core';
|
|||||||
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 toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
|
import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
|
||||||
|
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
|
||||||
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
||||||
|
|
||||||
const commandsModule = ({ servicesManager }) => {
|
function commandsModule({ servicesManager, commandsManager }) {
|
||||||
const {
|
const {
|
||||||
viewportGridService,
|
viewportGridService,
|
||||||
toolGroupService,
|
toolGroupService,
|
||||||
@ -27,10 +27,14 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
toolbarService,
|
toolbarService,
|
||||||
uiDialogService,
|
uiDialogService,
|
||||||
cornerstoneViewportService,
|
cornerstoneViewportService,
|
||||||
hangingProtocolService,
|
|
||||||
uiNotificationService,
|
uiNotificationService,
|
||||||
|
customizationService,
|
||||||
|
measurementService,
|
||||||
|
hangingProtocolService,
|
||||||
} = (servicesManager as ServicesManager).services;
|
} = (servicesManager as ServicesManager).services;
|
||||||
|
|
||||||
|
const { measurementServiceSource } = this;
|
||||||
|
|
||||||
function _getActiveViewportEnabledElement() {
|
function _getActiveViewportEnabledElement() {
|
||||||
return getActiveViewportEnabledElement(viewportGridService);
|
return getActiveViewportEnabledElement(viewportGridService);
|
||||||
}
|
}
|
||||||
@ -70,9 +74,165 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
getActiveViewportEnabledElement: () => {
|
/**
|
||||||
return _getActiveViewportEnabledElement();
|
* Generates the selector props for the context menu, specific to
|
||||||
|
* the cornerstone viewport, and then runs the context menu.
|
||||||
|
*/
|
||||||
|
showCornerstoneContextMenu: options => {
|
||||||
|
const element = _getActiveViewportEnabledElement()?.viewport?.element;
|
||||||
|
|
||||||
|
const optionsToUse = { ...options, element };
|
||||||
|
const { useSelectedAnnotation, nearbyToolData, event } = optionsToUse;
|
||||||
|
|
||||||
|
// This code is used to invoke the context menu via keyboard shortcuts
|
||||||
|
if (useSelectedAnnotation && !nearbyToolData) {
|
||||||
|
const firstAnnotationSelected = getFirstAnnotationSelected(element);
|
||||||
|
// filter by allowed selected tools from config property (if there is any)
|
||||||
|
const isToolAllowed =
|
||||||
|
!optionsToUse.allowedSelectedTools ||
|
||||||
|
optionsToUse.allowedSelectedTools.includes(
|
||||||
|
firstAnnotationSelected?.metadata?.toolName
|
||||||
|
);
|
||||||
|
if (isToolAllowed) {
|
||||||
|
optionsToUse.nearbyToolData = firstAnnotationSelected;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
optionsToUse.defaultPointsPosition = [];
|
||||||
|
// if (optionsToUse.nearbyToolData) {
|
||||||
|
// optionsToUse.defaultPointsPosition = commandsManager.runCommand(
|
||||||
|
// 'getToolDataActiveCanvasPoints',
|
||||||
|
// { toolData: optionsToUse.nearbyToolData }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// TODO - make the selectorProps richer by including the study metadata and display set.
|
||||||
|
optionsToUse.selectorProps = {
|
||||||
|
toolName: optionsToUse.nearbyToolData?.metadata?.toolName,
|
||||||
|
value: optionsToUse.nearbyToolData,
|
||||||
|
uid: optionsToUse.nearbyToolData?.annotationUID,
|
||||||
|
nearbyToolData: optionsToUse.nearbyToolData,
|
||||||
|
event,
|
||||||
|
...optionsToUse.selectorProps,
|
||||||
|
};
|
||||||
|
|
||||||
|
commandsManager.run(options, optionsToUse);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getNearbyToolData({ nearbyToolData, element, canvasCoordinates }) {
|
||||||
|
return (
|
||||||
|
nearbyToolData ??
|
||||||
|
cstUtils.getAnnotationNearPoint(element, canvasCoordinates)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Measurement tool commands:
|
||||||
|
|
||||||
|
/** Delete the given measurement */
|
||||||
|
deleteMeasurement: ({ uid }) => {
|
||||||
|
if (uid) {
|
||||||
|
measurementServiceSource.remove(uid);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the measurement labelling input dialog and update the label
|
||||||
|
* on the measurement with a response if not cancelled.
|
||||||
|
*/
|
||||||
|
setMeasurementLabel: ({ uid }) => {
|
||||||
|
const measurement = measurementService.getMeasurement(uid);
|
||||||
|
|
||||||
|
callInputDialog(
|
||||||
|
uiDialogService,
|
||||||
|
measurement,
|
||||||
|
(label, actionId) => {
|
||||||
|
if (actionId === 'cancel') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedMeasurement = Object.assign({}, measurement, {
|
||||||
|
label,
|
||||||
|
});
|
||||||
|
|
||||||
|
measurementService.update(
|
||||||
|
updatedMeasurement.uid,
|
||||||
|
updatedMeasurement,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param props - containing the updates to apply
|
||||||
|
* @param props.measurementKey - chooses the measurement key to apply the
|
||||||
|
* code to. This will typically be finding or site to apply a
|
||||||
|
* finind code or a findingSites code.
|
||||||
|
* @param props.code - A coding scheme value from DICOM, including:
|
||||||
|
* * CodeValue - the language independent code, for example '1234'
|
||||||
|
* * CodingSchemeDesignator - the issue of the code value
|
||||||
|
* * CodeMeaning - the text value shown to the user
|
||||||
|
* * ref - a string reference in the form `<designator>:<codeValue>`
|
||||||
|
* * Other fields
|
||||||
|
* Note it is a valid option to remove the finding or site values by
|
||||||
|
* supplying null for the code.
|
||||||
|
* @param props.uid - the measurement UID to find it with
|
||||||
|
* @param props.label - the text value for the code. Has NOTHING to do with
|
||||||
|
* the measurement label, which can be set with textLabel
|
||||||
|
* @param props.textLabel is the measurement label to apply. Set to null to
|
||||||
|
* delete.
|
||||||
|
*
|
||||||
|
* If the measurementKey is `site`, then the code will also be added/replace
|
||||||
|
* the 0 element of findingSites. This behaviour is expected to be enhanced
|
||||||
|
* in the future with ability to set other site information.
|
||||||
|
*/
|
||||||
|
updateMeasurement: props => {
|
||||||
|
const { code, uid, textLabel, label } = props;
|
||||||
|
const measurement = measurementService.getMeasurement(uid);
|
||||||
|
const updatedMeasurement = {
|
||||||
|
...measurement,
|
||||||
|
};
|
||||||
|
// Call it textLabel as the label value
|
||||||
|
// TODO - remove the label setting when direct rendering of findingSites is enabled
|
||||||
|
if (textLabel !== undefined) {
|
||||||
|
updatedMeasurement.label = textLabel;
|
||||||
|
}
|
||||||
|
if (code !== undefined) {
|
||||||
|
const measurementKey = code.type || 'finding';
|
||||||
|
|
||||||
|
if (code.ref && !code.CodeValue) {
|
||||||
|
const split = code.ref.indexOf(':');
|
||||||
|
code.CodeValue = code.ref.substring(split + 1);
|
||||||
|
code.CodeMeaning = code.text || label;
|
||||||
|
code.CodingSchemeDesignator = code.ref.substring(0, split);
|
||||||
|
}
|
||||||
|
updatedMeasurement[measurementKey] = code;
|
||||||
|
// TODO - remove this line once the measurements table customizations are in
|
||||||
|
if (measurementKey !== 'finding') {
|
||||||
|
if (updatedMeasurement.findingSites) {
|
||||||
|
updatedMeasurement.findingSites = updatedMeasurement.findingSites.filter(
|
||||||
|
it => it.type !== measurementKey
|
||||||
|
);
|
||||||
|
updatedMeasurement.findingSites.push(code);
|
||||||
|
} else {
|
||||||
|
updatedMeasurement.findingSites = [code];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
measurementService.update(
|
||||||
|
updatedMeasurement.uid,
|
||||||
|
updatedMeasurement,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Retrieve value commands
|
||||||
|
getActiveViewportEnabledElement: _getActiveViewportEnabledElement,
|
||||||
|
|
||||||
setViewportActive: ({ viewportId }) => {
|
setViewportActive: ({ viewportId }) => {
|
||||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(
|
const viewportInfo = cornerstoneViewportService.getViewportInfo(
|
||||||
viewportId
|
viewportId
|
||||||
@ -128,6 +288,14 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
});
|
});
|
||||||
viewport.render();
|
viewport.render();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Just call the toolbar service record interaction - allows
|
||||||
|
// executing a toolbar command as a full toolbar command with side affects
|
||||||
|
// coming from the ToolbarService itself.
|
||||||
|
toolbarServiceRecordInteraction: props => {
|
||||||
|
toolbarService.recordInteraction(props);
|
||||||
|
},
|
||||||
|
|
||||||
setToolActive: ({ toolName, toolGroupId = null }) => {
|
setToolActive: ({ toolName, toolGroupId = null }) => {
|
||||||
if (toolName === 'Crosshairs') {
|
if (toolName === 'Crosshairs') {
|
||||||
const activeViewportToolGroup = _getToolGroup(null);
|
const activeViewportToolGroup = _getToolGroup(null);
|
||||||
@ -150,7 +318,7 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toolGroup = _getToolGroup(toolGroupId);
|
const toolGroup = _getToolGroup(toolGroupId);
|
||||||
const toolGroupViewportIds = toolGroup.getViewportIds();
|
const toolGroupViewportIds = toolGroup?.getViewportIds?.();
|
||||||
|
|
||||||
// if toolGroup has been destroyed, or its viewports have been removed
|
// if toolGroup has been destroyed, or its viewports have been removed
|
||||||
if (!toolGroupViewportIds || !toolGroupViewportIds.length) {
|
if (!toolGroupViewportIds || !toolGroupViewportIds.length) {
|
||||||
@ -171,6 +339,17 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!toolGroup.getToolInstance(toolName)) {
|
||||||
|
uiNotificationService.show({
|
||||||
|
title: `${toolName} tool`,
|
||||||
|
message: `The ${toolName} tool is not available in this viewport.`,
|
||||||
|
type: 'info',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new Error(`ToolGroup ${toolGroup.id} does not have this tool.`);
|
||||||
|
}
|
||||||
|
|
||||||
const activeToolName = toolGroup.getActivePrimaryMouseButtonTool();
|
const activeToolName = toolGroup.getActivePrimaryMouseButtonTool();
|
||||||
|
|
||||||
if (activeToolName) {
|
if (activeToolName) {
|
||||||
@ -404,16 +583,6 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
(activeViewportIndex - 1 + viewports.length) % viewports.length;
|
(activeViewportIndex - 1 + viewports.length) % viewports.length;
|
||||||
viewportGridService.setActiveViewportIndex(nextViewportIndex);
|
viewportGridService.setActiveViewportIndex(nextViewportIndex);
|
||||||
},
|
},
|
||||||
setHangingProtocol: ({ protocolId }) => {
|
|
||||||
hangingProtocolService.setProtocol(protocolId);
|
|
||||||
},
|
|
||||||
toggleMPR: ({ toggledState }) => {
|
|
||||||
toggleMPRHangingProtocol({
|
|
||||||
toggledState,
|
|
||||||
servicesManager,
|
|
||||||
getToolGroup: _getToolGroup,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
toggleStackImageSync: ({ toggledState }) => {
|
toggleStackImageSync: ({ toggledState }) => {
|
||||||
toggleStackImageSync({
|
toggleStackImageSync({
|
||||||
getEnabledElement,
|
getEnabledElement,
|
||||||
@ -446,11 +615,53 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const definitions = {
|
const definitions = {
|
||||||
|
// The command here is to show the viewer context menu, as being the
|
||||||
|
// context menu
|
||||||
|
showCornerstoneContextMenu: {
|
||||||
|
commandFn: actions.showCornerstoneContextMenu,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {
|
||||||
|
menuCustomizationId: 'measurementsContextMenu',
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
commandName: 'showContextMenu',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
getNearbyToolData: {
|
||||||
|
commandFn: actions.getNearbyToolData,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteMeasurement: {
|
||||||
|
commandFn: actions.deleteMeasurement,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
setMeasurementLabel: {
|
||||||
|
commandFn: actions.setMeasurementLabel,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
updateMeasurement: {
|
||||||
|
commandFn: actions.updateMeasurement,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
|
||||||
setWindowLevel: {
|
setWindowLevel: {
|
||||||
commandFn: actions.setWindowLevel,
|
commandFn: actions.setWindowLevel,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
|
toolbarServiceRecordInteraction: {
|
||||||
|
commandFn: actions.toolbarServiceRecordInteraction,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
setToolActive: {
|
setToolActive: {
|
||||||
commandFn: actions.setToolActive,
|
commandFn: actions.setToolActive,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
@ -554,16 +765,6 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
setHangingProtocol: {
|
|
||||||
commandFn: actions.setHangingProtocol,
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
toggleMPR: {
|
|
||||||
commandFn: actions.toggleMPR,
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
toggleStackImageSync: {
|
toggleStackImageSync: {
|
||||||
commandFn: actions.toggleStackImageSync,
|
commandFn: actions.toggleStackImageSync,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
@ -581,6 +782,6 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
definitions,
|
definitions,
|
||||||
defaultContext: 'CORNERSTONE',
|
defaultContext: 'CORNERSTONE',
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
export default commandsModule;
|
export default commandsModule;
|
||||||
|
|||||||
@ -1,16 +1,46 @@
|
|||||||
const mpr = {
|
import { Types } from '@ohif/core';
|
||||||
id: 'mpr',
|
|
||||||
|
const mpr: Types.HangingProtocol.Protocol = {
|
||||||
locked: true,
|
locked: true,
|
||||||
hasUpdatedPriorsInformation: false,
|
hasUpdatedPriorsInformation: false,
|
||||||
name: 'mpr',
|
name: 'mpr',
|
||||||
createdDate: '2021-02-23T19:22:08.894Z',
|
createdDate: '2021-02-23T19:22:08.894Z',
|
||||||
modifiedDate: '2022-10-04T19:22:08.894Z',
|
modifiedDate: '2023-02-17',
|
||||||
availableTo: {},
|
availableTo: {},
|
||||||
editableBy: {},
|
editableBy: {},
|
||||||
|
// Unknown number of priors referenced - so just match any study
|
||||||
|
numberOfPriorsReferenced: 0,
|
||||||
protocolMatchingRules: [],
|
protocolMatchingRules: [],
|
||||||
imageLoadStrategy: 'nth',
|
imageLoadStrategy: 'nth',
|
||||||
|
callbacks: {
|
||||||
|
// Switches out of MPR mode when the layout change button is used
|
||||||
|
onLayoutChange: [
|
||||||
|
{
|
||||||
|
commandName: 'toggleHangingProtocol',
|
||||||
|
commandOptions: { protocolId: 'mpr' },
|
||||||
|
context: 'DEFAULT',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Turns off crosshairs when switching out of MPR mode
|
||||||
|
onProtocolExit: [
|
||||||
|
{
|
||||||
|
commandName: 'toolbarServiceRecordInteraction',
|
||||||
|
commandOptions: {
|
||||||
|
interactionType: 'tool',
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
commandOptions: {
|
||||||
|
toolName: 'WindowLevel',
|
||||||
|
},
|
||||||
|
context: 'CORNERSTONE',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
displaySetSelectors: {
|
displaySetSelectors: {
|
||||||
mprDisplaySet: {
|
activeDisplaySet: {
|
||||||
seriesMatchingRules: [
|
seriesMatchingRules: [
|
||||||
{
|
{
|
||||||
weight: 1,
|
weight: 1,
|
||||||
@ -27,8 +57,7 @@ const mpr = {
|
|||||||
},
|
},
|
||||||
stages: [
|
stages: [
|
||||||
{
|
{
|
||||||
id: 'mpr3Stage',
|
name: 'MPR 1x3',
|
||||||
name: 'mpr',
|
|
||||||
viewportStructure: {
|
viewportStructure: {
|
||||||
layoutType: 'grid',
|
layoutType: 'grid',
|
||||||
properties: {
|
properties: {
|
||||||
@ -74,6 +103,169 @@ const mpr = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'activeDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'mpr',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'sagittal',
|
||||||
|
initialImageOptions: {
|
||||||
|
preset: 'middle',
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'mpr',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'activeDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'mpr',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'coronal',
|
||||||
|
initialImageOptions: {
|
||||||
|
preset: 'middle',
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'mpr',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'activeDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mprAnd3DVolumeViewport = {
|
||||||
|
id: 'mprAnd3DVolumeViewport',
|
||||||
|
locked: true,
|
||||||
|
hasUpdatedPriorsInformation: false,
|
||||||
|
name: 'mpr',
|
||||||
|
createdDate: '2023-03-15T10:29:44.894Z',
|
||||||
|
modifiedDate: '2023-03-15T10:29:44.894Z',
|
||||||
|
availableTo: {},
|
||||||
|
editableBy: {},
|
||||||
|
protocolMatchingRules: [],
|
||||||
|
imageLoadStrategy: 'interleaveCenter',
|
||||||
|
displaySetSelectors: {
|
||||||
|
mprDisplaySet: {
|
||||||
|
seriesMatchingRules: [
|
||||||
|
{
|
||||||
|
weight: 1,
|
||||||
|
attribute: 'isReconstructable',
|
||||||
|
constraint: {
|
||||||
|
equals: {
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attribute: 'Modality',
|
||||||
|
constraint: {
|
||||||
|
equals: {
|
||||||
|
value: 'CT',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
id: 'mpr3Stage',
|
||||||
|
name: 'mpr',
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 2,
|
||||||
|
columns: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'mpr',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'axial',
|
||||||
|
initialImageOptions: {
|
||||||
|
preset: 'middle',
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'mpr',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'mprDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'volume3d',
|
||||||
|
viewportType: 'volume3d',
|
||||||
|
orientation: 'coronal',
|
||||||
|
customViewportProps: {
|
||||||
|
hideOverlays: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'mprDisplaySet',
|
||||||
|
options: {
|
||||||
|
presetName: 'CT-Bone',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'mpr',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'coronal',
|
||||||
|
initialImageOptions: {
|
||||||
|
preset: 'middle',
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'mpr',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
id: 'mprDisplaySet',
|
id: 'mprDisplaySet',
|
||||||
@ -103,29 +295,6 @@ const mpr = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
viewportOptions: {
|
|
||||||
toolGroupId: 'mpr',
|
|
||||||
viewportType: 'volume',
|
|
||||||
orientation: 'coronal',
|
|
||||||
initialImageOptions: {
|
|
||||||
preset: 'middle',
|
|
||||||
},
|
|
||||||
syncGroups: [
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'mpr',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
displaySets: [
|
|
||||||
{
|
|
||||||
id: 'mprDisplaySet',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -134,9 +303,13 @@ const mpr = {
|
|||||||
function getHangingProtocolModule() {
|
function getHangingProtocolModule() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'mpr',
|
name: 'mpr',
|
||||||
protocol: mpr,
|
protocol: mpr,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: mprAnd3DVolumeViewport.id,
|
||||||
|
protocol: mprAnd3DVolumeViewport,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
|||||||
import { Types } from '@ohif/core';
|
import { Types } from '@ohif/core';
|
||||||
|
|
||||||
import init from './init';
|
import init from './init';
|
||||||
import commandsModule from './commandsModule';
|
import getCommandsModule from './commandsModule';
|
||||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||||
import ToolGroupService from './services/ToolGroupService';
|
import ToolGroupService from './services/ToolGroupService';
|
||||||
import SyncGroupService from './services/SyncGroupService';
|
import SyncGroupService from './services/SyncGroupService';
|
||||||
@ -26,6 +26,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';
|
import { measurementMappingUtils } from './utils/measurementServiceMappings';
|
||||||
|
import { PublicViewportOptions } from './services/ViewportService/Viewport';
|
||||||
|
|
||||||
const Component = React.lazy(() => {
|
const Component = React.lazy(() => {
|
||||||
return import(
|
return import(
|
||||||
@ -50,7 +51,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
*/
|
*/
|
||||||
id,
|
id,
|
||||||
|
|
||||||
onModeExit: () => {
|
onModeExit: (): void => {
|
||||||
// Empty out the image load and retrieval pools to prevent memory leaks
|
// Empty out the image load and retrieval pools to prevent memory leaks
|
||||||
// on the mode exits
|
// on the mode exits
|
||||||
Object.values(cs3DEnums.RequestType).forEach(type => {
|
Object.values(cs3DEnums.RequestType).forEach(type => {
|
||||||
@ -67,12 +68,10 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
*
|
*
|
||||||
* @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools`
|
* @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools`
|
||||||
*/
|
*/
|
||||||
async preRegistration({
|
preRegistration: function (
|
||||||
servicesManager,
|
props: Types.Extensions.ExtensionParams
|
||||||
commandsManager,
|
): Promise<void> {
|
||||||
configuration = {},
|
const { servicesManager } = props;
|
||||||
appConfig,
|
|
||||||
}) {
|
|
||||||
// Todo: we should be consistent with how services get registered. Use REGISTRATION static method for all
|
// Todo: we should be consistent with how services get registered. Use REGISTRATION static method for all
|
||||||
servicesManager.registerService(
|
servicesManager.registerService(
|
||||||
CornerstoneViewportService(servicesManager)
|
CornerstoneViewportService(servicesManager)
|
||||||
@ -86,20 +85,21 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
CornerstoneCacheService.REGISTRATION(servicesManager)
|
CornerstoneCacheService.REGISTRATION(servicesManager)
|
||||||
);
|
);
|
||||||
|
|
||||||
await init({ servicesManager, commandsManager, configuration, appConfig });
|
return init.call(this, props);
|
||||||
},
|
},
|
||||||
|
|
||||||
getHangingProtocolModule,
|
getHangingProtocolModule,
|
||||||
getViewportModule({ servicesManager, commandsManager }) {
|
getViewportModule({ servicesManager, commandsManager }) {
|
||||||
const ExtendedOHIFCornerstoneViewport = props => {
|
const ExtendedOHIFCornerstoneViewport = props => {
|
||||||
// const onNewImageHandler = jumpData => {
|
// const onNewImageHandler = jumpData => {
|
||||||
// commandsManager.runCommand('jumpToImage', jumpData);
|
// commandsManager.runCommand('jumpToImage', jumpData);
|
||||||
// };
|
// };
|
||||||
const { ToolbarService } = servicesManager.services;
|
const { toolbarService } = (servicesManager as ServicesManager).services;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OHIFCornerstoneViewport
|
<OHIFCornerstoneViewport
|
||||||
{...props}
|
{...props}
|
||||||
ToolbarService={ToolbarService}
|
ToolbarService={toolbarService}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
/>
|
/>
|
||||||
@ -113,13 +113,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
getCommandsModule({ servicesManager, commandsManager, extensionManager }) {
|
getCommandsModule,
|
||||||
return commandsModule({
|
|
||||||
servicesManager,
|
|
||||||
commandsManager,
|
|
||||||
extensionManager,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getUtilityModule({ servicesManager }) {
|
getUtilityModule({ servicesManager }) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -150,5 +144,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default cornerstoneExtension;
|
export type { PublicViewportOptions };
|
||||||
export { measurementMappingUtils };
|
export { measurementMappingUtils };
|
||||||
|
export default cornerstoneExtension;
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ContextMenuMeasurements } from '@ohif/ui';
|
|
||||||
|
|
||||||
import * as cornerstone from '@cornerstonejs/core';
|
import * as cornerstone from '@cornerstonejs/core';
|
||||||
import * as cornerstoneTools from '@cornerstonejs/tools';
|
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||||
@ -21,15 +20,12 @@ import initWADOImageLoader from './initWADOImageLoader';
|
|||||||
import initCornerstoneTools from './initCornerstoneTools';
|
import initCornerstoneTools from './initCornerstoneTools';
|
||||||
|
|
||||||
import { connectToolsToMeasurementService } from './initMeasurementService';
|
import { connectToolsToMeasurementService } from './initMeasurementService';
|
||||||
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 nthLoader from './utils/nthLoader';
|
||||||
import interleaveTopToBottom from './utils/interleaveTopToBottom';
|
import interleaveTopToBottom from './utils/interleaveTopToBottom';
|
||||||
|
import initContextMenu from './initContextMenu';
|
||||||
const cs3DToolsEvents = Enums.Events;
|
import initDoubleClick from './initDoubleClick';
|
||||||
|
|
||||||
let CONTEXT_MENU_OPEN = false;
|
|
||||||
|
|
||||||
// TODO: Cypress tests are currently grabbing this from the window?
|
// TODO: Cypress tests are currently grabbing this from the window?
|
||||||
window.cornerstone = cornerstone;
|
window.cornerstone = cornerstone;
|
||||||
@ -42,11 +38,19 @@ export default async function init({
|
|||||||
commandsManager,
|
commandsManager,
|
||||||
configuration,
|
configuration,
|
||||||
appConfig,
|
appConfig,
|
||||||
}) {
|
}: Types.Extensions.ExtensionParams): Promise<void> {
|
||||||
await cs3DInit();
|
await cs3DInit();
|
||||||
|
|
||||||
// For debugging e2e tests that are failing on CI
|
// For debugging e2e tests that are failing on CI
|
||||||
cornerstone.setUseCPURendering(Boolean(appConfig.useCPURendering));
|
cornerstone.setUseCPURendering(Boolean(appConfig.useCPURendering));
|
||||||
|
cornerstone.setConfiguration({
|
||||||
|
...cornerstone.getConfiguration(),
|
||||||
|
rendering: {
|
||||||
|
...cornerstone.getConfiguration().rendering,
|
||||||
|
strictZSpacingForVolumeViewport:
|
||||||
|
appConfig.strictZSpacingForVolumeViewport,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// For debugging large datasets
|
// For debugging large datasets
|
||||||
const MAX_CACHE_SIZE_1GB = 1073741824;
|
const MAX_CACHE_SIZE_1GB = 1073741824;
|
||||||
@ -65,6 +69,7 @@ export default async function init({
|
|||||||
const {
|
const {
|
||||||
userAuthenticationService,
|
userAuthenticationService,
|
||||||
measurementService,
|
measurementService,
|
||||||
|
customizationService,
|
||||||
displaySetService,
|
displaySetService,
|
||||||
uiDialogService,
|
uiDialogService,
|
||||||
uiModalService,
|
uiModalService,
|
||||||
@ -74,6 +79,7 @@ export default async function init({
|
|||||||
hangingProtocolService,
|
hangingProtocolService,
|
||||||
toolGroupService,
|
toolGroupService,
|
||||||
viewportGridService,
|
viewportGridService,
|
||||||
|
stateSyncService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
window.services = servicesManager.services;
|
window.services = servicesManager.services;
|
||||||
@ -97,6 +103,22 @@ export default async function init({
|
|||||||
_showCPURenderingModal(uiModalService, hangingProtocolService);
|
_showCPURenderingModal(uiModalService, hangingProtocolService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stores a map from `lutPresentationId` to a Presentation object so that
|
||||||
|
// an OHIFCornerstoneViewport can be redisplayed with the same LUT
|
||||||
|
stateSyncService.register('lutPresentationStore', { clearOnModeExit: true });
|
||||||
|
|
||||||
|
// Stores a map from `positionPresentationId` to a Presentation object so that
|
||||||
|
// an OHIFCornerstoneViewport can be redisplayed with the same position
|
||||||
|
stateSyncService.register('positionPresentationStore', {
|
||||||
|
clearOnModeExit: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stores the entire ViewportGridService getState when toggling to one up
|
||||||
|
// (e.g. via a double click) so that it can be restored when toggling back.
|
||||||
|
stateSyncService.register('toggleOneUpViewportGridStore', {
|
||||||
|
clearOnModeExit: true,
|
||||||
|
});
|
||||||
|
|
||||||
const labelmapRepresentation =
|
const labelmapRepresentation =
|
||||||
cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;
|
cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;
|
||||||
|
|
||||||
@ -144,118 +166,12 @@ export default async function init({
|
|||||||
initWADOImageLoader(userAuthenticationService, appConfig);
|
initWADOImageLoader(userAuthenticationService, appConfig);
|
||||||
|
|
||||||
/* Measurement Service */
|
/* Measurement Service */
|
||||||
const measurementServiceSource = connectToolsToMeasurementService(
|
this.measurementServiceSource = connectToolsToMeasurementService(
|
||||||
servicesManager
|
servicesManager
|
||||||
);
|
);
|
||||||
|
|
||||||
initCineService(cineService);
|
initCineService(cineService);
|
||||||
|
|
||||||
const _getDefaultPosition = event => ({
|
|
||||||
x: (event && event.currentPoints.client[0]) || 0,
|
|
||||||
y: (event && event.currentPoints.client[1]) || 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onRightClick = event => {
|
|
||||||
if (!uiDialogService) {
|
|
||||||
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onGetMenuItems = defaultMenuItems => {
|
|
||||||
const { element, currentPoints } = event.detail;
|
|
||||||
|
|
||||||
const nearbyToolData = utilities.getAnnotationNearPoint(
|
|
||||||
element,
|
|
||||||
currentPoints.canvas
|
|
||||||
);
|
|
||||||
|
|
||||||
const menuItems = [];
|
|
||||||
if (nearbyToolData && nearbyToolData.metadata.toolName !== 'Crosshairs') {
|
|
||||||
defaultMenuItems.forEach(item => {
|
|
||||||
item.value = nearbyToolData;
|
|
||||||
item.element = element;
|
|
||||||
menuItems.push(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return menuItems;
|
|
||||||
};
|
|
||||||
|
|
||||||
CONTEXT_MENU_OPEN = true;
|
|
||||||
|
|
||||||
uiDialogService.dismiss({ id: 'context-menu' });
|
|
||||||
uiDialogService.create({
|
|
||||||
id: 'context-menu',
|
|
||||||
isDraggable: false,
|
|
||||||
preservePosition: false,
|
|
||||||
defaultPosition: _getDefaultPosition(event.detail),
|
|
||||||
content: ContextMenuMeasurements,
|
|
||||||
onClickOutside: () => {
|
|
||||||
uiDialogService.dismiss({ id: 'context-menu' });
|
|
||||||
CONTEXT_MENU_OPEN = false;
|
|
||||||
},
|
|
||||||
contentProps: {
|
|
||||||
onGetMenuItems,
|
|
||||||
eventData: event.detail,
|
|
||||||
onDelete: item => {
|
|
||||||
const { annotationUID } = item.value;
|
|
||||||
|
|
||||||
const uid = annotationUID;
|
|
||||||
// Sync'd w/ Measurement Service
|
|
||||||
if (uid) {
|
|
||||||
measurementServiceSource.remove(uid, {
|
|
||||||
element: item.element,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
CONTEXT_MENU_OPEN = false;
|
|
||||||
},
|
|
||||||
onClose: () => {
|
|
||||||
CONTEXT_MENU_OPEN = false;
|
|
||||||
uiDialogService.dismiss({ id: 'context-menu' });
|
|
||||||
},
|
|
||||||
onSetLabel: item => {
|
|
||||||
const { annotationUID } = item.value;
|
|
||||||
|
|
||||||
const measurement = measurementService.getMeasurement(annotationUID);
|
|
||||||
|
|
||||||
callInputDialog(
|
|
||||||
uiDialogService,
|
|
||||||
measurement,
|
|
||||||
(label, actionId) => {
|
|
||||||
if (actionId === 'cancel') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedMeasurement = Object.assign({}, measurement, {
|
|
||||||
label,
|
|
||||||
});
|
|
||||||
|
|
||||||
measurementService.update(
|
|
||||||
updatedMeasurement.uid,
|
|
||||||
updatedMeasurement,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
CONTEXT_MENU_OPEN = false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetContextMenu = () => {
|
|
||||||
if (!uiDialogService) {
|
|
||||||
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CONTEXT_MENU_OPEN = false;
|
|
||||||
|
|
||||||
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,
|
||||||
@ -266,32 +182,41 @@ export default async function init({
|
|||||||
viewportId
|
viewportId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ohifViewport = cornerstoneViewportService.getViewportInfo(
|
||||||
|
viewportId
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
lutPresentationStore,
|
||||||
|
positionPresentationStore,
|
||||||
|
} = stateSyncService.getState();
|
||||||
|
const { presentationIds } = ohifViewport.getViewportOptions();
|
||||||
|
const presentations = {
|
||||||
|
positionPresentation:
|
||||||
|
positionPresentationStore[presentationIds?.positionPresentationId],
|
||||||
|
lutPresentation:
|
||||||
|
lutPresentationStore[presentationIds?.lutPresentationId],
|
||||||
|
};
|
||||||
|
|
||||||
cornerstoneViewportService.setVolumesForViewport(
|
cornerstoneViewportService.setVolumesForViewport(
|
||||||
viewport,
|
viewport,
|
||||||
volumeInputArray
|
volumeInputArray,
|
||||||
|
presentations
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/*
|
initContextMenu({
|
||||||
* Because click gives us the native "mouse up", buttons will always be `0`
|
cornerstoneViewportService,
|
||||||
* Need to fallback to event.which;
|
customizationService,
|
||||||
*
|
commandsManager,
|
||||||
*/
|
});
|
||||||
const contextMenuHandleClick = evt => {
|
|
||||||
const mouseUpEvent = evt.detail.event;
|
|
||||||
const isRightClick = mouseUpEvent.which === 3;
|
|
||||||
|
|
||||||
const clickMethodHandler = isRightClick ? onRightClick : resetContextMenu;
|
initDoubleClick({
|
||||||
clickMethodHandler(evt);
|
customizationService,
|
||||||
};
|
commandsManager,
|
||||||
|
});
|
||||||
// const cancelContextMenuIfOpen = evt => {
|
|
||||||
// if (CONTEXT_MENU_OPEN) {
|
|
||||||
// resetContextMenu();
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
const newStackCallback = evt => {
|
const newStackCallback = evt => {
|
||||||
const { element } = evt.detail;
|
const { element } = evt.detail;
|
||||||
@ -326,12 +251,6 @@ export default async function init({
|
|||||||
|
|
||||||
function elementEnabledHandler(evt) {
|
function elementEnabledHandler(evt) {
|
||||||
const { element } = evt.detail;
|
const { element } = evt.detail;
|
||||||
|
|
||||||
element.addEventListener(
|
|
||||||
cs3DToolsEvents.MOUSE_CLICK,
|
|
||||||
contextMenuHandleClick
|
|
||||||
);
|
|
||||||
|
|
||||||
element.addEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
|
element.addEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
|
||||||
|
|
||||||
eventTarget.addEventListener(
|
eventTarget.addEventListener(
|
||||||
@ -343,11 +262,6 @@ export default async function init({
|
|||||||
function elementDisabledHandler(evt) {
|
function elementDisabledHandler(evt) {
|
||||||
const { element } = evt.detail;
|
const { element } = evt.detail;
|
||||||
|
|
||||||
element.removeEventListener(
|
|
||||||
cs3DToolsEvents.MOUSE_CLICK,
|
|
||||||
contextMenuHandleClick
|
|
||||||
);
|
|
||||||
|
|
||||||
element.removeEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
|
element.removeEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
|
||||||
|
|
||||||
// TODO - consider removing the callback when all elements are gone
|
// TODO - consider removing the callback when all elements are gone
|
||||||
@ -369,8 +283,8 @@ export default async function init({
|
|||||||
|
|
||||||
viewportGridService.subscribe(
|
viewportGridService.subscribe(
|
||||||
viewportGridService.EVENTS.ACTIVE_VIEWPORT_INDEX_CHANGED,
|
viewportGridService.EVENTS.ACTIVE_VIEWPORT_INDEX_CHANGED,
|
||||||
({ viewportIndex }) => {
|
({ viewportIndex, viewportId }) => {
|
||||||
const viewportId = `viewport-${viewportIndex}`;
|
viewportId = viewportId || `viewport-${viewportIndex}`;
|
||||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||||
|
|
||||||
if (!toolGroup || !toolGroup._toolInstances?.['ReferenceLines']) {
|
if (!toolGroup || !toolGroup._toolInstances?.['ReferenceLines']) {
|
||||||
@ -427,9 +341,9 @@ function _showCPURenderingModal(uiModalService, hangingProtocolService) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { unsubscribe } = hangingProtocolService.subscribe(
|
const { unsubscribe } = hangingProtocolService.subscribe(
|
||||||
hangingProtocolService.EVENTS.HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT,
|
hangingProtocolService.EVENTS.PROTOCOL_CHANGED,
|
||||||
({ progress }) => {
|
() => {
|
||||||
const done = callback(progress);
|
const done = callback(100);
|
||||||
|
|
||||||
if (done) {
|
if (done) {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
|
|||||||
107
extensions/cornerstone/src/initContextMenu.ts
Normal file
107
extensions/cornerstone/src/initContextMenu.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { eventTarget, EVENTS } from '@cornerstonejs/core';
|
||||||
|
import { Enums } from '@cornerstonejs/tools';
|
||||||
|
import { setEnabledElement } from './state';
|
||||||
|
import { findNearbyToolData } from './utils/findNearbyToolData';
|
||||||
|
|
||||||
|
const cs3DToolsEvents = Enums.Events;
|
||||||
|
|
||||||
|
const DEFAULT_CONTEXT_MENU_CLICKS = {
|
||||||
|
button1: {
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
commandName: 'closeContextMenu',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
button3: {
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
commandName: 'showCornerstoneContextMenu',
|
||||||
|
commandOptions: {
|
||||||
|
menuId: 'measurementsContextMenu',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a name, consisting of:
|
||||||
|
* * alt when the alt key is down
|
||||||
|
* * ctrl when the cctrl key is down
|
||||||
|
* * shift when the shift key is down
|
||||||
|
* * 'button' followed by the button number (1 left, 3 right etc)
|
||||||
|
*/
|
||||||
|
function getEventName(evt) {
|
||||||
|
const button = evt.detail.event.which;
|
||||||
|
const nameArr = [];
|
||||||
|
if (evt.detail.event.altKey) nameArr.push('alt');
|
||||||
|
if (evt.detail.event.ctrlKey) nameArr.push('ctrl');
|
||||||
|
if (evt.detail.event.shiftKey) nameArr.push('shift');
|
||||||
|
nameArr.push('button');
|
||||||
|
nameArr.push(button);
|
||||||
|
return nameArr.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function initContextMenu({
|
||||||
|
cornerstoneViewportService,
|
||||||
|
customizationService,
|
||||||
|
commandsManager,
|
||||||
|
}): void {
|
||||||
|
/*
|
||||||
|
* Run the commands associated with the given button press,
|
||||||
|
* defaults on button1 and button2
|
||||||
|
*/
|
||||||
|
const cornerstoneViewportHandleEvent = (name, evt) => {
|
||||||
|
const customizations =
|
||||||
|
customizationService.get('cornerstoneViewportClickCommands') ||
|
||||||
|
DEFAULT_CONTEXT_MENU_CLICKS;
|
||||||
|
const toRun = customizations[name];
|
||||||
|
console.log('initContextMenu::cornerstoneViewportHandleEvent', name, toRun);
|
||||||
|
const options = {
|
||||||
|
nearbyToolData: findNearbyToolData(commandsManager, evt),
|
||||||
|
event: evt,
|
||||||
|
};
|
||||||
|
commandsManager.run(toRun, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cornerstoneViewportHandleClick = evt => {
|
||||||
|
const name = getEventName(evt);
|
||||||
|
cornerstoneViewportHandleEvent(name, evt);
|
||||||
|
};
|
||||||
|
|
||||||
|
function elementEnabledHandler(evt) {
|
||||||
|
const { viewportId, element } = evt.detail;
|
||||||
|
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||||
|
if (!viewportInfo) return;
|
||||||
|
const viewportIndex = viewportInfo.getViewportIndex();
|
||||||
|
// TODO check update upstream
|
||||||
|
setEnabledElement(viewportIndex, element);
|
||||||
|
|
||||||
|
element.addEventListener(
|
||||||
|
cs3DToolsEvents.MOUSE_CLICK,
|
||||||
|
cornerstoneViewportHandleClick
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function elementDisabledHandler(evt) {
|
||||||
|
const { element } = evt.detail;
|
||||||
|
|
||||||
|
element.removeEventListener(
|
||||||
|
cs3DToolsEvents.MOUSE_CLICK,
|
||||||
|
cornerstoneViewportHandleClick
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
eventTarget.addEventListener(
|
||||||
|
EVENTS.ELEMENT_ENABLED,
|
||||||
|
elementEnabledHandler.bind(null)
|
||||||
|
);
|
||||||
|
|
||||||
|
eventTarget.addEventListener(
|
||||||
|
EVENTS.ELEMENT_DISABLED,
|
||||||
|
elementDisabledHandler.bind(null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default initContextMenu;
|
||||||
@ -23,6 +23,7 @@ import {
|
|||||||
addTool,
|
addTool,
|
||||||
annotation,
|
annotation,
|
||||||
ReferenceLinesTool,
|
ReferenceLinesTool,
|
||||||
|
TrackballRotateTool,
|
||||||
} from '@cornerstonejs/tools';
|
} from '@cornerstonejs/tools';
|
||||||
|
|
||||||
import CalibrationLineTool from './tools/CalibrationLineTool';
|
import CalibrationLineTool from './tools/CalibrationLineTool';
|
||||||
@ -51,6 +52,7 @@ export default function initCornerstoneTools(configuration = {}) {
|
|||||||
addTool(SegmentationDisplayTool);
|
addTool(SegmentationDisplayTool);
|
||||||
addTool(ReferenceLinesTool);
|
addTool(ReferenceLinesTool);
|
||||||
addTool(CalibrationLineTool);
|
addTool(CalibrationLineTool);
|
||||||
|
addTool(TrackballRotateTool);
|
||||||
|
|
||||||
// Modify annotation tools to use dashed lines on SR
|
// Modify annotation tools to use dashed lines on SR
|
||||||
const annotationStyle = {
|
const annotationStyle = {
|
||||||
@ -90,6 +92,7 @@ const toolNames = {
|
|||||||
SegmentationDisplay: SegmentationDisplayTool.toolName,
|
SegmentationDisplay: SegmentationDisplayTool.toolName,
|
||||||
ReferenceLines: ReferenceLinesTool.toolName,
|
ReferenceLines: ReferenceLinesTool.toolName,
|
||||||
CalibrationLine: CalibrationLineTool.toolName,
|
CalibrationLine: CalibrationLineTool.toolName,
|
||||||
|
TrackballRotateTool: TrackballRotateTool.toolName,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { toolNames };
|
export { toolNames };
|
||||||
|
|||||||
92
extensions/cornerstone/src/initDoubleClick.ts
Normal file
92
extensions/cornerstone/src/initDoubleClick.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { eventTarget, EVENTS } from '@cornerstonejs/core';
|
||||||
|
import { Enums } from '@cornerstonejs/tools';
|
||||||
|
import { CommandsManager, CustomizationService, Types } from '@ohif/core';
|
||||||
|
import { findNearbyToolData } from './utils/findNearbyToolData';
|
||||||
|
|
||||||
|
const cs3DToolsEvents = Enums.Events;
|
||||||
|
|
||||||
|
const DEFAULT_DOUBLE_CLICK = {
|
||||||
|
doubleClick: {
|
||||||
|
commandName: 'toggleOneUp',
|
||||||
|
commandOptions: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a double click event name, consisting of:
|
||||||
|
* * alt when the alt key is down
|
||||||
|
* * ctrl when the cctrl key is down
|
||||||
|
* * shift when the shift key is down
|
||||||
|
* * 'doubleClick'
|
||||||
|
*/
|
||||||
|
function getDoubleClickEventName(evt: CustomEvent) {
|
||||||
|
const nameArr = [];
|
||||||
|
if (evt.detail.event.altKey) nameArr.push('alt');
|
||||||
|
if (evt.detail.event.ctrlKey) nameArr.push('ctrl');
|
||||||
|
if (evt.detail.event.shiftKey) nameArr.push('shift');
|
||||||
|
nameArr.push('doubleClick');
|
||||||
|
return nameArr.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export type initDoubleClickArgs = {
|
||||||
|
customizationService: CustomizationService;
|
||||||
|
commandsManager: CommandsManager;
|
||||||
|
};
|
||||||
|
|
||||||
|
function initDoubleClick({
|
||||||
|
customizationService,
|
||||||
|
commandsManager,
|
||||||
|
}: initDoubleClickArgs): void {
|
||||||
|
const cornerstoneViewportHandleDoubleClick = (evt: CustomEvent) => {
|
||||||
|
// Do not allow double click on a tool.
|
||||||
|
const nearbyToolData = findNearbyToolData(commandsManager, evt);
|
||||||
|
if (nearbyToolData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventName = getDoubleClickEventName(evt);
|
||||||
|
|
||||||
|
// Allows for the customization of the double click on a viewport.
|
||||||
|
const customizations =
|
||||||
|
customizationService.get('cornerstoneViewportClickCommands') ||
|
||||||
|
DEFAULT_DOUBLE_CLICK;
|
||||||
|
|
||||||
|
const toRun = customizations[eventName];
|
||||||
|
|
||||||
|
if (!toRun) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
commandsManager.run(toRun);
|
||||||
|
};
|
||||||
|
|
||||||
|
function elementEnabledHandler(evt: CustomEvent) {
|
||||||
|
const { element } = evt.detail;
|
||||||
|
|
||||||
|
element.addEventListener(
|
||||||
|
cs3DToolsEvents.MOUSE_DOUBLE_CLICK,
|
||||||
|
cornerstoneViewportHandleDoubleClick
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function elementDisabledHandler(evt: CustomEvent) {
|
||||||
|
const { element } = evt.detail;
|
||||||
|
|
||||||
|
element.removeEventListener(
|
||||||
|
cs3DToolsEvents.MOUSE_DOUBLE_CLICK,
|
||||||
|
cornerstoneViewportHandleDoubleClick
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
eventTarget.addEventListener(
|
||||||
|
EVENTS.ELEMENT_ENABLED,
|
||||||
|
elementEnabledHandler.bind(null)
|
||||||
|
);
|
||||||
|
|
||||||
|
eventTarget.addEventListener(
|
||||||
|
EVENTS.ELEMENT_DISABLED,
|
||||||
|
elementDisabledHandler.bind(null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default initDoubleClick;
|
||||||
@ -62,12 +62,20 @@ class CornerstoneCacheService {
|
|||||||
viewportData = await this._getStackViewportData(
|
viewportData = await this._getStackViewportData(
|
||||||
dataSource,
|
dataSource,
|
||||||
displaySets,
|
displaySets,
|
||||||
initialImageIndex
|
initialImageIndex,
|
||||||
|
cs3DViewportType
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
if (
|
||||||
viewportData = await this._getVolumeViewportData(dataSource, displaySets);
|
cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC ||
|
||||||
|
cs3DViewportType === Enums.ViewportType.VOLUME_3D
|
||||||
|
) {
|
||||||
|
viewportData = await this._getVolumeViewportData(
|
||||||
|
dataSource,
|
||||||
|
displaySets,
|
||||||
|
cs3DViewportType
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
viewportData.viewportType = cs3DViewportType;
|
viewportData.viewportType = cs3DViewportType;
|
||||||
@ -100,7 +108,8 @@ class CornerstoneCacheService {
|
|||||||
|
|
||||||
const newViewportData = await this._getVolumeViewportData(
|
const newViewportData = await this._getVolumeViewportData(
|
||||||
dataSource,
|
dataSource,
|
||||||
displaySets
|
displaySets,
|
||||||
|
viewportData.viewportType
|
||||||
);
|
);
|
||||||
|
|
||||||
return newViewportData;
|
return newViewportData;
|
||||||
@ -109,7 +118,8 @@ class CornerstoneCacheService {
|
|||||||
private _getStackViewportData(
|
private _getStackViewportData(
|
||||||
dataSource,
|
dataSource,
|
||||||
displaySets,
|
displaySets,
|
||||||
initialImageIndex
|
initialImageIndex,
|
||||||
|
viewportType: Enums.ViewportType
|
||||||
): StackViewportData {
|
): StackViewportData {
|
||||||
// For Stack Viewport we don't have fusion currently
|
// For Stack Viewport we don't have fusion currently
|
||||||
const displaySet = displaySets[0];
|
const displaySet = displaySets[0];
|
||||||
@ -126,7 +136,7 @@ class CornerstoneCacheService {
|
|||||||
const { displaySetInstanceUID, StudyInstanceUID } = displaySet;
|
const { displaySetInstanceUID, StudyInstanceUID } = displaySet;
|
||||||
|
|
||||||
const StackViewportData: StackViewportData = {
|
const StackViewportData: StackViewportData = {
|
||||||
viewportType: Enums.ViewportType.STACK,
|
viewportType,
|
||||||
data: {
|
data: {
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
displaySetInstanceUID,
|
displaySetInstanceUID,
|
||||||
@ -143,7 +153,8 @@ class CornerstoneCacheService {
|
|||||||
|
|
||||||
private async _getVolumeViewportData(
|
private async _getVolumeViewportData(
|
||||||
dataSource,
|
dataSource,
|
||||||
displaySets
|
displaySets,
|
||||||
|
viewportType: Enums.ViewportType
|
||||||
): Promise<VolumeViewportData> {
|
): Promise<VolumeViewportData> {
|
||||||
// Todo: Check the cache for multiple scenarios to see if we need to
|
// Todo: Check the cache for multiple scenarios to see if we need to
|
||||||
// decache the volume data from other viewports or not
|
// decache the volume data from other viewports or not
|
||||||
@ -207,7 +218,7 @@ class CornerstoneCacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
viewportType: Enums.ViewportType.ORTHOGRAPHIC,
|
viewportType,
|
||||||
data: volumeData,
|
data: volumeData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,22 +1,22 @@
|
|||||||
import cloneDeep from 'lodash.clonedeep';
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
|
|
||||||
import { pubSubServiceInterface } from '@ohif/core';
|
|
||||||
import {
|
import {
|
||||||
utilities as cstUtils,
|
|
||||||
segmentation as cstSegmentation,
|
|
||||||
CONSTANTS as cstConstants,
|
|
||||||
Enums as csToolsEnums,
|
|
||||||
Types as cstTypes,
|
|
||||||
} from '@cornerstonejs/tools';
|
|
||||||
import {
|
|
||||||
eventTarget,
|
|
||||||
cache,
|
cache,
|
||||||
|
eventTarget,
|
||||||
|
getEnabledElementByIds,
|
||||||
|
metaData,
|
||||||
|
Types,
|
||||||
utilities as csUtils,
|
utilities as csUtils,
|
||||||
volumeLoader,
|
volumeLoader,
|
||||||
Types,
|
|
||||||
metaData,
|
|
||||||
getEnabledElementByIds,
|
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
|
import {
|
||||||
|
CONSTANTS as cstConstants,
|
||||||
|
Enums as csToolsEnums,
|
||||||
|
segmentation as cstSegmentation,
|
||||||
|
Types as cstTypes,
|
||||||
|
utilities as cstUtils,
|
||||||
|
} from '@cornerstonejs/tools';
|
||||||
|
import { pubSubServiceInterface } from '@ohif/core';
|
||||||
import isEqual from 'lodash.isequal';
|
import isEqual from 'lodash.isequal';
|
||||||
import { easeInOutBell } from '../../utils/transitions';
|
import { easeInOutBell } from '../../utils/transitions';
|
||||||
import {
|
import {
|
||||||
@ -202,7 +202,7 @@ class SegmentationService {
|
|||||||
this._setActiveSegment(segmentationId, segmentIndex, suppressEvents);
|
this._setActiveSegment(segmentationId, segmentIndex, suppressEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo: this includes nonhydrated segmentations which might not be
|
// Todo: this includes non-hydrated segmentations which might not be
|
||||||
// persisted in the store
|
// persisted in the store
|
||||||
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||||||
segmentation,
|
segmentation,
|
||||||
@ -1591,13 +1591,20 @@ class SegmentationService {
|
|||||||
|
|
||||||
segmentInfo.isVisible = isVisible;
|
segmentInfo.isVisible = isVisible;
|
||||||
|
|
||||||
cstSegmentation.config.visibility.setVisibilityForSegmentIndex(
|
cstSegmentation.config.visibility.setSegmentVisibility(
|
||||||
toolGroupId,
|
toolGroupId,
|
||||||
segmentationRepresentationUID,
|
segmentationRepresentationUID,
|
||||||
segmentIndex,
|
segmentIndex,
|
||||||
isVisible
|
isVisible
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// make sure to update the isVisible flag on the segmentation
|
||||||
|
// if a segment becomes invisible then the segmentation should be invisible
|
||||||
|
// in the status as well, and show correct icon
|
||||||
|
segmentation.isVisible = segmentation.segments
|
||||||
|
.filter(Boolean)
|
||||||
|
.every(segment => segment.isVisible);
|
||||||
|
|
||||||
if (suppressEvents === false) {
|
if (suppressEvents === false) {
|
||||||
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||||||
segmentation,
|
segmentation,
|
||||||
@ -1917,28 +1924,27 @@ class SegmentationService {
|
|||||||
representation => representation.segmentationId === segmentationId
|
representation => representation.segmentationId === segmentationId
|
||||||
);
|
);
|
||||||
|
|
||||||
const visibility = cstSegmentation.config.visibility.getSegmentationVisibility(
|
const { segmentsHidden } = representation;
|
||||||
toolGroupId,
|
|
||||||
representation.segmentationRepresentationUID
|
const currentVisibility = segmentsHidden.size === 0 ? true : false;
|
||||||
);
|
const newVisibility = !currentVisibility;
|
||||||
|
|
||||||
cstSegmentation.config.visibility.setSegmentationVisibility(
|
cstSegmentation.config.visibility.setSegmentationVisibility(
|
||||||
toolGroupId,
|
toolGroupId,
|
||||||
representation.segmentationRepresentationUID,
|
representation.segmentationRepresentationUID,
|
||||||
!visibility
|
newVisibility
|
||||||
);
|
);
|
||||||
|
|
||||||
// set all segments to visible as well
|
// update segments visibility
|
||||||
const segments = this.getSegmentation(segmentationId).segments;
|
const { segmentation } = this._getSegmentationInfo(
|
||||||
Object.keys(segments).forEach(segmentIndex => {
|
segmentationId,
|
||||||
if (segmentIndex !== '0') {
|
toolGroupId
|
||||||
this._setSegmentVisibility(
|
);
|
||||||
segmentationId,
|
|
||||||
Number(segmentIndex),
|
const segments = segmentation.segments.filter(Boolean);
|
||||||
!visibility,
|
|
||||||
toolGroupId
|
segments.forEach(segment => {
|
||||||
);
|
segment.isVisible = newVisibility;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { pubSubServiceInterface } from '@ohif/core';
|
import { PubSubService } from '@ohif/core';
|
||||||
import {
|
import {
|
||||||
RenderingEngine,
|
RenderingEngine,
|
||||||
StackViewport,
|
StackViewport,
|
||||||
@ -6,7 +6,10 @@ import {
|
|||||||
getRenderingEngine,
|
getRenderingEngine,
|
||||||
utilities as csUtils,
|
utilities as csUtils,
|
||||||
VolumeViewport,
|
VolumeViewport,
|
||||||
|
VolumeViewport3D,
|
||||||
cache,
|
cache,
|
||||||
|
utilities,
|
||||||
|
CONSTANTS,
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
|
|
||||||
import { utilities as csToolsUtils } from '@cornerstonejs/tools';
|
import { utilities as csToolsUtils } from '@cornerstonejs/tools';
|
||||||
@ -21,6 +24,7 @@ import {
|
|||||||
StackViewportData,
|
StackViewportData,
|
||||||
VolumeViewportData,
|
VolumeViewportData,
|
||||||
} from '../../types/CornerstoneCacheService';
|
} from '../../types/CornerstoneCacheService';
|
||||||
|
import { Presentation, Presentations } from '../../types/Presentation';
|
||||||
import {
|
import {
|
||||||
setColormap,
|
setColormap,
|
||||||
setLowerUpperColorTransferFunction,
|
setLowerUpperColorTransferFunction,
|
||||||
@ -37,18 +41,14 @@ const EVENTS = {
|
|||||||
* Handles cornerstone viewport logic including enabling, disabling, and
|
* Handles cornerstone viewport logic including enabling, disabling, and
|
||||||
* updating the viewport.
|
* updating the viewport.
|
||||||
*/
|
*/
|
||||||
class CornerstoneViewportService implements IViewportService {
|
class CornerstoneViewportService extends PubSubService
|
||||||
|
implements IViewportService {
|
||||||
renderingEngine: Types.IRenderingEngine | null;
|
renderingEngine: Types.IRenderingEngine | null;
|
||||||
viewportsInfo: Map<number, ViewportInfo>;
|
viewportsInfo: Map<number, ViewportInfo> = new Map();
|
||||||
|
viewportsById: Map<string, ViewportInfo> = new Map();
|
||||||
viewportGridResizeObserver: ResizeObserver | null;
|
viewportGridResizeObserver: ResizeObserver | null;
|
||||||
viewportsDisplaySets: Map<string, string[]> = new Map();
|
viewportsDisplaySets: Map<string, string[]> = new Map();
|
||||||
|
|
||||||
/**
|
|
||||||
* Service-specific
|
|
||||||
*/
|
|
||||||
EVENTS: { [key: string]: string };
|
|
||||||
listeners: { [key: string]: Array<(...args: any[]) => void> };
|
|
||||||
_broadcastEvent: unknown; // we should be able to extend the PubSub class to get this
|
|
||||||
// Some configs
|
// Some configs
|
||||||
enableResizeDetector: true;
|
enableResizeDetector: true;
|
||||||
resizeRefreshRateMs: 200;
|
resizeRefreshRateMs: 200;
|
||||||
@ -56,15 +56,10 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
servicesManager = null;
|
servicesManager = null;
|
||||||
|
|
||||||
constructor(servicesManager) {
|
constructor(servicesManager) {
|
||||||
|
super(EVENTS);
|
||||||
this.renderingEngine = null;
|
this.renderingEngine = null;
|
||||||
this.viewportGridResizeObserver = null;
|
this.viewportGridResizeObserver = null;
|
||||||
this.viewportsInfo = new Map();
|
|
||||||
//
|
|
||||||
this.listeners = {};
|
|
||||||
this.EVENTS = EVENTS;
|
|
||||||
this.servicesManager = servicesManager;
|
this.servicesManager = servicesManager;
|
||||||
Object.assign(this, pubSubServiceInterface);
|
|
||||||
//
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,12 +72,23 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
viewportOptions: PublicViewportOptions,
|
viewportOptions: PublicViewportOptions,
|
||||||
elementRef: HTMLDivElement
|
elementRef: HTMLDivElement
|
||||||
) {
|
) {
|
||||||
const viewportInfo = new ViewportInfo(
|
// Use the provided viewportId
|
||||||
viewportIndex,
|
// Not providing a viewportId is frowned upon because it does weird things
|
||||||
this.getViewportId(viewportIndex)
|
// on moving them around, but it does mostly work.
|
||||||
);
|
if (!viewportOptions.viewportId) {
|
||||||
|
console.warn('Should provide viewport id externally', viewportOptions);
|
||||||
|
viewportOptions.viewportId =
|
||||||
|
this.getViewportId(viewportIndex) || `viewport-${viewportIndex}`;
|
||||||
|
}
|
||||||
|
const { viewportId } = viewportOptions;
|
||||||
|
const viewportInfo = new ViewportInfo(viewportIndex, viewportId);
|
||||||
|
if (!viewportInfo.viewportId) {
|
||||||
|
throw new Error('Should have viewport ID afterwards');
|
||||||
|
}
|
||||||
|
|
||||||
viewportInfo.setElement(elementRef);
|
viewportInfo.setElement(elementRef);
|
||||||
this.viewportsInfo.set(viewportIndex, viewportInfo);
|
this.viewportsInfo.set(viewportIndex, viewportInfo);
|
||||||
|
this.viewportsById.set(viewportId, viewportInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getViewportIds(): string[] {
|
public getViewportIds(): string[] {
|
||||||
@ -96,7 +102,7 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getViewportId(viewportIndex: number): string {
|
public getViewportId(viewportIndex: number): string {
|
||||||
return `viewport-${viewportIndex}`;
|
return this.viewportsInfo[viewportIndex]?.viewportId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -149,9 +155,14 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
/**
|
/**
|
||||||
* Disables the viewport inside the renderingEngine, if no viewport is left
|
* Disables the viewport inside the renderingEngine, if no viewport is left
|
||||||
* it destroys the renderingEngine.
|
* it destroys the renderingEngine.
|
||||||
|
*
|
||||||
|
* This is called when the element goes away entirely - with new viewportId's
|
||||||
|
* created for every new viewport, this will be called whenever the set of
|
||||||
|
* viewports is changed, but NOT when the viewport position changes only.
|
||||||
|
*
|
||||||
* @param viewportIndex
|
* @param viewportIndex
|
||||||
*/
|
*/
|
||||||
public disableElement(viewportIndex: number) {
|
public disableElement(viewportIndex: number): void {
|
||||||
const viewportInfo = this.viewportsInfo.get(viewportIndex);
|
const viewportInfo = this.viewportsInfo.get(viewportIndex);
|
||||||
if (!viewportInfo) {
|
if (!viewportInfo) {
|
||||||
return;
|
return;
|
||||||
@ -163,6 +174,39 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
|
|
||||||
this.viewportsInfo.get(viewportIndex).destroy();
|
this.viewportsInfo.get(viewportIndex).destroy();
|
||||||
this.viewportsInfo.delete(viewportIndex);
|
this.viewportsInfo.delete(viewportIndex);
|
||||||
|
this.viewportsById.delete(viewportId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setPresentations(viewport, presentations?: Presentations): void {
|
||||||
|
const properties = presentations?.lutPresentation?.properties;
|
||||||
|
if (properties) viewport.setProperties(properties);
|
||||||
|
const camera = presentations?.positionPresentation?.camera;
|
||||||
|
if (camera) viewport.setCamera(camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPresentation(viewportIndex: number): Presentation {
|
||||||
|
const viewportInfo = this.viewportsInfo.get(viewportIndex);
|
||||||
|
if (!viewportInfo) return;
|
||||||
|
const { viewportType, presentationIds } = viewportInfo.getViewportOptions();
|
||||||
|
|
||||||
|
const csViewport = this.getCornerstoneViewportByIndex(viewportIndex);
|
||||||
|
if (!csViewport) return;
|
||||||
|
|
||||||
|
const properties = csViewport.getProperties();
|
||||||
|
if (properties.isComputedVOI) {
|
||||||
|
delete properties.voiRange;
|
||||||
|
delete properties.VOILUTFunction;
|
||||||
|
}
|
||||||
|
const initialImageIndex = csViewport.getCurrentImageIdIndex();
|
||||||
|
const camera = csViewport.getCamera();
|
||||||
|
return {
|
||||||
|
presentationIds,
|
||||||
|
viewportType:
|
||||||
|
!viewportType || viewportType === 'stack' ? 'stack' : 'volume',
|
||||||
|
properties,
|
||||||
|
initialImageIndex,
|
||||||
|
camera,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -177,29 +221,27 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
viewportIndex: number,
|
viewportIndex: number,
|
||||||
viewportData: StackViewportData | VolumeViewportData,
|
viewportData: StackViewportData | VolumeViewportData,
|
||||||
publicViewportOptions: PublicViewportOptions,
|
publicViewportOptions: PublicViewportOptions,
|
||||||
publicDisplaySetOptions: DisplaySetOptions[]
|
publicDisplaySetOptions: DisplaySetOptions[],
|
||||||
|
presentations?: Presentations
|
||||||
): void {
|
): void {
|
||||||
const renderingEngine = this.getRenderingEngine();
|
const renderingEngine = this.getRenderingEngine();
|
||||||
const viewportInfo = this.viewportsInfo.get(viewportIndex);
|
const viewportId =
|
||||||
|
publicViewportOptions.viewportId || this.getViewportId(viewportIndex);
|
||||||
if (!publicViewportOptions.viewportId) {
|
if (!viewportId) {
|
||||||
publicViewportOptions.viewportId = this.getViewportId(viewportIndex);
|
throw new Error('Must define viewportId externally');
|
||||||
}
|
}
|
||||||
|
|
||||||
let viewportId = viewportInfo.getViewportId();
|
const viewportInfo = this.viewportsById.get(viewportId);
|
||||||
|
|
||||||
// if currently there is a viewport with the viewportId, but it is not the same
|
if (!viewportInfo) {
|
||||||
// as the one we are trying to set, we need to disable the old one
|
throw new Error('Viewport info not defined');
|
||||||
// and enable the new one, we could ideally change the name of the viewportId
|
}
|
||||||
// but the viewportId is an integral part in renderers map, tools svg cache
|
|
||||||
// etc. which would require a lot of refactoring, for now we will just disable
|
|
||||||
// the old one and enable the new one at the end of this function
|
|
||||||
let newViewportId = null;
|
|
||||||
if (publicViewportOptions?.viewportId !== viewportId) {
|
|
||||||
newViewportId = publicViewportOptions.viewportId;
|
|
||||||
viewportInfo.setViewportId(newViewportId);
|
|
||||||
|
|
||||||
renderingEngine.disableElement(viewportId);
|
// If the viewport has moved index, then record the new index
|
||||||
|
if (viewportInfo.viewportIndex !== viewportIndex) {
|
||||||
|
this.viewportsInfo.delete(viewportInfo.viewportIndex);
|
||||||
|
this.viewportsInfo.set(viewportIndex, viewportInfo);
|
||||||
|
viewportInfo.viewportIndex = viewportIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
viewportInfo.setRenderingEngineId(renderingEngine.id);
|
viewportInfo.setRenderingEngineId(renderingEngine.id);
|
||||||
@ -217,12 +259,6 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
viewportInfo.setDisplaySetOptions(displaySetOptions);
|
viewportInfo.setDisplaySetOptions(displaySetOptions);
|
||||||
viewportInfo.setViewportData(viewportData);
|
viewportInfo.setViewportData(viewportData);
|
||||||
|
|
||||||
this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, {
|
|
||||||
viewportData,
|
|
||||||
viewportIndex,
|
|
||||||
});
|
|
||||||
|
|
||||||
viewportId = viewportInfo.getViewportId();
|
|
||||||
const element = viewportInfo.getElement();
|
const element = viewportInfo.getElement();
|
||||||
const type = viewportInfo.getViewportType();
|
const type = viewportInfo.getViewportType();
|
||||||
const background = viewportInfo.getBackground();
|
const background = viewportInfo.getBackground();
|
||||||
@ -245,7 +281,16 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
renderingEngine.enableElement(viewportInput);
|
renderingEngine.enableElement(viewportInput);
|
||||||
|
|
||||||
const viewport = renderingEngine.getViewport(viewportId);
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
this._setDisplaySets(viewport, viewportData, viewportInfo);
|
this._setDisplaySets(viewport, viewportData, viewportInfo, presentations);
|
||||||
|
|
||||||
|
// The broadcast event here ensures that listeners have a valid, up to date
|
||||||
|
// viewport to access. Doing it too early can result in exceptions or
|
||||||
|
// invalid data.
|
||||||
|
this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, {
|
||||||
|
viewportData,
|
||||||
|
viewportIndex,
|
||||||
|
viewportId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCornerstoneViewport(
|
public getCornerstoneViewport(
|
||||||
@ -308,8 +353,9 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
_setStackViewport(
|
_setStackViewport(
|
||||||
viewport: Types.IStackViewport,
|
viewport: Types.IStackViewport,
|
||||||
viewportData: StackViewportData,
|
viewportData: StackViewportData,
|
||||||
viewportInfo: ViewportInfo
|
viewportInfo: ViewportInfo,
|
||||||
) {
|
presentations: Presentations
|
||||||
|
): void {
|
||||||
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -320,29 +366,38 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
|
|
||||||
this.viewportsDisplaySets.set(viewport.id, [displaySetInstanceUID]);
|
this.viewportsDisplaySets.set(viewport.id, [displaySetInstanceUID]);
|
||||||
|
|
||||||
let initialImageIndexToUse = initialImageIndex;
|
let initialImageIndexToUse =
|
||||||
|
presentations?.positionPresentation?.initialImageIndex ??
|
||||||
|
initialImageIndex;
|
||||||
|
|
||||||
if (!initialImageIndexToUse) {
|
if (
|
||||||
|
initialImageIndexToUse === undefined ||
|
||||||
|
initialImageIndexToUse === null
|
||||||
|
) {
|
||||||
initialImageIndexToUse =
|
initialImageIndexToUse =
|
||||||
this._getInitialImageIndexForStackViewport(viewportInfo, imageIds) || 0;
|
this._getInitialImageIndexForStackViewport(viewportInfo, imageIds) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { voi, voiInverted } = displaySetOptions[0];
|
const properties = { ...presentations.lutPresentation?.properties };
|
||||||
const properties = {};
|
if (!presentations.lutPresentation?.properties) {
|
||||||
if (voi && (voi.windowWidth || voi.windowCenter)) {
|
const { voi, voiInverted } = displaySetOptions[0];
|
||||||
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
|
if (voi && (voi.windowWidth || voi.windowCenter)) {
|
||||||
voi.windowWidth,
|
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
|
||||||
voi.windowCenter
|
voi.windowWidth,
|
||||||
);
|
voi.windowCenter
|
||||||
properties.voiRange = { lower, upper };
|
);
|
||||||
}
|
properties.voiRange = { lower, upper };
|
||||||
|
}
|
||||||
|
|
||||||
if (voiInverted !== undefined) {
|
if (voiInverted !== undefined) {
|
||||||
properties.invert = voiInverted;
|
properties.invert = voiInverted;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
|
viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
|
||||||
viewport.setProperties(properties);
|
viewport.setProperties(properties);
|
||||||
|
const camera = presentations.positionPresentation?.camera;
|
||||||
|
if (camera) viewport.setCamera(camera);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -397,7 +452,8 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
async _setVolumeViewport(
|
async _setVolumeViewport(
|
||||||
viewport: Types.IVolumeViewport,
|
viewport: Types.IVolumeViewport,
|
||||||
viewportData: VolumeViewportData,
|
viewportData: VolumeViewportData,
|
||||||
viewportInfo: ViewportInfo
|
viewportInfo: ViewportInfo,
|
||||||
|
presentations: Presentations
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// TODO: We need to overhaul the way data sources work so requests can be made
|
// TODO: We need to overhaul the way data sources work so requests can be made
|
||||||
// async. I think we should follow the image loader pattern which is async and
|
// async. I think we should follow the image loader pattern which is async and
|
||||||
@ -423,6 +479,7 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
displaySetInstanceUIDs.push(displaySetInstanceUID);
|
displaySetInstanceUIDs.push(displaySetInstanceUID);
|
||||||
|
|
||||||
if (!volume) {
|
if (!volume) {
|
||||||
|
console.log('Volume display set not found');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -460,14 +517,24 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
volumeToLoad.forEach(volume => {
|
volumeToLoad.forEach(volume => {
|
||||||
volume.load();
|
if (!volume.loadStatus.loaded && !volume.loadStatus.loading) {
|
||||||
|
volume.load();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// This returns the async continuation only
|
// This returns the async continuation only
|
||||||
return this.setVolumesForViewport(viewport, volumeInputArray);
|
return this.setVolumesForViewport(
|
||||||
|
viewport,
|
||||||
|
volumeInputArray,
|
||||||
|
presentations
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setVolumesForViewport(viewport, volumeInputArray) {
|
public async setVolumesForViewport(
|
||||||
|
viewport,
|
||||||
|
volumeInputArray,
|
||||||
|
presentations
|
||||||
|
) {
|
||||||
const {
|
const {
|
||||||
displaySetService,
|
displaySetService,
|
||||||
segmentationService,
|
segmentationService,
|
||||||
@ -475,6 +542,7 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
} = this.servicesManager.services;
|
} = this.servicesManager.services;
|
||||||
|
|
||||||
await viewport.setVolumes(volumeInputArray);
|
await viewport.setVolumes(volumeInputArray);
|
||||||
|
this.setPresentations(viewport, presentations);
|
||||||
|
|
||||||
// load any secondary displaySets
|
// load any secondary displaySets
|
||||||
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
|
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
|
||||||
@ -557,6 +625,10 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
|
|
||||||
const viewportInfo = this.getViewportInfo(viewport.id);
|
const viewportInfo = this.getViewportInfo(viewport.id);
|
||||||
|
|
||||||
|
if (!viewportInfo) {
|
||||||
|
console.warn('Viewport info not defined for', viewport.id);
|
||||||
|
}
|
||||||
|
|
||||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
|
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
|
||||||
csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
|
csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
|
||||||
|
|
||||||
@ -589,7 +661,11 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
|
|
||||||
// Todo: keepCamera is an interim solution until we have a better solution for
|
// Todo: keepCamera is an interim solution until we have a better solution for
|
||||||
// keeping the camera position when the viewport data is changed
|
// keeping the camera position when the viewport data is changed
|
||||||
public updateViewport(viewportIndex, viewportData, keepCamera = false) {
|
public updateViewport(
|
||||||
|
viewportIndex: number,
|
||||||
|
viewportData,
|
||||||
|
keepCamera = false
|
||||||
|
) {
|
||||||
const viewportInfo = this.getViewportInfoByIndex(viewportIndex);
|
const viewportInfo = this.getViewportInfoByIndex(viewportIndex);
|
||||||
|
|
||||||
const viewportId = viewportInfo.getViewportId();
|
const viewportId = viewportInfo.getViewportId();
|
||||||
@ -614,7 +690,12 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getVOICallbacks(volumeId, displaySetOptions) {
|
_getVOICallbacks(volumeId, displaySetOptions) {
|
||||||
const { voi, voiInverted: inverted, colormap } = displaySetOptions;
|
const {
|
||||||
|
voi,
|
||||||
|
voiInverted: inverted,
|
||||||
|
colormap,
|
||||||
|
presetName,
|
||||||
|
} = displaySetOptions;
|
||||||
|
|
||||||
const voiCallbackArray = [];
|
const voiCallbackArray = [];
|
||||||
|
|
||||||
@ -639,25 +720,41 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (presetName) {
|
||||||
|
voiCallbackArray.push(volumeActor => {
|
||||||
|
utilities.applyPreset(
|
||||||
|
volumeActor,
|
||||||
|
CONSTANTS.VIEWPORT_PRESETS.find(preset => {
|
||||||
|
return preset.name === presetName;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
return voiCallbackArray;
|
return voiCallbackArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
_setDisplaySets(
|
_setDisplaySets(
|
||||||
viewport: StackViewport | VolumeViewport,
|
viewport: StackViewport | VolumeViewport,
|
||||||
viewportData: StackViewportData | VolumeViewportData,
|
viewportData: StackViewportData | VolumeViewportData,
|
||||||
viewportInfo: ViewportInfo
|
viewportInfo: ViewportInfo,
|
||||||
|
presentations: Presentations = {}
|
||||||
): void {
|
): void {
|
||||||
if (viewport instanceof StackViewport) {
|
if (viewport instanceof StackViewport) {
|
||||||
this._setStackViewport(
|
this._setStackViewport(
|
||||||
viewport,
|
viewport,
|
||||||
viewportData as StackViewportData,
|
viewportData as StackViewportData,
|
||||||
viewportInfo
|
viewportInfo,
|
||||||
|
presentations
|
||||||
);
|
);
|
||||||
} else if (viewport instanceof VolumeViewport) {
|
} else if (
|
||||||
|
viewport instanceof VolumeViewport ||
|
||||||
|
viewport instanceof VolumeViewport3D
|
||||||
|
) {
|
||||||
this._setVolumeViewport(
|
this._setVolumeViewport(
|
||||||
viewport,
|
viewport,
|
||||||
viewportData as VolumeViewportData,
|
viewportData as VolumeViewportData,
|
||||||
viewportInfo
|
viewportInfo,
|
||||||
|
presentations
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unknown viewport type');
|
throw new Error('Unknown viewport type');
|
||||||
@ -758,7 +855,7 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExtendedCornerstoneViewportService(serviceManager) {
|
export default function CornerstoneViewportServiceRegistration(serviceManager) {
|
||||||
return {
|
return {
|
||||||
name: 'cornerstoneViewportService',
|
name: 'cornerstoneViewportService',
|
||||||
altName: 'CornerstoneViewportService',
|
altName: 'CornerstoneViewportService',
|
||||||
@ -767,3 +864,5 @@ export default function ExtendedCornerstoneViewportService(serviceManager) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { CornerstoneViewportService, CornerstoneViewportServiceRegistration };
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import { Types, Enums } from '@cornerstonejs/core';
|
import { Types, Enums } from '@cornerstonejs/core';
|
||||||
|
import { Types as UITypes } from '@ohif/ui';
|
||||||
|
import {
|
||||||
|
StackViewportData,
|
||||||
|
VolumeViewportData,
|
||||||
|
} from '../../types/CornerstoneCacheService';
|
||||||
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
|
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
|
||||||
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
|
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
|
||||||
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
||||||
import JumpPresets from '../../utils/JumpPresets';
|
import JumpPresets from '../../utils/JumpPresets';
|
||||||
import { SyncGroup } from '../SyncGroupService/SyncGroupService';
|
import { SyncGroup } from '../SyncGroupService/SyncGroupService';
|
||||||
import {
|
|
||||||
StackViewportData,
|
|
||||||
VolumeViewportData,
|
|
||||||
} from '../../types/CornerstoneCacheService';
|
|
||||||
|
|
||||||
export type InitialImageOptions = {
|
export type InitialImageOptions = {
|
||||||
index?: number;
|
index?: number;
|
||||||
@ -15,10 +16,13 @@ export type InitialImageOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ViewportOptions = {
|
export type ViewportOptions = {
|
||||||
|
id?: string;
|
||||||
viewportType: Enums.ViewportType;
|
viewportType: Enums.ViewportType;
|
||||||
toolGroupId: string;
|
toolGroupId: string;
|
||||||
viewportId: string;
|
viewportId: string;
|
||||||
orientation?: Types.Orientation;
|
// Presentation ID to store/load presentation state from
|
||||||
|
presentationIds?: UITypes.PresentationIds;
|
||||||
|
orientation?: Enums.OrientationAxis;
|
||||||
background?: Types.Point3;
|
background?: Types.Point3;
|
||||||
syncGroups?: SyncGroup[];
|
syncGroups?: SyncGroup[];
|
||||||
initialImageOptions?: InitialImageOptions;
|
initialImageOptions?: InitialImageOptions;
|
||||||
@ -31,10 +35,12 @@ export type ViewportOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type PublicViewportOptions = {
|
export type PublicViewportOptions = {
|
||||||
|
id?: string;
|
||||||
viewportType?: string;
|
viewportType?: string;
|
||||||
toolGroupId?: string;
|
toolGroupId?: string;
|
||||||
|
presentationIds?: UITypes.PresentationIds;
|
||||||
viewportId?: string;
|
viewportId?: string;
|
||||||
orientation?: string;
|
orientation?: Enums.OrientationAxis;
|
||||||
background?: Types.Point3;
|
background?: Types.Point3;
|
||||||
syncGroups?: SyncGroup[];
|
syncGroups?: SyncGroup[];
|
||||||
initialImageOptions?: InitialImageOptions;
|
initialImageOptions?: InitialImageOptions;
|
||||||
@ -42,20 +48,32 @@ export type PublicViewportOptions = {
|
|||||||
allowUnmatchedView?: boolean;
|
allowUnmatchedView?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DisplaySetSelector = {
|
||||||
|
id?: string;
|
||||||
|
options?: PublicDisplaySetOptions;
|
||||||
|
};
|
||||||
|
|
||||||
export type PublicDisplaySetOptions = {
|
export type PublicDisplaySetOptions = {
|
||||||
|
/** The display set options can have an id in order to distinguish
|
||||||
|
* it from other similar items.
|
||||||
|
*/
|
||||||
|
id?: string;
|
||||||
voi?: VOI;
|
voi?: VOI;
|
||||||
voiInverted?: boolean;
|
voiInverted?: boolean;
|
||||||
blendMode?: string;
|
blendMode?: string;
|
||||||
slabThickness?: number;
|
slabThickness?: number;
|
||||||
colormap?: string;
|
colormap?: string;
|
||||||
|
presetName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DisplaySetOptions = {
|
export type DisplaySetOptions = {
|
||||||
|
id?: string;
|
||||||
voi?: VOI;
|
voi?: VOI;
|
||||||
voiInverted: boolean;
|
voiInverted: boolean;
|
||||||
blendMode?: Enums.BlendModes;
|
blendMode?: Enums.BlendModes;
|
||||||
slabThickness?: number;
|
slabThickness?: number;
|
||||||
colormap?: string;
|
colormap?: string;
|
||||||
|
presetName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type VOI = {
|
type VOI = {
|
||||||
@ -68,7 +86,6 @@ export type DisplaySet = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const STACK = 'stack';
|
const STACK = 'stack';
|
||||||
const VOLUME = 'volume';
|
|
||||||
const DEFAULT_TOOLGROUP_ID = 'default';
|
const DEFAULT_TOOLGROUP_ID = 'default';
|
||||||
|
|
||||||
class ViewportInfo {
|
class ViewportInfo {
|
||||||
@ -136,7 +153,7 @@ class ViewportInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setPublicDisplaySetOptions(
|
public setPublicDisplaySetOptions(
|
||||||
publicDisplaySetOptions: Array<PublicDisplaySetOptions>
|
publicDisplaySetOptions: PublicDisplaySetOptions[] | DisplaySetSelector[]
|
||||||
): void {
|
): void {
|
||||||
// map the displaySetOptions and check if they are undefined then set them to default values
|
// map the displaySetOptions and check if they are undefined then set them to default values
|
||||||
const displaySetOptions = this.mapDisplaySetOptions(
|
const displaySetOptions = this.mapDisplaySetOptions(
|
||||||
@ -167,7 +184,10 @@ class ViewportInfo {
|
|||||||
viewportOptionsEntry: PublicViewportOptions
|
viewportOptionsEntry: PublicViewportOptions
|
||||||
): void {
|
): void {
|
||||||
let viewportType = viewportOptionsEntry.viewportType;
|
let viewportType = viewportOptionsEntry.viewportType;
|
||||||
let toolGroupId = viewportOptionsEntry.toolGroupId;
|
const {
|
||||||
|
toolGroupId = DEFAULT_TOOLGROUP_ID,
|
||||||
|
presentationIds,
|
||||||
|
} = viewportOptionsEntry;
|
||||||
let orientation;
|
let orientation;
|
||||||
|
|
||||||
if (!viewportType) {
|
if (!viewportType) {
|
||||||
@ -179,14 +199,8 @@ class ViewportInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// map SAGITTAL, AXIAL, CORONAL orientation to be used by cornerstone
|
// map SAGITTAL, AXIAL, CORONAL orientation to be used by cornerstone
|
||||||
if (viewportOptionsEntry.viewportType?.toLowerCase() === VOLUME) {
|
if (viewportOptionsEntry.viewportType?.toLowerCase() !== STACK) {
|
||||||
orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation);
|
orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation);
|
||||||
} else {
|
|
||||||
orientation = Enums.OrientationAxis.AXIAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!toolGroupId) {
|
|
||||||
toolGroupId = DEFAULT_TOOLGROUP_ID;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setViewportOptions({
|
this.setViewportOptions({
|
||||||
@ -195,6 +209,7 @@ class ViewportInfo {
|
|||||||
viewportType: viewportType as Enums.ViewportType,
|
viewportType: viewportType as Enums.ViewportType,
|
||||||
orientation,
|
orientation,
|
||||||
toolGroupId,
|
toolGroupId,
|
||||||
|
presentationIds,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,7 +247,7 @@ class ViewportInfo {
|
|||||||
return this.viewportOptions.background || [0, 0, 0];
|
return this.viewportOptions.background || [0, 0, 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
public getOrientation(): Types.Orientation {
|
public getOrientation(): Enums.OrientationAxis {
|
||||||
return this.viewportOptions.orientation;
|
return this.viewportOptions.orientation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,12 +255,15 @@ class ViewportInfo {
|
|||||||
return this.viewportOptions.initialImageOptions;
|
return this.viewportOptions.initialImageOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle incoming public display set options or a display set select
|
||||||
|
// with a contained options.
|
||||||
private mapDisplaySetOptions(
|
private mapDisplaySetOptions(
|
||||||
publicDisplaySetOptions: Array<PublicDisplaySetOptions>
|
options: PublicDisplaySetOptions[] | DisplaySetSelector[] = [{}]
|
||||||
): Array<DisplaySetOptions> {
|
): Array<DisplaySetOptions> {
|
||||||
const displaySetOptions: Array<DisplaySetOptions> = [];
|
const displaySetOptions: Array<DisplaySetOptions> = [];
|
||||||
|
|
||||||
publicDisplaySetOptions.forEach(option => {
|
options.forEach(item => {
|
||||||
|
let option = item?.options || item;
|
||||||
if (!option) {
|
if (!option) {
|
||||||
option = {
|
option = {
|
||||||
blendMode: undefined,
|
blendMode: undefined,
|
||||||
@ -263,6 +281,7 @@ class ViewportInfo {
|
|||||||
colormap: option.colormap,
|
colormap: option.colormap,
|
||||||
slabThickness: option.slabThickness,
|
slabThickness: option.slabThickness,
|
||||||
blendMode,
|
blendMode,
|
||||||
|
presetName: option.presetName,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { metaData } from '@cornerstonejs/core';
|
import { metaData } from '@cornerstonejs/core';
|
||||||
import { LengthTool } from '@cornerstonejs/tools';
|
import { LengthTool, utilities } from '@cornerstonejs/tools';
|
||||||
import { calibrateImageSpacing } from '@cornerstonejs/tools/dist/esm/utilities';
|
|
||||||
import callInputDialog from '../utils/callInputDialog';
|
import callInputDialog from '../utils/callInputDialog';
|
||||||
import getActiveViewportEnabledElement from '../utils/getActiveViewportEnabledElement';
|
import getActiveViewportEnabledElement from '../utils/getActiveViewportEnabledElement';
|
||||||
|
|
||||||
|
const { calibrateImageSpacing } = utilities;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calibration Line tool works almost the same as the
|
* Calibration Line tool works almost the same as the
|
||||||
*/
|
*/
|
||||||
|
|||||||
23
extensions/cornerstone/src/types/Presentation.ts
Normal file
23
extensions/cornerstone/src/types/Presentation.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/** Store presentation data for either stack viewports or volume viewports */
|
||||||
|
import { Types } from '@cornerstonejs/core';
|
||||||
|
import { Types as UITypes } from '@ohif/ui';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has information on the presentation of the viewport.
|
||||||
|
*/
|
||||||
|
export interface Presentation extends Types.StackViewportProperties {
|
||||||
|
presentationIds: UITypes.PresentationIds;
|
||||||
|
viewportType: string;
|
||||||
|
initialImageIndex: number;
|
||||||
|
camera: Types.ICamera;
|
||||||
|
properties: Types.StackViewportProperties | Types.VolumeViewportProperties;
|
||||||
|
zoom?: number;
|
||||||
|
pan?: [number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Presentations = {
|
||||||
|
positionPresentation?: Presentation;
|
||||||
|
lutPresentation?: Presentation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Presentation;
|
||||||
21
extensions/cornerstone/src/utils/findNearbyToolData.ts
Normal file
21
extensions/cornerstone/src/utils/findNearbyToolData.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Finds tool nearby event position triggered.
|
||||||
|
*
|
||||||
|
* @param {Object} commandsManager mannager of commands
|
||||||
|
* @param {Object} event that has being triggered
|
||||||
|
* @returns cs toolData or undefined if not found.
|
||||||
|
*/
|
||||||
|
export const findNearbyToolData = (commandsManager, evt) => {
|
||||||
|
if (!evt?.detail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { element, currentPoints } = evt.detail;
|
||||||
|
return commandsManager.runCommand(
|
||||||
|
'getNearbyToolData',
|
||||||
|
{
|
||||||
|
element,
|
||||||
|
canvasCoordinates: currentPoints?.canvas,
|
||||||
|
},
|
||||||
|
'CORNERSTONE'
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import { Enums } from '@cornerstonejs/core';
|
import { Enums } from '@cornerstonejs/core';
|
||||||
import { log } from '@ohif/core';
|
|
||||||
|
|
||||||
const AXIAL = 'axial';
|
const AXIAL = 'axial';
|
||||||
const SAGITTAL = 'sagittal';
|
const SAGITTAL = 'sagittal';
|
||||||
|
|||||||
@ -2,18 +2,25 @@ import { Enums } from '@cornerstonejs/core';
|
|||||||
|
|
||||||
const STACK = 'stack';
|
const STACK = 'stack';
|
||||||
const VOLUME = 'volume';
|
const VOLUME = 'volume';
|
||||||
|
const ORTHOGRAPHIC = 'orthographic';
|
||||||
|
const VOLUME_3D = 'volume3d';
|
||||||
|
|
||||||
export default function getCornerstoneViewportType(
|
export default function getCornerstoneViewportType(
|
||||||
viewportType: string
|
viewportType: string
|
||||||
): Enums.ViewportType {
|
): Enums.ViewportType {
|
||||||
if (viewportType.toLowerCase() === STACK) {
|
const lowerViewportType = viewportType.toLowerCase();
|
||||||
|
if (lowerViewportType === STACK) {
|
||||||
return Enums.ViewportType.STACK;
|
return Enums.ViewportType.STACK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewportType.toLowerCase() === VOLUME) {
|
if (lowerViewportType === VOLUME || lowerViewportType === ORTHOGRAPHIC) {
|
||||||
return Enums.ViewportType.ORTHOGRAPHIC;
|
return Enums.ViewportType.ORTHOGRAPHIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lowerViewportType === VOLUME_3D) {
|
||||||
|
return Enums.ViewportType.VOLUME_3D;
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid viewport type: ${viewportType}. Valid types are: stack, volume`
|
`Invalid viewport type: ${viewportType}. Valid types are: stack, volume`
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,301 +0,0 @@
|
|||||||
import { Enums } from '@cornerstonejs/tools';
|
|
||||||
import removeToolGroupSegmentationRepresentations from '../removeToolGroupSegmentationRepresentations';
|
|
||||||
|
|
||||||
const MPR_TOOLGROUP_ID = 'mpr';
|
|
||||||
|
|
||||||
const cachedState = {
|
|
||||||
protocol: null,
|
|
||||||
stage: null,
|
|
||||||
viewportMatchDetails: null,
|
|
||||||
viewportStructure: null,
|
|
||||||
toolOptions: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const setCachedState = (
|
|
||||||
protocol,
|
|
||||||
stage,
|
|
||||||
viewportMatchDetails,
|
|
||||||
viewportStructure,
|
|
||||||
toolOptions
|
|
||||||
) => {
|
|
||||||
cachedState.protocol = protocol;
|
|
||||||
cachedState.stage = stage;
|
|
||||||
cachedState.viewportMatchDetails = viewportMatchDetails;
|
|
||||||
cachedState.viewportStructure = viewportStructure;
|
|
||||||
cachedState.toolOptions = JSON.parse(JSON.stringify(toolOptions));
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetCachedState = () => {
|
|
||||||
cachedState.protocol = null;
|
|
||||||
cachedState.stage = null;
|
|
||||||
cachedState.viewportMatchDetails = null;
|
|
||||||
cachedState.viewportStructure = null;
|
|
||||||
cachedState.toolOptions = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function toggleMPRHangingProtocol({
|
|
||||||
toggledState,
|
|
||||||
servicesManager,
|
|
||||||
getToolGroup,
|
|
||||||
}) {
|
|
||||||
const {
|
|
||||||
uiNotificationService,
|
|
||||||
hangingProtocolService,
|
|
||||||
viewportGridService,
|
|
||||||
toolbarService,
|
|
||||||
} = 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 {
|
|
||||||
activeViewportIndex,
|
|
||||||
viewports,
|
|
||||||
numRows,
|
|
||||||
numCols,
|
|
||||||
} = viewportGridService.getState();
|
|
||||||
const viewportDisplaySetInstanceUIDs =
|
|
||||||
viewports[activeViewportIndex].displaySetInstanceUIDs;
|
|
||||||
|
|
||||||
// What is the current active protocol and stage number to restore later
|
|
||||||
const { protocol, stage } = hangingProtocolService.getActiveProtocol();
|
|
||||||
|
|
||||||
const restoreErrorCallback = error => {
|
|
||||||
console.error(error);
|
|
||||||
uiNotificationService.show({
|
|
||||||
title: 'Multiplanar reconstruction (MPR) ',
|
|
||||||
message:
|
|
||||||
'Something went wrong while trying to restore the previous layout.',
|
|
||||||
type: 'info',
|
|
||||||
duration: 3000,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (toggledState) {
|
|
||||||
resetCachedState();
|
|
||||||
|
|
||||||
const {
|
|
||||||
viewportMatchDetails,
|
|
||||||
viewportStructure,
|
|
||||||
toolOptions,
|
|
||||||
} = _getViewportsInfo({
|
|
||||||
protocol,
|
|
||||||
stage,
|
|
||||||
viewports,
|
|
||||||
servicesManager,
|
|
||||||
});
|
|
||||||
|
|
||||||
setCachedState(
|
|
||||||
protocol,
|
|
||||||
stage,
|
|
||||||
viewportMatchDetails,
|
|
||||||
viewportStructure,
|
|
||||||
toolOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
const matchDetails = {
|
|
||||||
displaySetInstanceUIDs: viewportDisplaySetInstanceUIDs,
|
|
||||||
};
|
|
||||||
|
|
||||||
_disableCrosshairs(
|
|
||||||
toolOptions.map(({ toolGroupId }) => toolGroupId),
|
|
||||||
getToolGroup
|
|
||||||
);
|
|
||||||
|
|
||||||
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,
|
|
||||||
matchDetails,
|
|
||||||
errorCallback
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_disableCrosshairs([MPR_TOOLGROUP_ID], getToolGroup);
|
|
||||||
|
|
||||||
const { layoutType, properties } = cachedState.viewportStructure;
|
|
||||||
const { viewportMatchDetails } = cachedState;
|
|
||||||
|
|
||||||
// The reason we split the flow here is that we don't allow viewport grid
|
|
||||||
// change in the non default hanging protocol, so we can just apply the
|
|
||||||
// cached protocol and stage. However, for the default protocol, we need
|
|
||||||
// to also apply the layout type and properties.
|
|
||||||
if (cachedState.protocol.id !== 'default') {
|
|
||||||
hangingProtocolService.setProtocol(
|
|
||||||
cachedState.protocol.id,
|
|
||||||
viewportMatchDetails,
|
|
||||||
restoreErrorCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
hangingProtocolService.setProtocol(
|
|
||||||
'default',
|
|
||||||
viewportMatchDetails,
|
|
||||||
restoreErrorCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
if (numRows !== properties.rows || numCols !== properties.columns) {
|
|
||||||
viewportGridService.setLayout({
|
|
||||||
numRows: properties.rows,
|
|
||||||
numCols: properties.columns,
|
|
||||||
layoutType,
|
|
||||||
layoutOptions: properties.layoutOptions,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const numViewports =
|
|
||||||
properties.layoutOptions.length || properties.rows * properties.columns;
|
|
||||||
|
|
||||||
// loop inside viewportMatchDetails map
|
|
||||||
// and set the viewportOptions for each viewport
|
|
||||||
[...Array(numViewports).keys()].forEach(viewportIndex => {
|
|
||||||
const viewportMatchDetailsForViewport = viewportMatchDetails.get(
|
|
||||||
viewportIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
if (viewportMatchDetailsForViewport) {
|
|
||||||
const {
|
|
||||||
viewportOptions,
|
|
||||||
displaySetsInfo,
|
|
||||||
} = viewportMatchDetailsForViewport;
|
|
||||||
viewportGridService.setDisplaySetsForViewport({
|
|
||||||
viewportIndex,
|
|
||||||
displaySetInstanceUIDs: displaySetsInfo.map(
|
|
||||||
displaySetInfo => displaySetInfo.displaySetInstanceUID
|
|
||||||
),
|
|
||||||
viewportOptions,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
viewportGridService.setDisplaySetsForViewport({
|
|
||||||
viewportIndex,
|
|
||||||
displaySetInstanceUIDs: [],
|
|
||||||
viewportOptions: {},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
toolbarService.recordInteraction({
|
|
||||||
groupId: 'WindowLevel',
|
|
||||||
itemId: 'WindowLevel',
|
|
||||||
interactionType: 'tool',
|
|
||||||
commands: [
|
|
||||||
{
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: {
|
|
||||||
toolName: 'WindowLevel',
|
|
||||||
},
|
|
||||||
context: 'CORNERSTONE',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
//clear segmentations if they exist
|
|
||||||
removeToolGroupSegmentationRepresentations(MPR_TOOLGROUP_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _disableCrosshairs(toolGroupIds, getToolGroup) {
|
|
||||||
toolGroupIds.forEach(toolGroupId => {
|
|
||||||
const toolGroup = getToolGroup(toolGroupId);
|
|
||||||
if (
|
|
||||||
toolGroup.getToolInstance('Crosshairs')?.mode === Enums.ToolModes.Active
|
|
||||||
) {
|
|
||||||
toolGroup.setToolDisabled('Crosshairs');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function _getViewportsInfo({ protocol, stage, viewports, servicesManager }) {
|
|
||||||
// here we need to use the viewports and try to map it into the
|
|
||||||
// viewportMatchDetails and displaySetMatch that hangingProtocolService
|
|
||||||
// expects
|
|
||||||
const {
|
|
||||||
viewportGridService,
|
|
||||||
hangingProtocolService,
|
|
||||||
toolGroupService,
|
|
||||||
} = servicesManager.services;
|
|
||||||
|
|
||||||
const { numRows, numCols } = viewportGridService.getState();
|
|
||||||
|
|
||||||
let viewportMatchDetails = new Map();
|
|
||||||
|
|
||||||
const viewportStructure = {
|
|
||||||
layoutType: 'grid',
|
|
||||||
properties: {
|
|
||||||
rows: numRows,
|
|
||||||
columns: numCols,
|
|
||||||
layoutOptions: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
viewports.forEach((viewport, viewportIndex) => {
|
|
||||||
viewportStructure.properties.layoutOptions.push({
|
|
||||||
x: viewport.x,
|
|
||||||
y: viewport.y,
|
|
||||||
width: viewport.width,
|
|
||||||
height: viewport.height,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (protocol.id === 'default') {
|
|
||||||
viewports.forEach((viewport, viewportIndex) => {
|
|
||||||
if (viewport.displaySetInstanceUIDs) {
|
|
||||||
viewportMatchDetails.set(viewportIndex, {
|
|
||||||
displaySetsInfo: viewport.displaySetInstanceUIDs.map(
|
|
||||||
displaySetInstanceUID => {
|
|
||||||
return { displaySetInstanceUID };
|
|
||||||
}
|
|
||||||
),
|
|
||||||
viewportOptions: viewport.viewportOptions,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
({ viewportMatchDetails } = hangingProtocolService.getMatchDetails());
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the toolGroup state for viewports
|
|
||||||
let toolOptions = [];
|
|
||||||
const viewportIds = viewports
|
|
||||||
.map(
|
|
||||||
viewport =>
|
|
||||||
viewport.displaySetInstanceUIDs &&
|
|
||||||
viewport.displaySetInstanceUIDs.length > 0 &&
|
|
||||||
viewport.viewportOptions?.viewportId
|
|
||||||
)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
if (viewportIds.length) {
|
|
||||||
toolOptions = viewportIds
|
|
||||||
.map(viewportId => {
|
|
||||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
|
||||||
return toolGroup
|
|
||||||
? {
|
|
||||||
toolGroupId: toolGroup.id,
|
|
||||||
toolOptions: toolGroup.toolOptions,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { viewportMatchDetails, viewportStructure, toolOptions };
|
|
||||||
}
|
|
||||||
@ -18,7 +18,6 @@ const viewportIdVolumeInputArrayMap = new Map<string, unknown[]>();
|
|||||||
export default function interleaveNthLoader({
|
export default function interleaveNthLoader({
|
||||||
data: { viewportId, volumeInputArray },
|
data: { viewportId, volumeInputArray },
|
||||||
displaySetsMatchDetails,
|
displaySetsMatchDetails,
|
||||||
viewportMatchDetails: matchDetails,
|
|
||||||
}) {
|
}) {
|
||||||
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);
|
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);
|
||||||
|
|
||||||
@ -29,6 +28,7 @@ export default function interleaveNthLoader({
|
|||||||
const volume = cache.getVolume(volumeId);
|
const volume = cache.getVolume(volumeId);
|
||||||
|
|
||||||
if (!volume) {
|
if (!volume) {
|
||||||
|
console.log("interleaveNthLoader::No volume, can't load it");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,33 +39,6 @@ export default function interleaveNthLoader({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice();
|
||||||
// get volumes from cache
|
// get volumes from cache
|
||||||
const volumes = volumeIds.map(volumeId => {
|
const volumes = volumeIds.map(volumeId => {
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "OHIF/Viewers",
|
"repository": "OHIF/Viewers",
|
||||||
"main": "dist/index.umd.js",
|
"main": "dist/index.umd.js",
|
||||||
"module": "src/index.js",
|
"module": "src/index.ts",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -0,0 +1,208 @@
|
|||||||
|
import * as ContextMenuItemsBuilder from './ContextMenuItemsBuilder';
|
||||||
|
import ContextMenu from '../../../../platform/ui/src/components/ContextMenu/ContextMenu';
|
||||||
|
import { CommandsManager, ServicesManager, Types } from '@ohif/core';
|
||||||
|
import { Menu, MenuItem, Point, ContextMenuProps } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The context menu controller is a helper class that knows how
|
||||||
|
* to manage context menus based on the UI Customization Service.
|
||||||
|
* There are a few parts to this:
|
||||||
|
* 1. Basic controls to manage displaying and hiding context menus
|
||||||
|
* 2. Menu selection services, which use the UI customization service
|
||||||
|
* to choose which menu to display
|
||||||
|
* 3. Menu item adapter services to convert menu items into displayable and actionable items.
|
||||||
|
*
|
||||||
|
* The format for a menu is defined in the exported type MenuItem
|
||||||
|
*/
|
||||||
|
export default class ContextMenuController {
|
||||||
|
commandsManager: CommandsManager;
|
||||||
|
services: Types.Services;
|
||||||
|
menuItems: Menu[] | MenuItem[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
servicesManager: ServicesManager,
|
||||||
|
commandsManager: CommandsManager
|
||||||
|
) {
|
||||||
|
this.services = servicesManager.services as Obj;
|
||||||
|
this.commandsManager = commandsManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeContextMenu() {
|
||||||
|
this.services.uiDialogService.dismiss({ id: 'context-menu' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Figures out which context menu is appropriate to display and shows it.
|
||||||
|
*
|
||||||
|
* @param contextMenuProps - the context menu properties, see ./types.ts
|
||||||
|
* @param viewportElement - the DOM element this context menu is related to
|
||||||
|
* @param defaultPointsPosition - a default position to show the context menu
|
||||||
|
*/
|
||||||
|
showContextMenu(
|
||||||
|
contextMenuProps: ContextMenuProps,
|
||||||
|
viewportElement,
|
||||||
|
defaultPointsPosition
|
||||||
|
): void {
|
||||||
|
if (!this.services.uiDialogService) {
|
||||||
|
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { event, subMenu, menuId, menus, selectorProps } = contextMenuProps;
|
||||||
|
|
||||||
|
console.log('Getting items from', menus);
|
||||||
|
const items = ContextMenuItemsBuilder.getMenuItems(
|
||||||
|
selectorProps || contextMenuProps,
|
||||||
|
event,
|
||||||
|
menus,
|
||||||
|
menuId
|
||||||
|
);
|
||||||
|
|
||||||
|
this.services.uiDialogService.dismiss({ id: 'context-menu' });
|
||||||
|
this.services.uiDialogService.create({
|
||||||
|
id: 'context-menu',
|
||||||
|
isDraggable: false,
|
||||||
|
preservePosition: false,
|
||||||
|
preventCutOf: true,
|
||||||
|
defaultPosition: ContextMenuController._getDefaultPosition(
|
||||||
|
defaultPointsPosition,
|
||||||
|
event?.detail,
|
||||||
|
viewportElement
|
||||||
|
),
|
||||||
|
event,
|
||||||
|
content: ContextMenu,
|
||||||
|
|
||||||
|
// This naming is part of hte uiDialogService convention
|
||||||
|
// Clicking outside simpy closes the dialog box.
|
||||||
|
onClickOutside: () =>
|
||||||
|
this.services.uiDialogService.dismiss({ id: 'context-menu' }),
|
||||||
|
|
||||||
|
contentProps: {
|
||||||
|
items,
|
||||||
|
selectorProps,
|
||||||
|
menus,
|
||||||
|
event,
|
||||||
|
subMenu,
|
||||||
|
eventData: event?.detail,
|
||||||
|
|
||||||
|
onClose: () => {
|
||||||
|
this.services.uiDialogService.dismiss({ id: 'context-menu' });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a sub-menu, removing this menu
|
||||||
|
* @param {*} item
|
||||||
|
* @param {*} itemRef
|
||||||
|
* @param {*} subProps
|
||||||
|
*/
|
||||||
|
onShowSubMenu: (item, itemRef, subProps) => {
|
||||||
|
if (!itemRef.subMenu) {
|
||||||
|
console.warn('No submenu defined for', item, itemRef, subProps);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.showContextMenu(
|
||||||
|
{
|
||||||
|
...contextMenuProps,
|
||||||
|
menuId: itemRef.subMenu,
|
||||||
|
},
|
||||||
|
viewportElement,
|
||||||
|
defaultPointsPosition
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Default is to run the specified commands.
|
||||||
|
onDefault: (item, itemRef, subProps) => {
|
||||||
|
this.commandsManager.run(item, {
|
||||||
|
...selectorProps,
|
||||||
|
...itemRef,
|
||||||
|
subProps,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDefaultPosition = (): Point => {
|
||||||
|
return {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
static _getEventDefaultPosition = eventDetail => ({
|
||||||
|
x: eventDetail && eventDetail.currentPoints.client[0],
|
||||||
|
y: eventDetail && eventDetail.currentPoints.client[1],
|
||||||
|
});
|
||||||
|
|
||||||
|
static _getElementDefaultPosition = element => {
|
||||||
|
if (element) {
|
||||||
|
const boundingClientRect = element.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: boundingClientRect.x,
|
||||||
|
y: boundingClientRect.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: undefined,
|
||||||
|
y: undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
static _getCanvasPointsPosition = (points = [], element) => {
|
||||||
|
const viewerPos = ContextMenuController._getElementDefaultPosition(element);
|
||||||
|
|
||||||
|
for (let pointIndex = 0; pointIndex < points.length; pointIndex++) {
|
||||||
|
const point = {
|
||||||
|
x: points[pointIndex][0] || points[pointIndex]['x'],
|
||||||
|
y: points[pointIndex][1] || points[pointIndex]['y'],
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
ContextMenuController._isValidPosition(point) &&
|
||||||
|
ContextMenuController._isValidPosition(viewerPos)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
x: point.x + viewerPos.x,
|
||||||
|
y: point.y + viewerPos.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static _isValidPosition = (source): boolean => {
|
||||||
|
return (
|
||||||
|
source && typeof source.x === 'number' && typeof source.y === 'number'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the context menu default position. It look for the positions of: canvasPoints (got from selected), event that triggers it, current viewport element
|
||||||
|
*/
|
||||||
|
static _getDefaultPosition = (canvasPoints, eventDetail, viewerElement) => {
|
||||||
|
function* getPositionIterator() {
|
||||||
|
yield ContextMenuController._getCanvasPointsPosition(
|
||||||
|
canvasPoints,
|
||||||
|
viewerElement
|
||||||
|
);
|
||||||
|
yield ContextMenuController._getEventDefaultPosition(eventDetail);
|
||||||
|
yield ContextMenuController._getElementDefaultPosition(viewerElement);
|
||||||
|
yield ContextMenuController.getDefaultPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
const positionIterator = getPositionIterator();
|
||||||
|
|
||||||
|
let current = positionIterator.next();
|
||||||
|
let position = current.value;
|
||||||
|
|
||||||
|
while (!current.done) {
|
||||||
|
position = current.value;
|
||||||
|
|
||||||
|
if (ContextMenuController._isValidPosition(position)) {
|
||||||
|
positionIterator.return();
|
||||||
|
}
|
||||||
|
current = positionIterator.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return position;
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import ContextMenuItemsBuilder from "./ContextMenuItemsBuilder";
|
||||||
|
|
||||||
|
const menus = [
|
||||||
|
{
|
||||||
|
id: 'one',
|
||||||
|
selector: ({ value }) => value === 'one',
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'two',
|
||||||
|
selector: ({ value }) => value === 'two',
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'default',
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const menuBuilder = new ContextMenuItemsBuilder();
|
||||||
|
|
||||||
|
describe('ContextMenuItemsBuilder', () => {
|
||||||
|
test('findMenuDefault', () => {
|
||||||
|
expect(menuBuilder.findMenuDefault(menus, {})).toBe(menus[2]);
|
||||||
|
expect(menuBuilder.findMenuDefault(menus, { value: 'two' })).toBe(menus[1]);
|
||||||
|
expect(menuBuilder.findMenuDefault([], {})).toBeUndefined();
|
||||||
|
expect(menuBuilder.findMenuDefault(undefined, undefined)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,193 @@
|
|||||||
|
import { Types } from '@ohif/ui';
|
||||||
|
import { Menu, SelectorProps, MenuItem, ContextMenuProps } from './types';
|
||||||
|
|
||||||
|
type ContextMenuItem = Types.ContextMenuItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds menu by menu id
|
||||||
|
*
|
||||||
|
* @returns Menu having the menuId
|
||||||
|
*/
|
||||||
|
export function findMenuById(menus: Menu[], menuId?: string): Menu {
|
||||||
|
if (!menuId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return menus.find(menu => menu.id === menuId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default finding menu method. This method will go through
|
||||||
|
* the list of menus until it finds the first one which
|
||||||
|
* has no selector, OR has the selector, when applied to the
|
||||||
|
* check props, return true.
|
||||||
|
* The selectorProps are a set of provided properties which can be
|
||||||
|
* passed into the selector function to determine when to display a menu.
|
||||||
|
* For example, a selector function of:
|
||||||
|
* `({displayset}) => displaySet?.SeriesDescription?.indexOf?.('Left')!==-1
|
||||||
|
* would match series descriptions containing 'Left'.
|
||||||
|
*
|
||||||
|
* @param {Object[]} menus List of menus
|
||||||
|
* @param {*} subProps
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function findMenuDefault(
|
||||||
|
menus: Menu[],
|
||||||
|
subProps: Record<string, unknown>
|
||||||
|
): Menu {
|
||||||
|
if (!menus) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return menus.find(
|
||||||
|
menu => !menu.selector || menu.selector(subProps.selectorProps)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the menu to be used for different scenarios:
|
||||||
|
* This will first look for a subMenu with the specified subMenuId
|
||||||
|
* Next it will look for the first menu whose selector returns true.
|
||||||
|
*
|
||||||
|
* @param menus - List of menus
|
||||||
|
* @param props - root props
|
||||||
|
* @param menuIdFilter - menu id identifier (to be considered on selection)
|
||||||
|
* This is intended to support other types of filtering in the future.
|
||||||
|
*/
|
||||||
|
export function findMenu(
|
||||||
|
menus: Menu[],
|
||||||
|
props?: Types.IProps,
|
||||||
|
menuIdFilter?: string
|
||||||
|
) {
|
||||||
|
const { subMenu } = props;
|
||||||
|
|
||||||
|
function* findMenuIterator() {
|
||||||
|
yield findMenuById(menus, menuIdFilter || subMenu);
|
||||||
|
yield findMenuDefault(menus, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
const findIt = findMenuIterator();
|
||||||
|
|
||||||
|
let current = findIt.next();
|
||||||
|
let menu = current.value;
|
||||||
|
|
||||||
|
while (!current.done) {
|
||||||
|
menu = current.value;
|
||||||
|
|
||||||
|
if (menu) {
|
||||||
|
findIt.return();
|
||||||
|
}
|
||||||
|
current = findIt.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Menu chosen', menu?.id || 'NONE');
|
||||||
|
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the menu from a list of possible menus, based on the actual state of component props and tool data nearby.
|
||||||
|
* This uses the findMenu command above to first find the appropriate
|
||||||
|
* menu, and then it chooses the actual contents of that menu.
|
||||||
|
* A menu item can be optional by implementing the 'selector',
|
||||||
|
* which will be called with the selectorProps, and if it does not return true,
|
||||||
|
* then the item is excluded.
|
||||||
|
*
|
||||||
|
* Other menus can be delegated to by setting the delegating value to
|
||||||
|
* a string id for another menu. That menu's content will replace the
|
||||||
|
* current menu item (only if the item would be included).
|
||||||
|
*
|
||||||
|
* This allows single id menus to be chosen by id, but have varying contents
|
||||||
|
* based on the delegated menus.
|
||||||
|
*
|
||||||
|
* Finally, for each item, the adaptItem call is made. This allows
|
||||||
|
* items to modify themselves before being displayed, such as
|
||||||
|
* incorporating additional information from translation sources.
|
||||||
|
* See the `test-mode` examples for details.
|
||||||
|
*
|
||||||
|
* @param selectorProps
|
||||||
|
* @param {*} event event that originates the context menu
|
||||||
|
* @param {*} menus List of menus
|
||||||
|
* @param {*} menuIdFilter
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getMenuItems(
|
||||||
|
selectorProps: SelectorProps,
|
||||||
|
event: Event,
|
||||||
|
menus: Menu[],
|
||||||
|
menuIdFilter?: string
|
||||||
|
): MenuItem[] | void {
|
||||||
|
// Include both the check props and the ...check props as one is used
|
||||||
|
// by the child menu and the other used by the selector function
|
||||||
|
const subProps = { selectorProps, event };
|
||||||
|
|
||||||
|
const menu = findMenu(menus, subProps, menuIdFilter);
|
||||||
|
|
||||||
|
if (!menu) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!menu.items) {
|
||||||
|
console.warn('Must define items in menu', menu);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let menuItems = [];
|
||||||
|
menu.items.forEach(item => {
|
||||||
|
const { delegating, selector, subMenu } = item;
|
||||||
|
|
||||||
|
if (!selector || selector(selectorProps)) {
|
||||||
|
if (delegating) {
|
||||||
|
menuItems = [
|
||||||
|
...menuItems,
|
||||||
|
...getMenuItems(selectorProps, event, menus, subMenu),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
const toAdd = adaptItem(item, subProps);
|
||||||
|
menuItems.push(toAdd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return menuItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns item adapted to be consumed by ContextMenu component
|
||||||
|
* and then goes through the item to add action behaviour for clicking the item,
|
||||||
|
* making it compatible with the default ContextMenu display.
|
||||||
|
*
|
||||||
|
* @param {Object} item
|
||||||
|
* @param {Object} subProps
|
||||||
|
* @returns a MenuItem that is compatible with the base ContextMenu
|
||||||
|
* This requires having a label and set of actions to be called.
|
||||||
|
*/
|
||||||
|
export function adaptItem(
|
||||||
|
item: MenuItem,
|
||||||
|
subProps: ContextMenuProps
|
||||||
|
): ContextMenuItem {
|
||||||
|
const newItem: ContextMenuItem = {
|
||||||
|
...item,
|
||||||
|
value: subProps.selectorProps?.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (item.actionType === 'ShowSubMenu' && !newItem.iconRight) {
|
||||||
|
newItem.iconRight = 'chevron-menu';
|
||||||
|
}
|
||||||
|
if (!item.action) {
|
||||||
|
newItem.action = (itemRef, componentProps) => {
|
||||||
|
const { event = {} } = componentProps;
|
||||||
|
const { detail = {} } = event;
|
||||||
|
newItem.element = detail.element;
|
||||||
|
|
||||||
|
componentProps.onClose();
|
||||||
|
const action = componentProps[`on${itemRef.actionType || 'Default'}`];
|
||||||
|
if (action) {
|
||||||
|
action.call(componentProps, newItem, itemRef, subProps);
|
||||||
|
} else {
|
||||||
|
console.warn('No action defined for', itemRef);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return newItem;
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
const defaultContextMenu = {
|
||||||
|
id: 'measurementsContextMenu',
|
||||||
|
customizationType: 'ohif.contextMenu',
|
||||||
|
menus: [
|
||||||
|
// Get the items from the UI Customization for the menu name (and have a custom name)
|
||||||
|
{
|
||||||
|
id: 'forExistingMeasurement',
|
||||||
|
selector: ({ nearbyToolData }) => !!nearbyToolData,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Delete measurement',
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
commandName: 'deleteMeasurement',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Add Label',
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
commandName: 'setMeasurementLabel',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defaultContextMenu;
|
||||||
11
extensions/default/src/CustomizeableContextMenu/index.ts
Normal file
11
extensions/default/src/CustomizeableContextMenu/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import ContextMenuController from './ContextMenuController';
|
||||||
|
import ContextMenuItemsBuilder from './ContextMenuItemsBuilder';
|
||||||
|
import defaultContextMenu from './defaultContextMenu';
|
||||||
|
import * as CustomizeableContextMenuTypes from './types';
|
||||||
|
|
||||||
|
export {
|
||||||
|
ContextMenuController,
|
||||||
|
CustomizeableContextMenuTypes,
|
||||||
|
ContextMenuItemsBuilder,
|
||||||
|
defaultContextMenu,
|
||||||
|
};
|
||||||
125
extensions/default/src/CustomizeableContextMenu/types.ts
Normal file
125
extensions/default/src/CustomizeableContextMenu/types.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { Types } from '@ohif/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SelectorProps are properties used to decide whether to select a menu or
|
||||||
|
* menu item for display.
|
||||||
|
* An instance of SelectorProps is provided to the selector functions, which
|
||||||
|
* return true to include the item or false to exclude it.
|
||||||
|
* The point of this is to allow more specific conext menus which hide
|
||||||
|
* non-relevant menu options, optimizing the speed of selection of menus
|
||||||
|
*/
|
||||||
|
export interface SelectorProps {
|
||||||
|
// If the context menu is invoked in the context of a measurement, then it
|
||||||
|
// will contain the nearby tool data.
|
||||||
|
nearbyToolData?: Record<string, unknown>;
|
||||||
|
|
||||||
|
// The tool name for the nearby tool
|
||||||
|
toolName?: string;
|
||||||
|
|
||||||
|
// An annotation UID - this will be present if nearbyToolData is present.
|
||||||
|
uid?: string;
|
||||||
|
|
||||||
|
// If the context menu is invoked on an active viewport, then it will contain
|
||||||
|
// the first display set.
|
||||||
|
displaySet?: Record<string, unknown>;
|
||||||
|
|
||||||
|
// The triggering event - can be used to determine key modifiers
|
||||||
|
event?: Event;
|
||||||
|
|
||||||
|
// Any other properties
|
||||||
|
[propertyName: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of item actually required for the ContextMenu UI display
|
||||||
|
*/
|
||||||
|
export type UIMenuItem = {
|
||||||
|
label: string;
|
||||||
|
// Called when the item is selected
|
||||||
|
action?: (itemRef, componentProps) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A MenuItem is a single line item within a menu, and specifies a selectable
|
||||||
|
* value for the menu.
|
||||||
|
*/
|
||||||
|
export interface MenuItem {
|
||||||
|
id?: string;
|
||||||
|
/** The customization type is used to apply preset values to this item
|
||||||
|
* when registered with the customization service.
|
||||||
|
*/
|
||||||
|
customizationType?: string;
|
||||||
|
|
||||||
|
// The label is the value to show in the menu for this item
|
||||||
|
label?: string;
|
||||||
|
|
||||||
|
// Delegating items are used to include other sub-menus inline within
|
||||||
|
// this menu. That allows sharing part of the menu structure, but also,
|
||||||
|
// more importantly to use a single selector function to include/exclude
|
||||||
|
// and entire section of sub-menu.
|
||||||
|
// See the `siteSelectionSubMenu` within the example `findingsMenu`
|
||||||
|
// for an example
|
||||||
|
delegating?: boolean;
|
||||||
|
|
||||||
|
// A sub-menu is shown when this item is selected or is delegating.
|
||||||
|
// This item gives the name of the sub-menu.
|
||||||
|
subMenu?: string;
|
||||||
|
|
||||||
|
// The selector is used to determine if this menu entry will be shown
|
||||||
|
// or more importantly, if the delegating subMenu will be included.
|
||||||
|
selector?: (props: SelectorProps) => boolean;
|
||||||
|
|
||||||
|
/** Adapts the item by filling in additional properties as requried */
|
||||||
|
adaptItem?: (item: MenuItem, props: ContextMenuProps) => UIMenuItem;
|
||||||
|
|
||||||
|
/** List of commands to run when this item's action is taken. */
|
||||||
|
commands?: Types.Command[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A menu is a list of menu items, plus a selector.
|
||||||
|
* The selector is used to determine whether the menu should be displayed
|
||||||
|
* in a given context. The parameters passed to the selector come from
|
||||||
|
* the 'selectorProps' value in the options, and are intended to be context
|
||||||
|
* specific values containing things like the selected object, the currently
|
||||||
|
* displayed study etc so that the context menu can dynamically choose which
|
||||||
|
* view to show.
|
||||||
|
*/
|
||||||
|
export interface Menu {
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/** The customization type is used to apply preset values to this item
|
||||||
|
* when registered with the customization service.
|
||||||
|
*/
|
||||||
|
customizationType?: string;
|
||||||
|
|
||||||
|
// Choose whether this menu applies.
|
||||||
|
selector?: Types.Predicate;
|
||||||
|
|
||||||
|
items: MenuItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Point = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ContextMenuProps is the top level argument used to invoke the context menu
|
||||||
|
* itself. It contains the menus available for display, as well as the event
|
||||||
|
* and selector props used to decide the menu.
|
||||||
|
*/
|
||||||
|
export type ContextMenuProps = {
|
||||||
|
event?: EventTarget;
|
||||||
|
menuCustomizationId?: string;
|
||||||
|
menuId: string;
|
||||||
|
element?: HTMLElement;
|
||||||
|
|
||||||
|
/** A set of menus to choose from for this context menu */
|
||||||
|
menus: Menu[];
|
||||||
|
|
||||||
|
/** The properties used to decide the menu type */
|
||||||
|
selectorProps: SelectorProps;
|
||||||
|
|
||||||
|
defaultPointsPosition?: [number, number] | [];
|
||||||
|
};
|
||||||
@ -138,7 +138,9 @@ function createDicomLocalApi(dicomLocalConfig) {
|
|||||||
study.series.forEach(aSeries => {
|
study.series.forEach(aSeries => {
|
||||||
const { SeriesInstanceUID } = aSeries;
|
const { SeriesInstanceUID } = aSeries;
|
||||||
|
|
||||||
aSeries.instances.forEach(instance => {
|
const isMultiframe = aSeries.instances[0].NumberOfFrames > 1;
|
||||||
|
|
||||||
|
aSeries.instances.forEach((instance, index) => {
|
||||||
const {
|
const {
|
||||||
url: imageId,
|
url: imageId,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
@ -153,6 +155,7 @@ function createDicomLocalApi(dicomLocalConfig) {
|
|||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
|
frameIndex: isMultiframe ? index : 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -185,7 +188,7 @@ function createDicomLocalApi(dicomLocalConfig) {
|
|||||||
displaySet.images.forEach(instance => {
|
displaySet.images.forEach(instance => {
|
||||||
const NumberOfFrames = instance.NumberOfFrames;
|
const NumberOfFrames = instance.NumberOfFrames;
|
||||||
if (NumberOfFrames > 1) {
|
if (NumberOfFrames > 1) {
|
||||||
for (let i = 0; i < NumberOfFrames; i++) {
|
for (let i = 1; i <= NumberOfFrames; i++) {
|
||||||
const imageId = this.getImageIdsForInstance({
|
const imageId = this.getImageIdsForInstance({
|
||||||
instance,
|
instance,
|
||||||
frame: i,
|
frame: i,
|
||||||
|
|||||||
@ -104,7 +104,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
|||||||
query: {
|
query: {
|
||||||
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;
|
||||||
@ -129,7 +129,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;
|
||||||
|
|||||||
@ -218,6 +218,7 @@ export default function PanelMeasurementTable({
|
|||||||
>
|
>
|
||||||
<MeasurementTable
|
<MeasurementTable
|
||||||
title="Measurements"
|
title="Measurements"
|
||||||
|
servicesManager={servicesManager}
|
||||||
data={displayMeasurements}
|
data={displayMeasurements}
|
||||||
onClick={jumpToImage}
|
onClick={jumpToImage}
|
||||||
onEdit={onMeasurementItemEditHandler}
|
onEdit={onMeasurementItemEditHandler}
|
||||||
@ -248,14 +249,46 @@ function _getMappedMeasurements(measurementService) {
|
|||||||
return mappedMeasurements;
|
return mappedMeasurements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map the measurements to the display text.
|
||||||
|
* Adds finding and site inforamtion to the displayText and/or label,
|
||||||
|
* and provides as 'displayText' and 'label', while providing the original
|
||||||
|
* values as baseDisplayText and baseLabel
|
||||||
|
*/
|
||||||
function _mapMeasurementToDisplay(measurement, index, types) {
|
function _mapMeasurementToDisplay(measurement, index, types) {
|
||||||
const { displayText, uid, label, type, selected } = measurement;
|
const {
|
||||||
|
displayText: baseDisplayText,
|
||||||
|
uid,
|
||||||
|
label: baseLabel,
|
||||||
|
type,
|
||||||
|
selected,
|
||||||
|
findingSites,
|
||||||
|
finding,
|
||||||
|
} = measurement;
|
||||||
|
|
||||||
|
const firstSite = findingSites?.[0];
|
||||||
|
const label = baseLabel || finding?.text || firstSite?.text || '(empty)';
|
||||||
|
let displayText = baseDisplayText || [];
|
||||||
|
if (findingSites) {
|
||||||
|
const siteText = [];
|
||||||
|
findingSites.forEach(site => {
|
||||||
|
if (site?.text !== label) siteText.push(site.text);
|
||||||
|
});
|
||||||
|
displayText = [...siteText, ...displayText];
|
||||||
|
}
|
||||||
|
if (finding && finding?.text !== label) {
|
||||||
|
displayText = [finding.text, ...displayText];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uid,
|
uid,
|
||||||
label: label || '(empty)',
|
label,
|
||||||
|
baseLabel,
|
||||||
measurementType: type,
|
measurementType: type,
|
||||||
displayText: displayText || [],
|
displayText,
|
||||||
|
baseDisplayText,
|
||||||
isActive: selected,
|
isActive: selected,
|
||||||
|
finding,
|
||||||
|
findingSites,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,7 +51,7 @@ function PanelStudyBrowser({
|
|||||||
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.',
|
||||||
type: 'info',
|
type: 'info',
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
});
|
});
|
||||||
@ -303,6 +303,7 @@ function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
|
|||||||
seriesDate: ds.SeriesDate,
|
seriesDate: ds.SeriesDate,
|
||||||
seriesTime: ds.SeriesTime,
|
seriesTime: ds.SeriesTime,
|
||||||
numInstances: ds.numImageFrames,
|
numInstances: ds.numImageFrames,
|
||||||
|
countIcon: ds.countIcon,
|
||||||
StudyInstanceUID: ds.StudyInstanceUID,
|
StudyInstanceUID: ds.StudyInstanceUID,
|
||||||
componentType,
|
componentType,
|
||||||
imageSrc,
|
imageSrc,
|
||||||
|
|||||||
@ -1,10 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import { LayoutSelector as OHIFLayoutSelector, ToolbarButton } from '@ohif/ui';
|
||||||
LayoutSelector as OHIFLayoutSelector,
|
|
||||||
ToolbarButton,
|
|
||||||
useViewportGrid,
|
|
||||||
} from '@ohif/ui';
|
|
||||||
|
|
||||||
import { ServicesManager } from '@ohif/core';
|
import { ServicesManager } from '@ohif/core';
|
||||||
|
|
||||||
@ -16,8 +12,6 @@ function LayoutSelector({
|
|||||||
...rest
|
...rest
|
||||||
}) {
|
}) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [disableSelector, setDisableSelector] = useState(false);
|
|
||||||
const [viewportGridState, viewportGridService] = useViewportGrid();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hangingProtocolService,
|
hangingProtocolService,
|
||||||
@ -50,43 +44,19 @@ function LayoutSelector({
|
|||||||
};
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
/* Reset to default layout when component unmounts */
|
|
||||||
return () => {
|
|
||||||
viewportGridService.setLayout({ numCols: 1, numRows: 1 });
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onInteractionHandler = () => setIsOpen(!isOpen);
|
const onInteractionHandler = () => setIsOpen(!isOpen);
|
||||||
const DropdownContent = isOpen ? OHIFLayoutSelector : null;
|
const DropdownContent = isOpen ? OHIFLayoutSelector : null;
|
||||||
|
|
||||||
const onSelectionHandler = ({ numRows, numCols }) => {
|
const onSelectionHandler = props => {
|
||||||
// TODO Introduce a service to persist the state of the current hanging protocol/app.
|
toolbarService.recordInteraction({
|
||||||
|
interactionType: 'action',
|
||||||
// TODO Here the layout change will amount to a change of hanging protocol as specified by the extension for this layout selector tool
|
commands: [
|
||||||
// followed by the change of the grid itself.
|
{
|
||||||
if (hangingProtocolService.getActiveProtocol().protocol.id === 'mpr') {
|
commandName: 'setViewportGridLayout',
|
||||||
toolbarService.recordInteraction({
|
commandOptions: { ...props },
|
||||||
groupId: 'MPR',
|
context: 'DEFAULT',
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -107,7 +77,7 @@ function LayoutSelector({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
isActive={disableSelector ? false : isOpen}
|
isActive={isOpen}
|
||||||
type="toggle"
|
type="toggle"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -14,7 +14,12 @@ import {
|
|||||||
LoadingIndicatorProgress,
|
LoadingIndicatorProgress,
|
||||||
} from '@ohif/ui';
|
} from '@ohif/ui';
|
||||||
import i18n from '@ohif/i18n';
|
import i18n from '@ohif/i18n';
|
||||||
import { hotkeys } from '@ohif/core';
|
import {
|
||||||
|
ServicesManager,
|
||||||
|
HangingProtocolService,
|
||||||
|
hotkeys,
|
||||||
|
CommandsManager,
|
||||||
|
} from '@ohif/core';
|
||||||
import { useAppConfig } from '@state';
|
import { useAppConfig } from '@state';
|
||||||
import Toolbar from '../Toolbar/Toolbar';
|
import Toolbar from '../Toolbar/Toolbar';
|
||||||
|
|
||||||
@ -33,7 +38,7 @@ function ViewerLayout({
|
|||||||
rightPanels = [],
|
rightPanels = [],
|
||||||
leftPanelDefaultClosed = false,
|
leftPanelDefaultClosed = false,
|
||||||
rightPanelDefaultClosed = false,
|
rightPanelDefaultClosed = false,
|
||||||
}) {
|
}): React.FunctionComponent {
|
||||||
const [appConfig] = useAppConfig();
|
const [appConfig] = useAppConfig();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -169,15 +174,13 @@ function ViewerLayout({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { unsubscribe } = hangingProtocolService.subscribe(
|
const { unsubscribe } = hangingProtocolService.subscribe(
|
||||||
hangingProtocolService.EVENTS.HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT,
|
HangingProtocolService.EVENTS.PROTOCOL_CHANGED,
|
||||||
|
|
||||||
// Todo: right now to set the loading indicator to false, we need to wait for the
|
// Todo: right now to set the loading indicator to false, we need to wait for the
|
||||||
// hangingProtocolService to finish applying the viewport matching to each viewport,
|
// hangingProtocolService to finish applying the viewport matching to each viewport,
|
||||||
// however, this might not be the only approach to set the loading indicator to false. we need to explore this further.
|
// however, this might not be the only approach to set the loading indicator to false. we need to explore this further.
|
||||||
({ progress }) => {
|
() => {
|
||||||
if (progress === 100) {
|
setShowLoadingIndicator(false);
|
||||||
setShowLoadingIndicator(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -265,7 +268,8 @@ ViewerLayout.propTypes = {
|
|||||||
extensionManager: PropTypes.shape({
|
extensionManager: PropTypes.shape({
|
||||||
getModuleEntry: PropTypes.func.isRequired,
|
getModuleEntry: PropTypes.func.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
commandsManager: PropTypes.object,
|
commandsManager: PropTypes.instanceOf(CommandsManager),
|
||||||
|
servicesManager: PropTypes.instanceOf(ServicesManager),
|
||||||
// From modes
|
// From modes
|
||||||
leftPanels: PropTypes.array,
|
leftPanels: PropTypes.array,
|
||||||
rightPanels: PropTypes.array,
|
rightPanels: PropTypes.array,
|
||||||
|
|||||||
@ -1,97 +0,0 @@
|
|||||||
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
const commandsModule = ({ servicesManager, commandsManager }) => {
|
|
||||||
const {
|
|
||||||
measurementService,
|
|
||||||
hangingProtocolService,
|
|
||||||
uiNotificationService,
|
|
||||||
viewportGridService,
|
|
||||||
displaySetService,
|
|
||||||
} = servicesManager.services;
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
displayNotification: ({ text, title, type }) => {
|
|
||||||
uiNotificationService.show({
|
|
||||||
title: title,
|
|
||||||
message: text,
|
|
||||||
type: type,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
clearMeasurements: () => {
|
|
||||||
measurementService.clear();
|
|
||||||
},
|
|
||||||
nextStage: () => {
|
|
||||||
// next stage in hanging protocols
|
|
||||||
hangingProtocolService.nextProtocolStage();
|
|
||||||
},
|
|
||||||
previousStage: () => {
|
|
||||||
hangingProtocolService.previousProtocolStage();
|
|
||||||
},
|
|
||||||
openDICOMTagViewer() {
|
|
||||||
const { activeViewportIndex, viewports } = viewportGridService.getState();
|
|
||||||
const activeViewportSpecificData = viewports[activeViewportIndex];
|
|
||||||
const { displaySetInstanceUIDs } = activeViewportSpecificData;
|
|
||||||
|
|
||||||
const displaySets = displaySetService.activeDisplaySets;
|
|
||||||
const { uiModalService } = servicesManager.services;
|
|
||||||
|
|
||||||
const displaySetInstanceUID = displaySetInstanceUIDs[0];
|
|
||||||
uiModalService.show({
|
|
||||||
content: DicomTagBrowser,
|
|
||||||
contentProps: {
|
|
||||||
displaySets,
|
|
||||||
displaySetInstanceUID,
|
|
||||||
onClose: uiModalService.hide,
|
|
||||||
},
|
|
||||||
title: 'DICOM Tag Browser',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle viewport overlay (the information panel shown on the four corners
|
|
||||||
* of the viewport)
|
|
||||||
* @see ViewportOverlay and CustomizableViewportOverlay components
|
|
||||||
*/
|
|
||||||
toggleOverlays: () => {
|
|
||||||
const overlays = document.getElementsByClassName('viewport-overlay');
|
|
||||||
for (let i = 0; i < overlays.length; i++) {
|
|
||||||
overlays.item(i).classList.toggle('hidden');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const definitions = {
|
|
||||||
clearMeasurements: {
|
|
||||||
commandFn: actions.clearMeasurements,
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
displayNotification: {
|
|
||||||
commandFn: actions.displayNotification,
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
nextStage: {
|
|
||||||
commandFn: actions.nextStage,
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
previousStage: {
|
|
||||||
commandFn: actions.previousStage,
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
openDICOMTagViewer: {
|
|
||||||
commandFn: actions.openDICOMTagViewer,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
actions,
|
|
||||||
definitions,
|
|
||||||
defaultContext: 'DEFAULT',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default commandsModule;
|
|
||||||
575
extensions/default/src/commandsModule.ts
Normal file
575
extensions/default/src/commandsModule.ts
Normal file
@ -0,0 +1,575 @@
|
|||||||
|
import { ServicesManager, utils } from '@ohif/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ContextMenuController,
|
||||||
|
defaultContextMenu,
|
||||||
|
} from './CustomizeableContextMenu';
|
||||||
|
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
|
||||||
|
import reuseCachedLayouts from './utils/reuseCachedLayouts';
|
||||||
|
import findViewportsByPosition, {
|
||||||
|
findOrCreateViewport as layoutFindOrCreate,
|
||||||
|
} from './findViewportsByPosition';
|
||||||
|
|
||||||
|
import { ContextMenuProps } from './CustomizeableContextMenu/types';
|
||||||
|
|
||||||
|
const { subscribeToNextViewportGridChange } = utils;
|
||||||
|
|
||||||
|
export type HangingProtocolParams = {
|
||||||
|
protocolId?: string;
|
||||||
|
stageIndex?: number;
|
||||||
|
activeStudyUID?: string;
|
||||||
|
stageId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a command is a hanging protocol one.
|
||||||
|
* For now, just use the two hanging protocol commands that are in this
|
||||||
|
* commands module, but if others get added elsewhere this may need enhancing.
|
||||||
|
*/
|
||||||
|
const isHangingProtocolCommand = command =>
|
||||||
|
command &&
|
||||||
|
(command.commandName === 'setHangingProtocol' ||
|
||||||
|
command.commandName === 'toggleHangingProtocol');
|
||||||
|
|
||||||
|
const commandsModule = ({
|
||||||
|
servicesManager,
|
||||||
|
commandsManager,
|
||||||
|
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
|
||||||
|
const {
|
||||||
|
customizationService,
|
||||||
|
measurementService,
|
||||||
|
hangingProtocolService,
|
||||||
|
uiNotificationService,
|
||||||
|
viewportGridService,
|
||||||
|
displaySetService,
|
||||||
|
stateSyncService,
|
||||||
|
toolbarService,
|
||||||
|
} = (servicesManager as ServicesManager).services;
|
||||||
|
|
||||||
|
// Define a context menu controller for use with any context menus
|
||||||
|
const contextMenuController = new ContextMenuController(
|
||||||
|
servicesManager,
|
||||||
|
commandsManager
|
||||||
|
);
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
/**
|
||||||
|
* Show the context menu.
|
||||||
|
* @param options.menuId defines the menu name to lookup, from customizationService
|
||||||
|
* @param options.defaultMenu contains the default menu set to use
|
||||||
|
* @param options.element is the element to show the menu within
|
||||||
|
* @param options.event is the event that caused the context menu
|
||||||
|
* @param options.selectorProps is the set of selection properties to use
|
||||||
|
*/
|
||||||
|
showContextMenu: (options: ContextMenuProps) => {
|
||||||
|
const {
|
||||||
|
menuCustomizationId,
|
||||||
|
element,
|
||||||
|
event,
|
||||||
|
selectorProps,
|
||||||
|
defaultPointsPosition = [],
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const optionsToUse = { ...options };
|
||||||
|
|
||||||
|
if (menuCustomizationId) {
|
||||||
|
Object.assign(
|
||||||
|
optionsToUse,
|
||||||
|
customizationService.get(menuCustomizationId, defaultContextMenu)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - make the selectorProps richer by including the study metadata and display set.
|
||||||
|
const { protocol, stage } = hangingProtocolService.getActiveProtocol();
|
||||||
|
optionsToUse.selectorProps = {
|
||||||
|
event,
|
||||||
|
protocol,
|
||||||
|
stage,
|
||||||
|
...selectorProps,
|
||||||
|
};
|
||||||
|
|
||||||
|
contextMenuController.showContextMenu(
|
||||||
|
optionsToUse,
|
||||||
|
element,
|
||||||
|
defaultPointsPosition
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Close a context menu currently displayed */
|
||||||
|
closeContextMenu: () => {
|
||||||
|
contextMenuController.closeContextMenu();
|
||||||
|
},
|
||||||
|
|
||||||
|
displayNotification: ({ text, title, type }) => {
|
||||||
|
uiNotificationService.show({
|
||||||
|
title: title,
|
||||||
|
message: text,
|
||||||
|
type: type,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
clearMeasurements: () => {
|
||||||
|
measurementService.clear();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles off all tools which contain a commandName of setHangingProtocol
|
||||||
|
* or toggleHangingProtocol, and which match/don't match the protocol id/stage
|
||||||
|
*/
|
||||||
|
toggleHpTools: () => {
|
||||||
|
const {
|
||||||
|
protocol,
|
||||||
|
stageIndex: toggleStageIndex,
|
||||||
|
stage,
|
||||||
|
} = hangingProtocolService.getActiveProtocol();
|
||||||
|
const enableListener = button => {
|
||||||
|
if (!button.id) return;
|
||||||
|
const { commands, items } = button.props || button;
|
||||||
|
if (items) {
|
||||||
|
items.forEach(enableListener);
|
||||||
|
}
|
||||||
|
const hpCommand = commands?.find?.(isHangingProtocolCommand);
|
||||||
|
if (!hpCommand) return;
|
||||||
|
const { protocolId, stageIndex, stageId } = hpCommand.commandOptions;
|
||||||
|
const isActive =
|
||||||
|
(!protocolId || protocolId === protocol.id) &&
|
||||||
|
(stageIndex === undefined || stageIndex === toggleStageIndex) &&
|
||||||
|
(!stageId || stageId === stage.id);
|
||||||
|
toolbarService.setActive(button.id, isActive);
|
||||||
|
};
|
||||||
|
Object.values(toolbarService.getButtons()).forEach(enableListener);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the specified protocol
|
||||||
|
* 1. Records any existing state using the viewport grid service
|
||||||
|
* 2. Finds the destination state - this can be one of:
|
||||||
|
* a. The specified protocol stage
|
||||||
|
* b. An alternate (toggled or restored) protocol stage
|
||||||
|
* c. A restored custom layout
|
||||||
|
* 3. Finds the parameters for the specified state
|
||||||
|
* a. Gets the displaySetSelectorMap
|
||||||
|
* b. Gets the map by position
|
||||||
|
* c. Gets any toggle mapping to map position to/from current view
|
||||||
|
* 4. If restore, then sets layout
|
||||||
|
* a. Maps viewport position by currently displayed viewport map id
|
||||||
|
* b. Uses toggle information to map display set id
|
||||||
|
* 5. Else applies the hanging protocol
|
||||||
|
* a. HP Service is provided displaySetSelectorMap
|
||||||
|
* b. HP Service will throw an exception if it isn't applicable
|
||||||
|
* @param options - contains information on the HP to apply
|
||||||
|
* @param options.activeStudyUID - the updated study to apply the HP to
|
||||||
|
* @param options.protocolId - the protocol ID to change to
|
||||||
|
* @param options.stageId - the stageId to apply
|
||||||
|
* @param options.stageIndex - the index of the stage to go to.
|
||||||
|
* @param options.reset - flag to indicate if the HP should be reset to its original and not restored to a previous state
|
||||||
|
*/
|
||||||
|
setHangingProtocol: ({
|
||||||
|
activeStudyUID = '',
|
||||||
|
protocolId,
|
||||||
|
stageId,
|
||||||
|
stageIndex,
|
||||||
|
reset = false,
|
||||||
|
}: HangingProtocolParams): boolean => {
|
||||||
|
try {
|
||||||
|
// Stores in the state the reuseID to displaySetUID mapping
|
||||||
|
// Pass in viewportId for the active viewport. This item will get set as
|
||||||
|
// the activeViewportId
|
||||||
|
const state = viewportGridService.getState();
|
||||||
|
const hpInfo = hangingProtocolService.getState();
|
||||||
|
const {
|
||||||
|
protocol: oldProtocol,
|
||||||
|
} = hangingProtocolService.getActiveProtocol();
|
||||||
|
const stateSyncReduce = reuseCachedLayouts(
|
||||||
|
state,
|
||||||
|
hangingProtocolService,
|
||||||
|
stateSyncService
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
hangingProtocolStageIndexMap,
|
||||||
|
viewportGridStore,
|
||||||
|
displaySetSelectorMap,
|
||||||
|
} = stateSyncReduce;
|
||||||
|
|
||||||
|
if (!protocolId) {
|
||||||
|
// Re-use the previous protocol id, and optionally stage
|
||||||
|
protocolId = hpInfo.protocolId;
|
||||||
|
if (stageId === undefined && stageIndex === undefined) {
|
||||||
|
stageIndex = hpInfo.stageIndex;
|
||||||
|
}
|
||||||
|
} else if (stageIndex === undefined && stageId === undefined) {
|
||||||
|
// Re-set the same stage as was previously used
|
||||||
|
const hangingId = `${activeStudyUID ||
|
||||||
|
hpInfo.activeStudyUID}:${protocolId}`;
|
||||||
|
stageIndex = hangingProtocolStageIndexMap[hangingId]?.stageIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStageIdx =
|
||||||
|
stageIndex ??
|
||||||
|
hangingProtocolService.getStageIndex(protocolId, {
|
||||||
|
stageId,
|
||||||
|
stageIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (activeStudyUID) {
|
||||||
|
hangingProtocolService.setActiveStudyUID(activeStudyUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedHanging = `${
|
||||||
|
hangingProtocolService.getState().activeStudyUID
|
||||||
|
}:${protocolId}:${useStageIdx || 0}`;
|
||||||
|
|
||||||
|
const restoreProtocol = !reset && viewportGridStore[storedHanging];
|
||||||
|
|
||||||
|
if (
|
||||||
|
protocolId === hpInfo.protocolId &&
|
||||||
|
useStageIdx === hpInfo.stageIndex &&
|
||||||
|
!activeStudyUID
|
||||||
|
) {
|
||||||
|
// Clear the HP setting to reset them
|
||||||
|
hangingProtocolService.setProtocol(protocolId, {
|
||||||
|
stageId,
|
||||||
|
stageIndex: useStageIdx,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
hangingProtocolService.setProtocol(protocolId, {
|
||||||
|
displaySetSelectorMap,
|
||||||
|
stageId,
|
||||||
|
stageIndex: useStageIdx,
|
||||||
|
restoreProtocol,
|
||||||
|
});
|
||||||
|
if (restoreProtocol) {
|
||||||
|
viewportGridService.set(viewportGridStore[storedHanging]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Do this after successfully applying the update
|
||||||
|
stateSyncService.store(stateSyncReduce);
|
||||||
|
// This is a default action applied
|
||||||
|
actions.toggleHpTools(hangingProtocolService.getActiveProtocol());
|
||||||
|
// Send the notification about updating the state
|
||||||
|
if (protocolId !== hpInfo.protocolId) {
|
||||||
|
const { protocol } = hangingProtocolService.getActiveProtocol();
|
||||||
|
// The old protocol callbacks are used for turning off things
|
||||||
|
// like crosshairs when moving to the new HP
|
||||||
|
commandsManager.run(oldProtocol.callbacks?.onProtocolExit);
|
||||||
|
// The new protocol callback is used for things like
|
||||||
|
// activating modes etc.
|
||||||
|
commandsManager.run(protocol.callbacks?.onProtocolEnter);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
actions.toggleHpTools(hangingProtocolService.getActiveProtocol());
|
||||||
|
uiNotificationService.show({
|
||||||
|
title: 'Apply Hanging Protocol',
|
||||||
|
message: 'The hanging protocol could not be applied.',
|
||||||
|
type: 'error',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleHangingProtocol: ({
|
||||||
|
protocolId,
|
||||||
|
stageIndex,
|
||||||
|
}: HangingProtocolParams): boolean => {
|
||||||
|
const {
|
||||||
|
protocol,
|
||||||
|
stageIndex: desiredStageIndex,
|
||||||
|
activeStudy,
|
||||||
|
} = hangingProtocolService.getActiveProtocol();
|
||||||
|
const { toggleHangingProtocol } = stateSyncService.getState();
|
||||||
|
const storedHanging = `${
|
||||||
|
activeStudy.StudyInstanceUID
|
||||||
|
}:${protocolId}:${stageIndex | 0}`;
|
||||||
|
if (
|
||||||
|
protocol.id === protocolId &&
|
||||||
|
(stageIndex === undefined || stageIndex === desiredStageIndex)
|
||||||
|
) {
|
||||||
|
// Toggling off - restore to previous state
|
||||||
|
const previousState = toggleHangingProtocol[storedHanging] || {
|
||||||
|
protocolId: 'default',
|
||||||
|
};
|
||||||
|
return actions.setHangingProtocol(previousState);
|
||||||
|
} else {
|
||||||
|
stateSyncService.store({
|
||||||
|
toggleHangingProtocol: {
|
||||||
|
...toggleHangingProtocol,
|
||||||
|
[storedHanging]: {
|
||||||
|
protocolId: protocol.id,
|
||||||
|
stageIndex: desiredStageIndex,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return actions.setHangingProtocol({
|
||||||
|
protocolId,
|
||||||
|
stageIndex,
|
||||||
|
reset: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deltaStage: ({ direction }) => {
|
||||||
|
const {
|
||||||
|
protocolId,
|
||||||
|
stageIndex: oldStageIndex,
|
||||||
|
} = hangingProtocolService.getState();
|
||||||
|
const { protocol } = hangingProtocolService.getActiveProtocol();
|
||||||
|
for (
|
||||||
|
let stageIndex = oldStageIndex + direction;
|
||||||
|
stageIndex >= 0 && stageIndex < protocol.stages.length;
|
||||||
|
stageIndex += direction
|
||||||
|
) {
|
||||||
|
if (protocol.stages[stageIndex].status !== 'disabled') {
|
||||||
|
return actions.setHangingProtocol({
|
||||||
|
protocolId,
|
||||||
|
stageIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uiNotificationService.show({
|
||||||
|
title: 'Change Stage',
|
||||||
|
message: 'The hanging protocol has no more applicable stages',
|
||||||
|
type: 'info',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the viewport grid layout in terms of the MxN layout.
|
||||||
|
*/
|
||||||
|
setViewportGridLayout: ({ numRows, numCols }) => {
|
||||||
|
const { protocol } = hangingProtocolService.getActiveProtocol();
|
||||||
|
const onLayoutChange = protocol.callbacks?.onLayoutChange;
|
||||||
|
if (commandsManager.run(onLayoutChange, { numRows, numCols }) === false) {
|
||||||
|
console.log(
|
||||||
|
'setViewportGridLayout running',
|
||||||
|
onLayoutChange,
|
||||||
|
numRows,
|
||||||
|
numCols
|
||||||
|
);
|
||||||
|
// Don't apply the layout if the run command returns false
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const completeLayout = () => {
|
||||||
|
const state = viewportGridService.getState();
|
||||||
|
const stateReduce = findViewportsByPosition(
|
||||||
|
state,
|
||||||
|
{ numRows, numCols },
|
||||||
|
stateSyncService
|
||||||
|
);
|
||||||
|
const findOrCreateViewport = layoutFindOrCreate.bind(
|
||||||
|
null,
|
||||||
|
hangingProtocolService,
|
||||||
|
stateReduce.viewportsByPosition
|
||||||
|
);
|
||||||
|
|
||||||
|
viewportGridService.setLayout({
|
||||||
|
numRows,
|
||||||
|
numCols,
|
||||||
|
findOrCreateViewport,
|
||||||
|
});
|
||||||
|
stateSyncService.store(stateReduce);
|
||||||
|
};
|
||||||
|
// Need to finish any work in the callback
|
||||||
|
window.setTimeout(completeLayout, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleOneUp() {
|
||||||
|
const viewportGridState = viewportGridService.getState();
|
||||||
|
const { activeViewportIndex, viewports, layout } = viewportGridState;
|
||||||
|
const {
|
||||||
|
displaySetInstanceUIDs,
|
||||||
|
displaySetOptions,
|
||||||
|
viewportOptions,
|
||||||
|
} = viewports[activeViewportIndex];
|
||||||
|
|
||||||
|
if (layout.numCols === 1 && layout.numRows === 1) {
|
||||||
|
// The viewer is in one-up. Check if there is a state to restore/toggle back to.
|
||||||
|
const { toggleOneUpViewportGridStore } = stateSyncService.getState();
|
||||||
|
|
||||||
|
if (!toggleOneUpViewportGridStore.layout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// There is a state to toggle back to. The viewport that was
|
||||||
|
// originally toggled to one up was the former active viewport.
|
||||||
|
const viewportIndexToUpdate =
|
||||||
|
toggleOneUpViewportGridStore.activeViewportIndex;
|
||||||
|
|
||||||
|
// Determine which viewports need to be updated. This is particularly
|
||||||
|
// important when MPR is toggled to one up and a different reconstructable
|
||||||
|
// is swapped in. Note that currently HangingProtocolService.getViewportsRequireUpdate
|
||||||
|
// does not support viewport with multiple display sets.
|
||||||
|
const updatedViewports =
|
||||||
|
displaySetInstanceUIDs.length > 1
|
||||||
|
? []
|
||||||
|
: displaySetInstanceUIDs
|
||||||
|
.map(displaySetInstanceUID =>
|
||||||
|
hangingProtocolService.getViewportsRequireUpdate(
|
||||||
|
viewportIndexToUpdate,
|
||||||
|
displaySetInstanceUID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
// This findOrCreateViewport returns either one of the updatedViewports
|
||||||
|
// returned from the HP service OR if there is not one from the HP service then
|
||||||
|
// simply returns what was in the previous state.
|
||||||
|
const findOrCreateViewport = (viewportIndex: number) => {
|
||||||
|
const viewport = updatedViewports.find(
|
||||||
|
viewport => viewport.viewportIndex === viewportIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
return viewport
|
||||||
|
? { viewportOptions, displaySetOptions, ...viewport }
|
||||||
|
: toggleOneUpViewportGridStore.viewports[viewportIndex];
|
||||||
|
};
|
||||||
|
|
||||||
|
const layoutOptions = viewportGridService.getLayoutOptionsFromState(
|
||||||
|
toggleOneUpViewportGridStore
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restore the previous layout including the active viewport.
|
||||||
|
viewportGridService.setLayout({
|
||||||
|
numRows: toggleOneUpViewportGridStore.layout.numRows,
|
||||||
|
numCols: toggleOneUpViewportGridStore.layout.numCols,
|
||||||
|
activeViewportIndex: viewportIndexToUpdate,
|
||||||
|
layoutOptions,
|
||||||
|
findOrCreateViewport,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// We are not in one-up, so toggle to one up.
|
||||||
|
|
||||||
|
// Store the current viewport grid state so we can toggle it back later.
|
||||||
|
stateSyncService.store({
|
||||||
|
toggleOneUpViewportGridStore: viewportGridState,
|
||||||
|
});
|
||||||
|
|
||||||
|
// This findOrCreateViewport only return one viewport - the active
|
||||||
|
// one being toggled to one up.
|
||||||
|
const findOrCreateViewport = () => {
|
||||||
|
return {
|
||||||
|
displaySetInstanceUIDs,
|
||||||
|
displaySetOptions,
|
||||||
|
viewportOptions,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set the layout to be 1x1/one-up.
|
||||||
|
viewportGridService.setLayout({
|
||||||
|
numRows: 1,
|
||||||
|
numCols: 1,
|
||||||
|
findOrCreateViewport,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to ANY (i.e. manual and hanging protocol) layout changes so that
|
||||||
|
// any grid layout state to toggle to from one up is cleared. This is performed on
|
||||||
|
// a timeout to avoid clearing the state for the actual to one up change.
|
||||||
|
// Whenever the next layout change event is fired, the subscriptions are unsubscribed.
|
||||||
|
const clearToggleOneUpViewportGridStore = () => {
|
||||||
|
const toggleOneUpViewportGridStore = {};
|
||||||
|
stateSyncService.store({
|
||||||
|
toggleOneUpViewportGridStore,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
subscribeToNextViewportGridChange(
|
||||||
|
viewportGridService,
|
||||||
|
clearToggleOneUpViewportGridStore
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openDICOMTagViewer() {
|
||||||
|
const { activeViewportIndex, viewports } = viewportGridService.getState();
|
||||||
|
const activeViewportSpecificData = viewports[activeViewportIndex];
|
||||||
|
const { displaySetInstanceUIDs } = activeViewportSpecificData;
|
||||||
|
|
||||||
|
const displaySets = displaySetService.activeDisplaySets;
|
||||||
|
const { UIModalService } = servicesManager.services;
|
||||||
|
|
||||||
|
const displaySetInstanceUID = displaySetInstanceUIDs[0];
|
||||||
|
UIModalService.show({
|
||||||
|
content: DicomTagBrowser,
|
||||||
|
contentProps: {
|
||||||
|
displaySets,
|
||||||
|
displaySetInstanceUID,
|
||||||
|
onClose: UIModalService.hide,
|
||||||
|
},
|
||||||
|
title: 'DICOM Tag Browser',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle viewport overlay (the information panel shown on the four corners
|
||||||
|
* of the viewport)
|
||||||
|
* @see ViewportOverlay and CustomizableViewportOverlay components
|
||||||
|
*/
|
||||||
|
toggleOverlays: () => {
|
||||||
|
const overlays = document.getElementsByClassName('viewport-overlay');
|
||||||
|
for (let i = 0; i < overlays.length; i++) {
|
||||||
|
overlays.item(i).classList.toggle('hidden');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const definitions = {
|
||||||
|
showContextMenu: {
|
||||||
|
commandFn: actions.showContextMenu,
|
||||||
|
},
|
||||||
|
closeContextMenu: {
|
||||||
|
commandFn: actions.closeContextMenu,
|
||||||
|
},
|
||||||
|
clearMeasurements: {
|
||||||
|
commandFn: actions.clearMeasurements,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
displayNotification: {
|
||||||
|
commandFn: actions.displayNotification,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
setHangingProtocol: {
|
||||||
|
commandFn: actions.setHangingProtocol,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
toggleHangingProtocol: {
|
||||||
|
commandFn: actions.toggleHangingProtocol,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
nextStage: {
|
||||||
|
commandFn: actions.deltaStage,
|
||||||
|
storeContexts: [],
|
||||||
|
options: { direction: 1 },
|
||||||
|
},
|
||||||
|
previousStage: {
|
||||||
|
commandFn: actions.deltaStage,
|
||||||
|
storeContexts: [],
|
||||||
|
options: { direction: -1 },
|
||||||
|
},
|
||||||
|
setViewportGridLayout: {
|
||||||
|
commandFn: actions.setViewportGridLayout,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
toggleOneUp: {
|
||||||
|
commandFn: actions.toggleOneUp,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
openDICOMTagViewer: {
|
||||||
|
commandFn: actions.openDICOMTagViewer,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
actions,
|
||||||
|
definitions,
|
||||||
|
defaultContext: 'DEFAULT',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default commandsModule;
|
||||||
106
extensions/default/src/findViewportsByPosition.ts
Normal file
106
extensions/default/src/findViewportsByPosition.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { StateSyncService, Types } from '@ohif/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This find or create viewport is paired with the reduce results from
|
||||||
|
* below, and the action of this viewport is to look for previously filled
|
||||||
|
* viewports, and to re-use by position id. If there is no filled viewport,
|
||||||
|
* then one can be re-used from the display set if it isn't going to be displayed.
|
||||||
|
* @param hangingProtocolService - bound parameter supplied before using this
|
||||||
|
* @param viewportsByPosition - bound parameter supplied before using this
|
||||||
|
* @param viewportIndex - the index to retrieve
|
||||||
|
* @param positionId - the current position on screen to retrieve
|
||||||
|
* @param options - the set of options used, so that subsequent calls can
|
||||||
|
* store state that is reset by the setLayout.
|
||||||
|
* This class uses the options to store the already viewed
|
||||||
|
* display sets, filling it initially with the pre-existing viewports.
|
||||||
|
*/
|
||||||
|
export const findOrCreateViewport = (
|
||||||
|
hangingProtocolService,
|
||||||
|
viewportsByPosition,
|
||||||
|
viewportIndex: number,
|
||||||
|
positionId: string,
|
||||||
|
options: Record<string, unknown>
|
||||||
|
) => {
|
||||||
|
const byPositionViewport = viewportsByPosition?.[positionId];
|
||||||
|
if (byPositionViewport) return { ...byPositionViewport };
|
||||||
|
const { protocolId, stageIndex } = hangingProtocolService.getState();
|
||||||
|
|
||||||
|
// Setup the initial in display correctly for initial view/select
|
||||||
|
if (!options.inDisplay) {
|
||||||
|
options.inDisplay = [...viewportsByPosition.initialInDisplay];
|
||||||
|
}
|
||||||
|
// See if there is a default viewport for new views.
|
||||||
|
const missing = hangingProtocolService.getMissingViewport(
|
||||||
|
protocolId,
|
||||||
|
stageIndex,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
if (missing) {
|
||||||
|
const displaySetInstanceUIDs = missing.displaySetsInfo.map(
|
||||||
|
it => it.displaySetInstanceUID
|
||||||
|
);
|
||||||
|
options.inDisplay.push(...displaySetInstanceUIDs);
|
||||||
|
return {
|
||||||
|
displaySetInstanceUIDs,
|
||||||
|
displaySetOptions: missing.displaySetsInfo.map(
|
||||||
|
it => it.displaySetOptions
|
||||||
|
),
|
||||||
|
viewportOptions: {
|
||||||
|
...missing.viewportOptions,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the information on what viewports are displayed in which position.
|
||||||
|
* Also records what instances from the existing positions are going to be in
|
||||||
|
* view initially.
|
||||||
|
* @param state is the viewport grid state
|
||||||
|
* @param syncService is the state sync service to use for getting existing state
|
||||||
|
* @returns Set of states that can be applied to the state sync to remember
|
||||||
|
* the current view state.
|
||||||
|
*/
|
||||||
|
const findViewportsByPosition = (
|
||||||
|
state,
|
||||||
|
{ numRows, numCols },
|
||||||
|
syncService: StateSyncService
|
||||||
|
): Record<string, Record<string, unknown>> => {
|
||||||
|
const { viewports } = state;
|
||||||
|
const syncState = syncService.getState();
|
||||||
|
const viewportsByPosition = { ...syncState.viewportsByPosition };
|
||||||
|
const initialInDisplay = [];
|
||||||
|
|
||||||
|
for (const viewport of viewports) {
|
||||||
|
if (viewport.positionId) {
|
||||||
|
const storedViewport = {
|
||||||
|
...viewport,
|
||||||
|
viewportOptions: { ...viewport.viewportOptions },
|
||||||
|
};
|
||||||
|
viewportsByPosition[viewport.positionId] = storedViewport;
|
||||||
|
// The cache doesn't store the viewport options - it is only useful
|
||||||
|
// for remembering the type of viewport and UIDs
|
||||||
|
delete storedViewport.viewportId;
|
||||||
|
delete storedViewport.viewportOptions.viewportId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let row = 0; row < numRows; row++) {
|
||||||
|
for (let col = 0; col < numCols; col++) {
|
||||||
|
const pos = col + row * numCols;
|
||||||
|
const positionId = viewports?.[pos]?.positionId || `${col}-${row}`;
|
||||||
|
const viewport = viewportsByPosition[positionId];
|
||||||
|
if (viewport?.displaySetInstanceUIDs) {
|
||||||
|
initialInDisplay.push(...viewport.displaySetInstanceUIDs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the initially displayed elements
|
||||||
|
viewportsByPosition.initialInDisplay = initialInDisplay;
|
||||||
|
|
||||||
|
return { viewportsByPosition };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default findViewportsByPosition;
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { CustomizationService } from '@ohif/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DataSourceSelector from './Panels/DataSourceSelector';
|
import DataSourceSelector from './Panels/DataSourceSelector';
|
||||||
|
|
||||||
@ -82,7 +83,6 @@ export default function getCustomizationModule() {
|
|||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
id: 'ohif.overlayItem',
|
id: 'ohif.overlayItem',
|
||||||
uiType: 'uiType',
|
|
||||||
content: function (props) {
|
content: function (props) {
|
||||||
if (this.condition && !this.condition(props)) return null;
|
if (this.condition && !this.condition(props)) return null;
|
||||||
|
|
||||||
@ -91,8 +91,8 @@ export default function getCustomizationModule() {
|
|||||||
instance && this.attribute
|
instance && this.attribute
|
||||||
? instance[this.attribute]
|
? instance[this.attribute]
|
||||||
: this.contentF && typeof this.contentF === 'function'
|
: this.contentF && typeof this.contentF === 'function'
|
||||||
? this.contentF(props)
|
? this.contentF(props)
|
||||||
: null;
|
: null;
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -109,6 +109,29 @@ export default function getCustomizationModule() {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'ohif.contextMenu',
|
||||||
|
|
||||||
|
/** Applies the customizationType to all the menu items.
|
||||||
|
* This function clones the object and child objects to prevent
|
||||||
|
* changes to the original customization object.
|
||||||
|
*/
|
||||||
|
transform: function (customizationService: CustomizationService) {
|
||||||
|
// Don't modify the children, as those are copied by reference
|
||||||
|
const clonedObject = { ...this };
|
||||||
|
clonedObject.menus = this.menus.map(menu => ({ ...menu }));
|
||||||
|
|
||||||
|
for (const menu of clonedObject.menus) {
|
||||||
|
const { items: originalItems } = menu;
|
||||||
|
menu.items = [];
|
||||||
|
for (const item of originalItems) {
|
||||||
|
menu.items.push(customizationService.transform(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clonedObject;
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
const defaultProtocol = {
|
const defaultProtocol = {
|
||||||
id: 'default',
|
id: 'default',
|
||||||
locked: true,
|
locked: true,
|
||||||
|
// Don't store this hanging protocol as it applies to the currently active
|
||||||
|
// display set by default
|
||||||
|
// cacheId: null,
|
||||||
hasUpdatedPriorsInformation: false,
|
hasUpdatedPriorsInformation: false,
|
||||||
name: 'Default',
|
name: 'Default',
|
||||||
createdDate: '2021-02-23T19:22:08.894Z',
|
createdDate: '2021-02-23T19:22:08.894Z',
|
||||||
@ -9,6 +12,25 @@ const defaultProtocol = {
|
|||||||
editableBy: {},
|
editableBy: {},
|
||||||
protocolMatchingRules: [],
|
protocolMatchingRules: [],
|
||||||
toolGroupIds: ['default'],
|
toolGroupIds: ['default'],
|
||||||
|
// -1 would be used to indicate active only, whereas other values are
|
||||||
|
// the number of required priors referenced - so 0 means active with
|
||||||
|
// 0 or more priors.
|
||||||
|
numberOfPriorsReferenced: 0,
|
||||||
|
// Default viewport is used to define the viewport when
|
||||||
|
// additional viewports are added using the layout tool
|
||||||
|
defaultViewport: {
|
||||||
|
viewportOptions: {
|
||||||
|
viewportType: 'stack',
|
||||||
|
toolGroupId: 'default',
|
||||||
|
allowUnmatchedView: true,
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
matchedDisplaySetsIndex: -1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
displaySetSelectors: {
|
displaySetSelectors: {
|
||||||
defaultDisplaySetId: {
|
defaultDisplaySetId: {
|
||||||
// Unused currently
|
// Unused currently
|
||||||
@ -24,12 +46,12 @@ const defaultProtocol = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
studyMatchingRules: [],
|
// Can be used to select matching studies
|
||||||
|
// studyMatchingRules: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
stages: [
|
stages: [
|
||||||
{
|
{
|
||||||
id: 'hYbmMy3b7pz7GLiaT',
|
|
||||||
name: 'default',
|
name: 'default',
|
||||||
viewportStructure: {
|
viewportStructure: {
|
||||||
layoutType: 'grid',
|
layoutType: 'grid',
|
||||||
@ -41,6 +63,7 @@ const defaultProtocol = {
|
|||||||
viewports: [
|
viewports: [
|
||||||
{
|
{
|
||||||
viewportOptions: {
|
viewportOptions: {
|
||||||
|
viewportType: 'stack',
|
||||||
toolGroupId: 'default',
|
toolGroupId: 'default',
|
||||||
// initialImageOptions: {
|
// initialImageOptions: {
|
||||||
// index: 180,
|
// index: 180,
|
||||||
@ -56,14 +79,218 @@ const defaultProtocol = {
|
|||||||
],
|
],
|
||||||
createdDate: '2021-02-23T18:32:42.850Z',
|
createdDate: '2021-02-23T18:32:42.850Z',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// This is an example of a 2x2 layout that requires at least 2 viewports
|
||||||
|
// filled to be navigatable to
|
||||||
|
{
|
||||||
|
name: '2x2',
|
||||||
|
// Indicate that the number of viewports needed is 2 filled viewports,
|
||||||
|
// but that 4 viewports are preferred.
|
||||||
|
stageActivation: {
|
||||||
|
enabled: {
|
||||||
|
minViewportsMatched: 4,
|
||||||
|
},
|
||||||
|
passive: {
|
||||||
|
minViewportsMatched: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 2,
|
||||||
|
columns: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
matchedDisplaySetsIndex: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
matchedDisplaySetsIndex: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
matchedDisplaySetsIndex: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
matchedDisplaySetsIndex: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: '3x1',
|
||||||
|
// Indicate that the number of viewports needed is 2 filled viewports,
|
||||||
|
// but that 4 viewports are preferred.
|
||||||
|
stageActivation: {
|
||||||
|
enabled: {
|
||||||
|
minViewportsMatched: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 1,
|
||||||
|
columns: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
matchedDisplaySetsIndex: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
matchedDisplaySetsIndex: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// This is an example of a layout with more than one element in it
|
||||||
|
// It can be navigated to using , and . (prev/next stage)
|
||||||
|
{
|
||||||
|
name: '2x1',
|
||||||
|
// Indicate that the number of viewports needed is 1 filled viewport,
|
||||||
|
// but that 2 viewports are preferred.
|
||||||
|
stageActivation: {
|
||||||
|
enabled: {
|
||||||
|
minViewportsMatched: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 1,
|
||||||
|
columns: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
// initialImageOptions: {
|
||||||
|
// index: 180,
|
||||||
|
// preset: 'middle', // 'first', 'last', 'middle'
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
// Shows the second index of this image set
|
||||||
|
matchedDisplaySetsIndex: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: '2x1',
|
||||||
|
// Indicate that the number of viewports needed is 1 filled viewport,
|
||||||
|
// but that 2 viewports are preferred.
|
||||||
|
stageActivation: {
|
||||||
|
enabled: {
|
||||||
|
minViewportsMatched: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 2,
|
||||||
|
columns: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions: {},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
// Shows the second index of this image set
|
||||||
|
matchedDisplaySetsIndex: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
numberOfPriorsReferenced: -1,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getHangingProtocolModule() {
|
function getHangingProtocolModule() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: defaultProtocol.id,
|
name: defaultProtocol.id,
|
||||||
protocol: defaultProtocol,
|
protocol: defaultProtocol,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -29,6 +29,7 @@ const makeDisplaySet = instances => {
|
|||||||
SeriesDescription: instance.SeriesDescription || '',
|
SeriesDescription: instance.SeriesDescription || '',
|
||||||
Modality: instance.Modality,
|
Modality: instance.Modality,
|
||||||
isMultiFrame: isMultiFrame(instance),
|
isMultiFrame: isMultiFrame(instance),
|
||||||
|
countIcon: displayReconstructableInfo.value ? 'icon-mpr' : undefined,
|
||||||
numImageFrames: instances.length,
|
numImageFrames: instances.length,
|
||||||
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`,
|
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`,
|
||||||
isReconstructable: displayReconstructableInfo.value,
|
isReconstructable: displayReconstructableInfo.value,
|
||||||
|
|||||||
@ -1,32 +1,34 @@
|
|||||||
|
import { Types } from '@ohif/core';
|
||||||
|
|
||||||
import getDataSourcesModule from './getDataSourcesModule.js';
|
import getDataSourcesModule from './getDataSourcesModule.js';
|
||||||
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
||||||
import getPanelModule from './getPanelModule';
|
import getPanelModule from './getPanelModule';
|
||||||
import getSopClassHandlerModule from './getSopClassHandlerModule.js';
|
import getSopClassHandlerModule from './getSopClassHandlerModule.js';
|
||||||
import getToolbarModule from './getToolbarModule';
|
import getToolbarModule from './getToolbarModule';
|
||||||
import commandsModule from './commandsModule';
|
import getCommandsModule from './commandsModule';
|
||||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||||
import getStudiesForPatientByStudyInstanceUID from './Panels/getStudiesForPatientByStudyInstanceUID';
|
import getStudiesForPatientByStudyInstanceUID from './Panels/getStudiesForPatientByStudyInstanceUID';
|
||||||
import getCustomizationModule from './getCustomizationModule';
|
import getCustomizationModule from './getCustomizationModule';
|
||||||
import { id } from './id.js';
|
import { id } from './id.js';
|
||||||
import init from './init';
|
import preRegistration from './init';
|
||||||
|
import {
|
||||||
|
ContextMenuController,
|
||||||
|
CustomizeableContextMenuTypes,
|
||||||
|
} from './CustomizeableContextMenu';
|
||||||
|
|
||||||
const defaultExtension = {
|
const defaultExtension: 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.
|
||||||
*/
|
*/
|
||||||
id,
|
id,
|
||||||
preRegistration: ({ servicesManager, configuration = {} }) => {
|
preRegistration,
|
||||||
init({ servicesManager, configuration });
|
|
||||||
},
|
|
||||||
getDataSourcesModule,
|
getDataSourcesModule,
|
||||||
getLayoutTemplateModule,
|
getLayoutTemplateModule,
|
||||||
getPanelModule,
|
getPanelModule,
|
||||||
getHangingProtocolModule,
|
getHangingProtocolModule,
|
||||||
getSopClassHandlerModule,
|
getSopClassHandlerModule,
|
||||||
getToolbarModule,
|
getToolbarModule,
|
||||||
getCommandsModule({ servicesManager, commandsManager }) {
|
getCommandsModule,
|
||||||
return commandsModule({ servicesManager, commandsManager });
|
|
||||||
},
|
|
||||||
getUtilityModule({ servicesManager }) {
|
getUtilityModule({ servicesManager }) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -42,3 +44,5 @@ const defaultExtension = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default defaultExtension;
|
export default defaultExtension;
|
||||||
|
|
||||||
|
export { ContextMenuController, CustomizeableContextMenuTypes };
|
||||||
@ -10,7 +10,8 @@ const metadataProvider = classes.MetadataProvider;
|
|||||||
* @param {Object} servicesManager
|
* @param {Object} servicesManager
|
||||||
* @param {Object} configuration
|
* @param {Object} configuration
|
||||||
*/
|
*/
|
||||||
export default function init({ servicesManager, configuration }) {
|
export default function init({ servicesManager, configuration = {} }): void {
|
||||||
|
const { stateSyncService } = servicesManager.services;
|
||||||
// Add
|
// Add
|
||||||
DicomMetadataStore.subscribe(
|
DicomMetadataStore.subscribe(
|
||||||
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
|
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
|
||||||
@ -23,6 +24,34 @@ export default function init({ servicesManager, configuration }) {
|
|||||||
DicomMetadataStore.EVENTS.SERIES_UPDATED,
|
DicomMetadataStore.EVENTS.SERIES_UPDATED,
|
||||||
handlePETImageMetadata
|
handlePETImageMetadata
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// viewportGridStore is a sync state which stores the entire
|
||||||
|
// ViewportGridService getState, by the keys `<activeStudyUID>:<protocolId>:<stageIndex>`
|
||||||
|
// Used to recover manual changes to the layout of a stage.
|
||||||
|
stateSyncService.register('viewportGridStore', { clearOnModeExit: true });
|
||||||
|
|
||||||
|
// displaySetSelectorMap stores a map from
|
||||||
|
// `<activeStudyUID>:<displaySetSelectorId>:<matchOffset>` to
|
||||||
|
// a displaySetInstanceUID, used to display named display sets in
|
||||||
|
// specific spots within a hanging protocol and be able to remember what the
|
||||||
|
// user did with those named spots between stages and protocols.
|
||||||
|
stateSyncService.register('displaySetSelectorMap', { clearOnModeExit: true });
|
||||||
|
|
||||||
|
// Stores a map from `<activeStudyUID>:${protocolId}` to the getHPInfo results
|
||||||
|
// in order to recover the correct stage when returning to a Hanging Protocol.
|
||||||
|
stateSyncService.register('hangingProtocolStageIndexMap', {
|
||||||
|
clearOnModeExit: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stores a map from the to be applied hanging protocols `<activeStudyUID>:<protocolId>`
|
||||||
|
// to the previously applied hanging protolStageIndexMap key, in order to toggle
|
||||||
|
// off the applied protocol and remember the old state.
|
||||||
|
stateSyncService.register('toggleHangingProtocol', { clearOnModeExit: true });
|
||||||
|
|
||||||
|
// Stores the viewports by `rows-cols` position so that when the layout
|
||||||
|
// changes numRows and numCols, the viewports can be remembers and then replaced
|
||||||
|
// afterwards.
|
||||||
|
stateSyncService.register('viewportsByPosition', { clearOnModeExit: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => {
|
const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => {
|
||||||
75
extensions/default/src/utils/reuseCachedLayouts.ts
Normal file
75
extensions/default/src/utils/reuseCachedLayouts.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { HangingProtocolService, StateSyncService, Types } from '@ohif/core';
|
||||||
|
|
||||||
|
export type ReturnType = {
|
||||||
|
hangingProtocolStageIndexMap: Record<string, Types.HangingProtocol.HPInfo>;
|
||||||
|
viewportGridStore: Record<string, unknown>;
|
||||||
|
displaySetSelectorMap: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates a set of state information for hanging protocols and viewport grid
|
||||||
|
* which defines the currently applied hanging protocol state.
|
||||||
|
* @param state is the viewport grid state
|
||||||
|
* @param syncService is the state sync service to use for getting existing state
|
||||||
|
* @returns Set of states that can be applied to the state sync to remember
|
||||||
|
* the current view state.
|
||||||
|
*/
|
||||||
|
const reuseCachedLayout = (
|
||||||
|
state,
|
||||||
|
hangingProtocolService: HangingProtocolService,
|
||||||
|
syncService: StateSyncService
|
||||||
|
): ReturnType => {
|
||||||
|
const { activeViewportIndex, viewports, layout } = state;
|
||||||
|
const hpInfo = hangingProtocolService.getState();
|
||||||
|
const { protocolId, stageIndex, activeStudyUID } = hpInfo;
|
||||||
|
const { protocol } = hangingProtocolService.getActiveProtocol();
|
||||||
|
const stage = protocol.stages[stageIndex];
|
||||||
|
const storeId = `${activeStudyUID}:${protocolId}:${stageIndex}`;
|
||||||
|
const syncState = syncService.getState();
|
||||||
|
const cacheId = `${activeStudyUID}:${protocolId}`;
|
||||||
|
const viewportGridStore = { ...syncState.viewportGridStore };
|
||||||
|
const hangingProtocolStageIndexMap = {
|
||||||
|
...syncState.hangingProtocolStageIndexMap,
|
||||||
|
};
|
||||||
|
const displaySetSelectorMap = { ...syncState.displaySetSelectorMap };
|
||||||
|
const { rows, columns } = stage.viewportStructure.properties;
|
||||||
|
const custom =
|
||||||
|
stage.viewports.length !== state.viewports.length ||
|
||||||
|
state.layout.numRows !== rows ||
|
||||||
|
state.layout.numCols !== columns;
|
||||||
|
|
||||||
|
hangingProtocolStageIndexMap[cacheId] = hpInfo;
|
||||||
|
|
||||||
|
if (storeId && custom) {
|
||||||
|
viewportGridStore[storeId] = { ...state };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let idx = 0; idx < state.viewports.length; idx++) {
|
||||||
|
const viewport = state.viewports[idx];
|
||||||
|
const { displaySetOptions, displaySetInstanceUIDs } = viewport;
|
||||||
|
if (!displaySetOptions) continue;
|
||||||
|
for (let i = 0; i < displaySetOptions.length; i++) {
|
||||||
|
const displaySetUID = displaySetInstanceUIDs[i];
|
||||||
|
if (!displaySetUID) continue;
|
||||||
|
if (idx === activeViewportIndex && i === 0) {
|
||||||
|
displaySetSelectorMap[
|
||||||
|
`${activeStudyUID}:activeDisplaySet:0`
|
||||||
|
] = displaySetUID;
|
||||||
|
}
|
||||||
|
if (displaySetOptions[i]?.id) {
|
||||||
|
displaySetSelectorMap[
|
||||||
|
`${activeStudyUID}:${displaySetOptions[i].id}:${displaySetOptions[i]
|
||||||
|
.matchedDisplaySetsIndex || 0}`
|
||||||
|
] = displaySetUID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hangingProtocolStageIndexMap,
|
||||||
|
viewportGridStore,
|
||||||
|
displaySetSelectorMap,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reuseCachedLayout;
|
||||||
@ -32,10 +32,11 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^3.0.0",
|
"@ohif/core": "^3.0.0",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"@cornerstonejs/core": "^0.33.2",
|
"@cornerstonejs/core": "^0.40.0",
|
||||||
"@cornerstonejs/tools": "^0.50.2",
|
"@cornerstonejs/tools": "^0.60.1",
|
||||||
"@ohif/extension-cornerstone-dicom-sr": "^3.0.0",
|
"@ohif/extension-cornerstone-dicom-sr": "^3.0.0",
|
||||||
"dcmjs": "^0.29.4",
|
"dcmjs": "^0.29.4",
|
||||||
|
"lodash.debounce": "^4.17.21",
|
||||||
"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",
|
||||||
|
|||||||
@ -7,12 +7,12 @@ const RESPONSE = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function promptBeginTracking({ servicesManager, extensionManager }, ctx, evt) {
|
function promptBeginTracking({ servicesManager, extensionManager }, ctx, evt) {
|
||||||
const { UIViewportDialogService } = servicesManager.services;
|
const { uiViewportDialogService } = servicesManager.services;
|
||||||
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
|
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
|
||||||
|
|
||||||
return new Promise(async function(resolve, reject) {
|
return new Promise(async function(resolve, reject) {
|
||||||
let promptResult = await _askTrackMeasurements(
|
let promptResult = await _askTrackMeasurements(
|
||||||
UIViewportDialogService,
|
uiViewportDialogService,
|
||||||
viewportIndex
|
viewportIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ function promptBeginTracking({ servicesManager, extensionManager }, ctx, evt) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
|
function _askTrackMeasurements(uiViewportDialogService, viewportIndex) {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
const message = 'Track measurements for this series?';
|
const message = 'Track measurements for this series?';
|
||||||
const actions = [
|
const actions = [
|
||||||
@ -49,11 +49,11 @@ function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
const onSubmit = result => {
|
const onSubmit = result => {
|
||||||
UIViewportDialogService.hide();
|
uiViewportDialogService.hide();
|
||||||
resolve(result);
|
resolve(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
UIViewportDialogService.show({
|
uiViewportDialogService.show({
|
||||||
viewportIndex,
|
viewportIndex,
|
||||||
id: 'measurement-tracking-prompt-begin-tracking',
|
id: 'measurement-tracking-prompt-begin-tracking',
|
||||||
type: 'info',
|
type: 'info',
|
||||||
@ -61,7 +61,7 @@ function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
|
|||||||
actions,
|
actions,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onOutsideClick: () => {
|
onOutsideClick: () => {
|
||||||
UIViewportDialogService.hide();
|
uiViewportDialogService.hide();
|
||||||
resolve(RESPONSE.CANCEL);
|
resolve(RESPONSE.CANCEL);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -16,7 +16,7 @@ function promptHydrateStructuredReport(
|
|||||||
evt
|
evt
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
UIViewportDialogService,
|
uiViewportDialogService,
|
||||||
displaySetService,
|
displaySetService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
const { viewportIndex, displaySetInstanceUID } = evt;
|
const { viewportIndex, displaySetInstanceUID } = evt;
|
||||||
@ -26,7 +26,7 @@ function promptHydrateStructuredReport(
|
|||||||
|
|
||||||
return new Promise(async function(resolve, reject) {
|
return new Promise(async function(resolve, reject) {
|
||||||
const promptResult = await _askTrackMeasurements(
|
const promptResult = await _askTrackMeasurements(
|
||||||
UIViewportDialogService,
|
uiViewportDialogService,
|
||||||
viewportIndex
|
viewportIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ function promptHydrateStructuredReport(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
|
function _askTrackMeasurements(uiViewportDialogService, viewportIndex) {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
const message =
|
const message =
|
||||||
'Do you want to continue tracking measurements for this study?';
|
'Do you want to continue tracking measurements for this study?';
|
||||||
@ -72,18 +72,18 @@ function _askTrackMeasurements(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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -33,7 +33,7 @@ function promptTrackNewSeries({ servicesManager, extensionManager }, ctx, evt) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _askShouldAddMeasurements(UIViewportDialogService, viewportIndex) {
|
function _askShouldAddMeasurements(uiViewportDialogService, viewportIndex) {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
const message =
|
const message =
|
||||||
'Do you want to add this measurement to the existing report?';
|
'Do you want to add this measurement to the existing report?';
|
||||||
@ -51,18 +51,18 @@ function _askShouldAddMeasurements(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,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {
|
||||||
StudySummary,
|
StudySummary,
|
||||||
@ -11,6 +11,7 @@ import { DicomMetadataStore, utils } from '@ohif/core';
|
|||||||
import { useDebounce } from '@hooks';
|
import { useDebounce } from '@hooks';
|
||||||
import ActionButtons from './ActionButtons';
|
import ActionButtons from './ActionButtons';
|
||||||
import { useTrackedMeasurements } from '../../getContextModule';
|
import { useTrackedMeasurements } from '../../getContextModule';
|
||||||
|
import debounce from 'lodash.debounce';
|
||||||
|
|
||||||
const { downloadCSVReport } = utils;
|
const { downloadCSVReport } = utils;
|
||||||
const { formatDate } = utils;
|
const { formatDate } = utils;
|
||||||
@ -45,6 +46,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
|||||||
DISPLAY_STUDY_SUMMARY_INITIAL_VALUE
|
DISPLAY_STUDY_SUMMARY_INITIAL_VALUE
|
||||||
);
|
);
|
||||||
const [displayMeasurements, setDisplayMeasurements] = useState([]);
|
const [displayMeasurements, setDisplayMeasurements] = useState([]);
|
||||||
|
const measurementsPanelRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const measurements = measurementService.getMeasurements();
|
const measurements = measurementService.getMeasurements();
|
||||||
@ -125,6 +127,12 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
|||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
measurementService.subscribe(evt, () => {
|
measurementService.subscribe(evt, () => {
|
||||||
setMeasurementsUpdated(Date.now().toString());
|
setMeasurementsUpdated(Date.now().toString());
|
||||||
|
if (evt === added) {
|
||||||
|
debounce(() => {
|
||||||
|
measurementsPanelRef.current.scrollTop =
|
||||||
|
measurementsPanelRef.current.scrollHeight;
|
||||||
|
}, 300)();
|
||||||
|
}
|
||||||
}).unsubscribe
|
}).unsubscribe
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -241,6 +249,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="overflow-x-hidden overflow-y-auto invisible-scrollbar"
|
className="overflow-x-hidden overflow-y-auto invisible-scrollbar"
|
||||||
|
ref={measurementsPanelRef}
|
||||||
data-cy={'trackedMeasurements-panel'}
|
data-cy={'trackedMeasurements-panel'}
|
||||||
>
|
>
|
||||||
{displayStudySummary.key && (
|
{displayStudySummary.key && (
|
||||||
@ -253,6 +262,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
|||||||
<MeasurementTable
|
<MeasurementTable
|
||||||
title="Measurements"
|
title="Measurements"
|
||||||
data={displayMeasurementsWithoutFindings}
|
data={displayMeasurementsWithoutFindings}
|
||||||
|
servicesManager={servicesManager}
|
||||||
onClick={jumpToImage}
|
onClick={jumpToImage}
|
||||||
onEdit={onMeasurementItemEditHandler}
|
onEdit={onMeasurementItemEditHandler}
|
||||||
/>
|
/>
|
||||||
@ -260,6 +270,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
|||||||
<MeasurementTable
|
<MeasurementTable
|
||||||
title="Additional Findings"
|
title="Additional Findings"
|
||||||
data={additionalFindings}
|
data={additionalFindings}
|
||||||
|
servicesManager={servicesManager}
|
||||||
onClick={jumpToImage}
|
onClick={jumpToImage}
|
||||||
onEdit={onMeasurementItemEditHandler}
|
onEdit={onMeasurementItemEditHandler}
|
||||||
/>
|
/>
|
||||||
@ -318,13 +329,40 @@ function _mapMeasurementToDisplay(measurement, types, displaySetService) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { displayText } = measurement;
|
const {
|
||||||
|
displayText: baseDisplayText,
|
||||||
|
uid,
|
||||||
|
label: baseLabel,
|
||||||
|
type,
|
||||||
|
selected,
|
||||||
|
findingSites,
|
||||||
|
finding,
|
||||||
|
} = measurement;
|
||||||
|
|
||||||
|
const firstSite = findingSites?.[0];
|
||||||
|
const label = baseLabel || finding?.text || firstSite?.text || '(empty)';
|
||||||
|
let displayText = baseDisplayText || [];
|
||||||
|
if (findingSites) {
|
||||||
|
const siteText = [];
|
||||||
|
findingSites.forEach(site => {
|
||||||
|
if (site?.text !== label) siteText.push(site.text);
|
||||||
|
});
|
||||||
|
displayText = [...siteText, ...displayText];
|
||||||
|
}
|
||||||
|
if (finding && finding?.text !== label) {
|
||||||
|
displayText = [finding.text, ...displayText];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uid: measurement.uid,
|
uid,
|
||||||
label: measurement.label || '(empty)',
|
label,
|
||||||
measurementType: measurement.type,
|
baseLabel,
|
||||||
displayText: displayText || [],
|
measurementType: type,
|
||||||
isActive: measurement.selected,
|
displayText,
|
||||||
|
baseDisplayText,
|
||||||
|
isActive: selected,
|
||||||
|
finding,
|
||||||
|
findingSites,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -453,6 +453,7 @@ function _mapDisplaySets(
|
|||||||
modality: ds.Modality,
|
modality: ds.Modality,
|
||||||
seriesDate: formatDate(ds.SeriesDate),
|
seriesDate: formatDate(ds.SeriesDate),
|
||||||
numInstances: ds.numImageFrames,
|
numInstances: ds.numImageFrames,
|
||||||
|
countIcon: ds.countIcon,
|
||||||
StudyInstanceUID: ds.StudyInstanceUID,
|
StudyInstanceUID: ds.StudyInstanceUID,
|
||||||
componentType,
|
componentType,
|
||||||
imageSrc,
|
imageSrc,
|
||||||
|
|||||||
@ -80,7 +80,7 @@ function TrackedCornerstoneViewport(props) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
annotation.config.style.setViewportToolStyles(`viewport-${viewportIndex}`, {
|
annotation.config.style.setViewportToolStyles(viewportId, {
|
||||||
global: {
|
global: {
|
||||||
lineDash: '4,4',
|
lineDash: '4,4',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export default (study, extraData) => Math.max(...(extraData?.displaySets?.map?.(ds => (ds.numImageFrames ?? 0))) || [0]);
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export default (study, extraData) => extraData?.displaySets?.length;
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export default (study, extraData) => {
|
||||||
|
const ret = extraData?.displaySets?.filter(ds => ds.numImageFrames>0)?.length;
|
||||||
|
console.log("number of display sets with images", ret);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
33
extensions/test-extension/src/custom-attribute/sameAs.ts
Normal file
33
extensions/test-extension/src/custom-attribute/sameAs.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* This function extracts an attribute from the already matched display sets, and
|
||||||
|
* compares it to the attribute in the current display set, and indicates if they match.
|
||||||
|
* From 'this', it uses:
|
||||||
|
* `sameAttribute` as the attribute name to look for
|
||||||
|
* `sameDisplaySetId` as the display set id to look for
|
||||||
|
* From `options`, it looks for
|
||||||
|
*/
|
||||||
|
export default function (displaySet, options) {
|
||||||
|
const { sameAttribute, sameDisplaySetId } = this;
|
||||||
|
if( !sameAttribute ) {
|
||||||
|
console.log("sameAttribute not defined in", this);
|
||||||
|
return `sameAttribute not defined in ${this.id}`;
|
||||||
|
}
|
||||||
|
if( !sameDisplaySetId ) {
|
||||||
|
console.log("sameDisplaySetId not defined in", this);
|
||||||
|
return `sameDisplaySetId not defined in ${this.id}`;
|
||||||
|
}
|
||||||
|
const { displaySetMatchDetails, displaySets } = options;
|
||||||
|
const match = displaySetMatchDetails.get(sameDisplaySetId);
|
||||||
|
if( !match ) {
|
||||||
|
console.log("No match for display set", sameDisplaySetId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { displaySetInstanceUID } = match;
|
||||||
|
const altDisplaySet = displaySets.find(it => it.displaySetInstanceUID==displaySetInstanceUID);
|
||||||
|
if( !altDisplaySet ) {
|
||||||
|
console.log("No display set found with", displaySetInstanceUID, "in", displaySets);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const testValue = altDisplaySet[sameAttribute];
|
||||||
|
return testValue===displaySet[sameAttribute];
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export default (study, extraData) => extraData?.displaySets?.map(ds => ds.SeriesDescription);
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Coding values is a map of simple string coding values to a set of
|
||||||
|
* attributes associated with the coding value.
|
||||||
|
*
|
||||||
|
* The simple string is in the format `<codingSchemeDesignator>:<codingValue>`
|
||||||
|
* That allows extracting the DICOM attributes from the designator/value, and
|
||||||
|
* allows for passing around the simple string.
|
||||||
|
* The additional attributes contained in the object include:
|
||||||
|
* * text - this is the coding scheme text display value, and may be language specific
|
||||||
|
* * type - this defines a named type, typically 'site'. Different names can be used
|
||||||
|
* to allow setting different findingSites values in order to define a hierarchy.
|
||||||
|
* * color - used to apply annotation color
|
||||||
|
* It is also possible to define additional attributes here, used by custom
|
||||||
|
* extensions.
|
||||||
|
*
|
||||||
|
* See https://dicom.nema.org/medical/dicom/current/output/html/part16.html
|
||||||
|
* for definitions of SCT and other code values.
|
||||||
|
*/
|
||||||
|
const codingValues = {
|
||||||
|
id: 'codingValues',
|
||||||
|
|
||||||
|
// Sites
|
||||||
|
'SCT:69536005': {
|
||||||
|
text: 'Head',
|
||||||
|
type: 'site',
|
||||||
|
},
|
||||||
|
'SCT:45048000': {
|
||||||
|
text: 'Neck',
|
||||||
|
type: 'site',
|
||||||
|
},
|
||||||
|
'SCT:818981001': {
|
||||||
|
text: 'Abdomen',
|
||||||
|
type: 'site',
|
||||||
|
},
|
||||||
|
'SCT:816092008': {
|
||||||
|
text: 'Pelvis',
|
||||||
|
type: 'site',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Findings
|
||||||
|
'SCT:371861004': {
|
||||||
|
text: 'Mild intimal coronary irregularities',
|
||||||
|
color: 'green',
|
||||||
|
},
|
||||||
|
'SCT:194983005': {
|
||||||
|
text: 'Aortic insufficiency',
|
||||||
|
color: 'darkred',
|
||||||
|
},
|
||||||
|
'SCT:399232001': {
|
||||||
|
text: '2-chamber',
|
||||||
|
},
|
||||||
|
'SCT:103340004': {
|
||||||
|
text: 'SAX',
|
||||||
|
},
|
||||||
|
'SCT:91134007': {
|
||||||
|
text: 'MV',
|
||||||
|
},
|
||||||
|
'SCT:122972007': {
|
||||||
|
text: 'PV',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Orientations
|
||||||
|
'SCT:24422004': {
|
||||||
|
text: 'Axial',
|
||||||
|
color: '#000000',
|
||||||
|
type: 'orientation',
|
||||||
|
},
|
||||||
|
'SCT:81654009': {
|
||||||
|
text: 'Coronal',
|
||||||
|
color: '#000000',
|
||||||
|
type: 'orientation',
|
||||||
|
},
|
||||||
|
'SCT:30730003': {
|
||||||
|
text: 'Sagittal',
|
||||||
|
color: '#000000',
|
||||||
|
type: 'orientation',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default codingValues;
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
const codeMenuItem = {
|
||||||
|
id: '@ohif/contextMenuAnnotationCode',
|
||||||
|
|
||||||
|
/** Applies the code value setup for this item */
|
||||||
|
transform: function (customizationService) {
|
||||||
|
const { code: codeRef } = this;
|
||||||
|
if (!codeRef) throw new Error(`item ${this} has no code ref`);
|
||||||
|
const codingValues = customizationService.get('codingValues');
|
||||||
|
const code = codingValues[codeRef];
|
||||||
|
return {
|
||||||
|
...this,
|
||||||
|
codeRef,
|
||||||
|
code: { ref: codeRef, ...code },
|
||||||
|
label: code.text,
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
commandName: 'updateMeasurement',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default codeMenuItem;
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
const findingsContextMenu = {
|
||||||
|
id: 'measurementsContextMenu',
|
||||||
|
customizationType: 'ohif.contextMenu',
|
||||||
|
menus: [
|
||||||
|
{
|
||||||
|
id: 'forExistingMeasurement',
|
||||||
|
// selector restricts context menu to when there is nearbyToolData
|
||||||
|
selector: ({ nearbyToolData }) => !!nearbyToolData,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
customizationType: 'ohif.contextSubMenu',
|
||||||
|
label: 'Site',
|
||||||
|
actionType: 'ShowSubMenu',
|
||||||
|
subMenu: 'siteSelectionSubMenu',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customizationType: 'ohif.contextSubMenu',
|
||||||
|
label: 'Finding',
|
||||||
|
actionType: 'ShowSubMenu',
|
||||||
|
subMenu: 'findingSelectionSubMenu',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// customizationType is implicit here in the configuration setup
|
||||||
|
label: 'Delete Measurement',
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
commandName: 'deleteMeasurement',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Add Label',
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
commandName: 'setMeasurementLabel',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// The example below shows how to include a delegating sub-menu,
|
||||||
|
// Only available on the @ohif/hp-extension.mn hanging protocol
|
||||||
|
// To demonstrate, select the 3x1 layout from the protocol menu
|
||||||
|
// and right click on a measurement.
|
||||||
|
{
|
||||||
|
label: 'IncludeSubMenu',
|
||||||
|
selector: ({ protocol }) => protocol?.id === '@ohif/hp-extension.mn',
|
||||||
|
delegating: true,
|
||||||
|
subMenu: 'orientationSelectionSubMenu',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'orientationSelectionSubMenu',
|
||||||
|
selector: ({ nearbyToolData }) => false,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
customizationType: '@ohif/contextMenuAnnotationCode',
|
||||||
|
code: 'SCT:24422004',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customizationType: '@ohif/contextMenuAnnotationCode',
|
||||||
|
code: 'SCT:81654009',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'findingSelectionSubMenu',
|
||||||
|
selector: ({ nearbyToolData }) => false,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
customizationType: '@ohif/contextMenuAnnotationCode',
|
||||||
|
code: 'SCT:371861004',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customizationType: '@ohif/contextMenuAnnotationCode',
|
||||||
|
code: 'SCT:194983005',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'siteSelectionSubMenu',
|
||||||
|
selector: ({ nearbyToolData }) => !!nearbyToolData,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
customizationType: '@ohif/contextMenuAnnotationCode',
|
||||||
|
code: 'SCT:69536005',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customizationType: '@ohif/contextMenuAnnotationCode',
|
||||||
|
code: 'SCT:45048000',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default findingsContextMenu;
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import codingValues from './codingValues';
|
||||||
|
import contextMenuCodeItem from './contextMenuCodeItem';
|
||||||
|
import findingsContextMenu from './findingsContextMenu';
|
||||||
|
|
||||||
|
export { codingValues, contextMenuCodeItem, findingsContextMenu };
|
||||||
14
extensions/test-extension/src/getCustomizationModule.ts
Normal file
14
extensions/test-extension/src/getCustomizationModule.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import {
|
||||||
|
codingValues,
|
||||||
|
contextMenuCodeItem,
|
||||||
|
findingsContextMenu,
|
||||||
|
} from './custom-context-menu';
|
||||||
|
|
||||||
|
export default function getCustomizationModule() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'custom-context-menu',
|
||||||
|
value: [codingValues, contextMenuCodeItem, findingsContextMenu],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
257
extensions/test-extension/src/hp/hpMN.ts
Normal file
257
extensions/test-extension/src/hp/hpMN.ts
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
import { Types } from '@ohif/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This hanging protocol has multiple stages, which are enabled when
|
||||||
|
* there are enough display sets with images to fill the stage, and
|
||||||
|
* are passive when there is at least one display set.
|
||||||
|
* Enabled display sets are navigated to by default, while passive ones
|
||||||
|
* are navigated to manually using the ctrl+end keyboard shortcut.
|
||||||
|
*/
|
||||||
|
const hpMN: Types.HangingProtocol.Protocol = {
|
||||||
|
hasUpdatedPriorsInformation: false,
|
||||||
|
id: '@ohif/hp-extension.mn',
|
||||||
|
description: 'Has various hanging protocol layouts for use in testing',
|
||||||
|
name: '2x2',
|
||||||
|
protocolMatchingRules: [
|
||||||
|
{
|
||||||
|
id: 'OneOrMoreSeries',
|
||||||
|
weight: 1,
|
||||||
|
attribute: 'numberOfDisplaySetsWithImages',
|
||||||
|
constraint: {
|
||||||
|
greaterThan: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
toolGroupIds: ['default'],
|
||||||
|
displaySetSelectors: {
|
||||||
|
defaultDisplaySetId: {
|
||||||
|
seriesMatchingRules: [
|
||||||
|
{
|
||||||
|
attribute: 'numImageFrames',
|
||||||
|
constraint: {
|
||||||
|
greaterThan: { value: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultViewport: {
|
||||||
|
viewportOptions: {
|
||||||
|
viewportType: 'stack',
|
||||||
|
toolGroupId: 'default',
|
||||||
|
allowUnmatchedView: true,
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
matchedDisplaySetsIndex: -1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
id: '2x2',
|
||||||
|
stageActivation: {
|
||||||
|
enabled: {
|
||||||
|
minViewportsMatched: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 2,
|
||||||
|
columns: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
allowUnmatchedView: true,
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
allowUnmatchedView: true,
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 1,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
allowUnmatchedView: true,
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 2,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
allowUnmatchedView: true,
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 3,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// 3x1 stage
|
||||||
|
{
|
||||||
|
id: '3x1',
|
||||||
|
// Obsolete settings:
|
||||||
|
requiredViewports: 1,
|
||||||
|
preferredViewports: 3,
|
||||||
|
// New equivalent:
|
||||||
|
stageActivation: {
|
||||||
|
enabled: {
|
||||||
|
minViewportsMatched: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 1,
|
||||||
|
columns: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
allowUnmatchedView: true,
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
reuseId: '0-0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
allowUnmatchedView: true,
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 1,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
reuseId: '1-0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
allowUnmatchedView: true,
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 2,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
reuseId: '0-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// A 2x1 stage
|
||||||
|
{
|
||||||
|
id: '2x1',
|
||||||
|
requiredViewports: 1,
|
||||||
|
preferredViewports: 2,
|
||||||
|
stageActivation: {
|
||||||
|
enabled: {
|
||||||
|
minViewportsMatched: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 1,
|
||||||
|
columns: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
allowUnmatchedView: true,
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
allowUnmatchedView: true,
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 1,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// A 1x1 stage - should be automatically activated if there is only 1 viewable instance
|
||||||
|
{
|
||||||
|
id: '1x1',
|
||||||
|
requiredViewports: 1,
|
||||||
|
preferredViewports: 1,
|
||||||
|
stageActivation: {
|
||||||
|
enabled: {
|
||||||
|
minViewportsMatched: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 1,
|
||||||
|
columns: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
allowUnmatchedView: true,
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
numberOfPriorsReferenced: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default hpMN;
|
||||||
17
extensions/test-extension/src/hp/index.ts
Normal file
17
extensions/test-extension/src/hp/index.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import hpMN from './hpMN';
|
||||||
|
|
||||||
|
const hangingProtocols = [
|
||||||
|
{
|
||||||
|
name: '@ohif/hp-extension.mn',
|
||||||
|
protocol: hpMN,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a single study hanging protocol which can be referenced as
|
||||||
|
* `@ohif/hp-exgtension.mn`, that has initial layouts which show images
|
||||||
|
* only display sets, up to a 2x2 view.
|
||||||
|
*/
|
||||||
|
export default function getHangingProtocolModule() {
|
||||||
|
return hangingProtocols;
|
||||||
|
}
|
||||||
@ -1,17 +1,68 @@
|
|||||||
import { id } from './id';
|
|
||||||
import { Types } from '@ohif/core';
|
import { Types } from '@ohif/core';
|
||||||
|
|
||||||
|
import { id } from './id';
|
||||||
|
|
||||||
|
import getHangingProtocolModule from './hp';
|
||||||
|
import getCustomizationModule from './getCustomizationModule';
|
||||||
|
// import {setViewportZoomPan, storeViewportZoomPan } from './custom-viewport/setViewportZoomPan';
|
||||||
|
import sameAs from './custom-attribute/sameAs';
|
||||||
|
import numberOfDisplaySets from './custom-attribute/numberOfDisplaySets';
|
||||||
|
import numberOfDisplaySetsWithImages from './custom-attribute/numberOfDisplaySetsWithImages';
|
||||||
|
import maxNumImageFrames from './custom-attribute/maxNumImageFrames';
|
||||||
|
import seriesDescriptionsFromDisplaySets from './custom-attribute/seriesDescriptionsFromDisplaySets';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* The test extension provides additional behaviour for testing various
|
||||||
|
* customizations and settings for OHIF.
|
||||||
*/
|
*/
|
||||||
const testExtension: Types.Extensions.Extension = {
|
const testExtension: 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.
|
||||||
*/
|
*/
|
||||||
id,
|
id,
|
||||||
preRegistration() {
|
|
||||||
console.debug('hello from test-extension init.js');
|
/** Register additional behaviour:
|
||||||
|
* * HP custom attribute seriesDescriptions to retrieve an array of all series descriptions
|
||||||
|
* * HP custom attribute numberOfDisplaySets to retrieve the number of display sets
|
||||||
|
* * HP custom attribute numberOfDisplaySetsWithImages to retrieve the number of display sets containing images
|
||||||
|
* * HP custom attribute to return a boolean true, when the attribute sameAttribute has the same
|
||||||
|
* value as another series description in an already matched display set selector named with the value
|
||||||
|
* in `sameDisplaySetId`
|
||||||
|
*/
|
||||||
|
preRegistration: ({ servicesManager }: Types.Extensions.ExtensionParams) => {
|
||||||
|
const { hangingProtocolService } = servicesManager.services;
|
||||||
|
hangingProtocolService.addCustomAttribute(
|
||||||
|
'seriesDescriptions',
|
||||||
|
'Series Descriptions',
|
||||||
|
seriesDescriptionsFromDisplaySets
|
||||||
|
);
|
||||||
|
hangingProtocolService.addCustomAttribute(
|
||||||
|
'numberOfDisplaySets',
|
||||||
|
'Number of displays sets',
|
||||||
|
numberOfDisplaySets
|
||||||
|
);
|
||||||
|
hangingProtocolService.addCustomAttribute(
|
||||||
|
'numberOfDisplaySetsWithImages',
|
||||||
|
'Number of displays sets with images',
|
||||||
|
numberOfDisplaySetsWithImages
|
||||||
|
);
|
||||||
|
hangingProtocolService.addCustomAttribute(
|
||||||
|
'maxNumImageFrames',
|
||||||
|
'Maximum of number of image frames',
|
||||||
|
maxNumImageFrames
|
||||||
|
);
|
||||||
|
hangingProtocolService.addCustomAttribute(
|
||||||
|
'sameAs',
|
||||||
|
'Match an attribute in an existing display set',
|
||||||
|
sameAs
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Registers some additional hanging protocols. See hp/index.tsx for more details */
|
||||||
|
getHangingProtocolModule,
|
||||||
|
|
||||||
|
/** Registers some customizations */
|
||||||
|
getCustomizationModule,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default testExtension;
|
export default testExtension;
|
||||||
|
|||||||
@ -1,3 +1,219 @@
|
|||||||
|
import {
|
||||||
|
ctAXIAL,
|
||||||
|
ctCORONAL,
|
||||||
|
ctSAGITTAL,
|
||||||
|
fusionAXIAL,
|
||||||
|
fusionCORONAL,
|
||||||
|
fusionSAGITTAL,
|
||||||
|
mipSAGITTAL,
|
||||||
|
ptAXIAL,
|
||||||
|
ptCORONAL,
|
||||||
|
ptSAGITTAL,
|
||||||
|
} from './utils/hpViewports';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* represents a 3x4 viewport layout configuration. The layout displays CT axial, sagittal, and coronal
|
||||||
|
* images in the first row, PT axial, sagittal, and coronal images in the second row, and fusion axial,
|
||||||
|
* sagittal, and coronal images in the third row. The fourth column is fully spanned by a MIP sagittal
|
||||||
|
* image, covering all three rows. It has synchronizers for windowLevel for all CT and PT images, and
|
||||||
|
* also camera synchronizer for each orientation
|
||||||
|
*/
|
||||||
|
const stage1 = {
|
||||||
|
name: 'default',
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 3,
|
||||||
|
columns: 4,
|
||||||
|
layoutOptions: [
|
||||||
|
{
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 1 / 4,
|
||||||
|
height: 1 / 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 1 / 4,
|
||||||
|
y: 0,
|
||||||
|
width: 1 / 4,
|
||||||
|
height: 1 / 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 2 / 4,
|
||||||
|
y: 0,
|
||||||
|
width: 1 / 4,
|
||||||
|
height: 1 / 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 0,
|
||||||
|
y: 1 / 3,
|
||||||
|
width: 1 / 4,
|
||||||
|
height: 1 / 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 1 / 4,
|
||||||
|
y: 1 / 3,
|
||||||
|
width: 1 / 4,
|
||||||
|
height: 1 / 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 2 / 4,
|
||||||
|
y: 1 / 3,
|
||||||
|
width: 1 / 4,
|
||||||
|
height: 1 / 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 0,
|
||||||
|
y: 2 / 3,
|
||||||
|
width: 1 / 4,
|
||||||
|
height: 1 / 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 1 / 4,
|
||||||
|
y: 2 / 3,
|
||||||
|
width: 1 / 4,
|
||||||
|
height: 1 / 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 2 / 4,
|
||||||
|
y: 2 / 3,
|
||||||
|
width: 1 / 4,
|
||||||
|
height: 1 / 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 3 / 4,
|
||||||
|
y: 0,
|
||||||
|
width: 1 / 4,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
ctAXIAL,
|
||||||
|
ctSAGITTAL,
|
||||||
|
ctCORONAL,
|
||||||
|
ptAXIAL,
|
||||||
|
ptSAGITTAL,
|
||||||
|
ptCORONAL,
|
||||||
|
fusionAXIAL,
|
||||||
|
fusionSAGITTAL,
|
||||||
|
fusionCORONAL,
|
||||||
|
mipSAGITTAL,
|
||||||
|
],
|
||||||
|
createdDate: '2021-02-23T18:32:42.850Z',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The layout displays CT axial image in the top-left viewport, fusion axial image
|
||||||
|
* in the top-right viewport, PT axial image in the bottom-left viewport, and MIP
|
||||||
|
* sagittal image in the bottom-right viewport. The layout follows a simple grid
|
||||||
|
* pattern with 2 rows and 2 columns. It includes synchronizers as well.
|
||||||
|
*/
|
||||||
|
const stage2 = {
|
||||||
|
name: 'Fusion 2x2',
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 2,
|
||||||
|
columns: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [ctAXIAL, fusionAXIAL, ptAXIAL, mipSAGITTAL],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top row displays CT images in axial, sagittal, and coronal orientations from
|
||||||
|
* left to right, respectively. The bottom row displays PT images in axial, sagittal,
|
||||||
|
* and coronal orientations from left to right, respectively.
|
||||||
|
* The layout follows a simple grid pattern with 2 rows and 3 columns.
|
||||||
|
* It includes synchronizers as well.
|
||||||
|
*/
|
||||||
|
const stage3 = {
|
||||||
|
name: '2x3-layout',
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 2,
|
||||||
|
columns: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [ctAXIAL, ctSAGITTAL, ctCORONAL, ptAXIAL, ptSAGITTAL, ptCORONAL],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In this layout, the top row displays PT images in coronal, sagittal, and axial
|
||||||
|
* orientations from left to right, respectively, followed by a MIP sagittal image
|
||||||
|
* that spans both rows on the rightmost side. The bottom row displays fusion images
|
||||||
|
* in coronal, sagittal, and axial orientations from left to right, respectively.
|
||||||
|
* There is no viewport in the bottom row's rightmost position, as the MIP sagittal viewport
|
||||||
|
* from the top row spans the full height of both rows.
|
||||||
|
* It includes synchronizers as well.
|
||||||
|
*/
|
||||||
|
const stage4 = {
|
||||||
|
name: '2x4-layout',
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 2,
|
||||||
|
columns: 4,
|
||||||
|
layoutOptions: [
|
||||||
|
{
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 1 / 4,
|
||||||
|
height: 1 / 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 1 / 4,
|
||||||
|
y: 0,
|
||||||
|
width: 1 / 4,
|
||||||
|
height: 1 / 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 2 / 4,
|
||||||
|
y: 0,
|
||||||
|
width: 1 / 4,
|
||||||
|
height: 1 / 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 3 / 4,
|
||||||
|
y: 0,
|
||||||
|
width: 1 / 4,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 0,
|
||||||
|
y: 1 / 2,
|
||||||
|
width: 1 / 4,
|
||||||
|
height: 1 / 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 1 / 4,
|
||||||
|
y: 1 / 2,
|
||||||
|
width: 1 / 4,
|
||||||
|
height: 1 / 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 2 / 4,
|
||||||
|
y: 1 / 2,
|
||||||
|
width: 1 / 4,
|
||||||
|
height: 1 / 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
ptCORONAL,
|
||||||
|
ptSAGITTAL,
|
||||||
|
ptAXIAL,
|
||||||
|
mipSAGITTAL,
|
||||||
|
fusionCORONAL,
|
||||||
|
fusionSAGITTAL,
|
||||||
|
fusionAXIAL,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const ptCT = {
|
const ptCT = {
|
||||||
id: '@ohif/extension-tmtv.hangingProtocolModule.ptCT',
|
id: '@ohif/extension-tmtv.hangingProtocolModule.ptCT',
|
||||||
locked: true,
|
locked: true,
|
||||||
@ -32,7 +248,6 @@ const ptCT = {
|
|||||||
ctDisplaySet: {
|
ctDisplaySet: {
|
||||||
seriesMatchingRules: [
|
seriesMatchingRules: [
|
||||||
{
|
{
|
||||||
weight: 1,
|
|
||||||
attribute: 'Modality',
|
attribute: 'Modality',
|
||||||
constraint: {
|
constraint: {
|
||||||
equals: {
|
equals: {
|
||||||
@ -42,7 +257,6 @@ const ptCT = {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
weight: 1,
|
|
||||||
attribute: 'isReconstructable',
|
attribute: 'isReconstructable',
|
||||||
constraint: {
|
constraint: {
|
||||||
equals: {
|
equals: {
|
||||||
@ -75,7 +289,6 @@ const ptCT = {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
weight: 1,
|
|
||||||
attribute: 'isReconstructable',
|
attribute: 'isReconstructable',
|
||||||
constraint: {
|
constraint: {
|
||||||
equals: {
|
equals: {
|
||||||
@ -103,508 +316,14 @@ const ptCT = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
stages: [
|
stages: [stage1, stage2, stage3, stage4],
|
||||||
{
|
|
||||||
id: 'hYbmMy3b7pz7GLiaT',
|
|
||||||
name: 'default',
|
|
||||||
viewportStructure: {
|
|
||||||
layoutType: 'grid',
|
|
||||||
properties: {
|
|
||||||
rows: 3,
|
|
||||||
columns: 4,
|
|
||||||
layoutOptions: [
|
|
||||||
{
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1 / 4,
|
|
||||||
height: 1 / 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: 1 / 4,
|
|
||||||
y: 0,
|
|
||||||
width: 1 / 4,
|
|
||||||
height: 1 / 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: 2 / 4,
|
|
||||||
y: 0,
|
|
||||||
width: 1 / 4,
|
|
||||||
height: 1 / 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: 0,
|
|
||||||
y: 1 / 3,
|
|
||||||
width: 1 / 4,
|
|
||||||
height: 1 / 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: 1 / 4,
|
|
||||||
y: 1 / 3,
|
|
||||||
width: 1 / 4,
|
|
||||||
height: 1 / 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: 2 / 4,
|
|
||||||
y: 1 / 3,
|
|
||||||
width: 1 / 4,
|
|
||||||
height: 1 / 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: 0,
|
|
||||||
y: 2 / 3,
|
|
||||||
width: 1 / 4,
|
|
||||||
height: 1 / 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: 1 / 4,
|
|
||||||
y: 2 / 3,
|
|
||||||
width: 1 / 4,
|
|
||||||
height: 1 / 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: 2 / 4,
|
|
||||||
y: 2 / 3,
|
|
||||||
width: 1 / 4,
|
|
||||||
height: 1 / 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: 3 / 4,
|
|
||||||
y: 0,
|
|
||||||
width: 1 / 4,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
viewports: [
|
|
||||||
{
|
|
||||||
viewportOptions: {
|
|
||||||
viewportId: 'ctAXIAL',
|
|
||||||
viewportType: 'volume',
|
|
||||||
orientation: 'axial',
|
|
||||||
toolGroupId: 'ctToolGroup',
|
|
||||||
initialImageOptions: {
|
|
||||||
// index: 5,
|
|
||||||
preset: 'first', // 'first', 'last', 'middle'
|
|
||||||
},
|
|
||||||
syncGroups: [
|
|
||||||
{
|
|
||||||
type: 'cameraPosition',
|
|
||||||
id: 'axialSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'ctWLSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
displaySets: [
|
|
||||||
{
|
|
||||||
id: 'ctDisplaySet',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
viewportOptions: {
|
|
||||||
viewportId: 'ctSAGITTAL',
|
|
||||||
viewportType: 'volume',
|
|
||||||
orientation: 'sagittal',
|
|
||||||
toolGroupId: 'ctToolGroup',
|
|
||||||
syncGroups: [
|
|
||||||
{
|
|
||||||
type: 'cameraPosition',
|
|
||||||
id: 'sagittalSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'ctWLSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
displaySets: [
|
|
||||||
{
|
|
||||||
id: 'ctDisplaySet',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
viewportOptions: {
|
|
||||||
viewportId: 'ctCORONAL',
|
|
||||||
viewportType: 'volume',
|
|
||||||
orientation: 'coronal',
|
|
||||||
toolGroupId: 'ctToolGroup',
|
|
||||||
syncGroups: [
|
|
||||||
{
|
|
||||||
type: 'cameraPosition',
|
|
||||||
id: 'coronalSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'ctWLSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
displaySets: [
|
|
||||||
{
|
|
||||||
id: 'ctDisplaySet',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
viewportOptions: {
|
|
||||||
viewportId: 'ptAXIAL',
|
|
||||||
viewportType: 'volume',
|
|
||||||
background: [1, 1, 1],
|
|
||||||
orientation: 'axial',
|
|
||||||
toolGroupId: 'ptToolGroup',
|
|
||||||
initialImageOptions: {
|
|
||||||
// index: 5,
|
|
||||||
preset: 'first', // 'first', 'last', 'middle'
|
|
||||||
},
|
|
||||||
syncGroups: [
|
|
||||||
{
|
|
||||||
type: 'cameraPosition',
|
|
||||||
id: 'axialSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'ptWLSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'ptFusionWLSync',
|
|
||||||
source: true,
|
|
||||||
target: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
displaySets: [
|
|
||||||
{
|
|
||||||
options: {
|
|
||||||
voi: {
|
|
||||||
windowWidth: 5,
|
|
||||||
windowCenter: 2.5,
|
|
||||||
},
|
|
||||||
voiInverted: true,
|
|
||||||
},
|
|
||||||
id: 'ptDisplaySet',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
viewportOptions: {
|
|
||||||
viewportId: 'ptSAGITTAL',
|
|
||||||
viewportType: 'volume',
|
|
||||||
orientation: 'sagittal',
|
|
||||||
background: [1, 1, 1],
|
|
||||||
toolGroupId: 'ptToolGroup',
|
|
||||||
syncGroups: [
|
|
||||||
{
|
|
||||||
type: 'cameraPosition',
|
|
||||||
id: 'sagittalSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'ptWLSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'ptFusionWLSync',
|
|
||||||
source: true,
|
|
||||||
target: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
displaySets: [
|
|
||||||
{
|
|
||||||
options: {
|
|
||||||
voi: {
|
|
||||||
windowWidth: 5,
|
|
||||||
windowCenter: 2.5,
|
|
||||||
},
|
|
||||||
voiInverted: true,
|
|
||||||
},
|
|
||||||
id: 'ptDisplaySet',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
viewportOptions: {
|
|
||||||
viewportId: 'ptCORONAL',
|
|
||||||
viewportType: 'volume',
|
|
||||||
orientation: 'coronal',
|
|
||||||
background: [1, 1, 1],
|
|
||||||
toolGroupId: 'ptToolGroup',
|
|
||||||
syncGroups: [
|
|
||||||
{
|
|
||||||
type: 'cameraPosition',
|
|
||||||
id: 'coronalSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'ptWLSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'ptFusionWLSync',
|
|
||||||
source: true,
|
|
||||||
target: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
displaySets: [
|
|
||||||
{
|
|
||||||
options: {
|
|
||||||
voi: {
|
|
||||||
windowWidth: 5,
|
|
||||||
windowCenter: 2.5,
|
|
||||||
},
|
|
||||||
voiInverted: true,
|
|
||||||
},
|
|
||||||
id: 'ptDisplaySet',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
viewportOptions: {
|
|
||||||
viewportId: 'fusionAXIAL',
|
|
||||||
viewportType: 'volume',
|
|
||||||
orientation: 'axial',
|
|
||||||
toolGroupId: 'fusionToolGroup',
|
|
||||||
initialImageOptions: {
|
|
||||||
// index: 5,
|
|
||||||
preset: 'first', // 'first', 'last', 'middle'
|
|
||||||
},
|
|
||||||
syncGroups: [
|
|
||||||
{
|
|
||||||
type: 'cameraPosition',
|
|
||||||
id: 'axialSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'ctWLSync',
|
|
||||||
source: false,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'fusionWLSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'ptFusionWLSync',
|
|
||||||
source: false,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
displaySets: [
|
|
||||||
{
|
|
||||||
id: 'ctDisplaySet',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
options: {
|
|
||||||
colormap: 'hsv',
|
|
||||||
voi: {
|
|
||||||
windowWidth: 5,
|
|
||||||
windowCenter: 2.5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
id: 'ptDisplaySet',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
viewportOptions: {
|
|
||||||
viewportId: 'fusionSAGITTAL',
|
|
||||||
viewportType: 'volume',
|
|
||||||
orientation: 'sagittal',
|
|
||||||
toolGroupId: 'fusionToolGroup',
|
|
||||||
// initialImageOptions: {
|
|
||||||
// index: 180,
|
|
||||||
// preset: 'middle', // 'first', 'last', 'middle'
|
|
||||||
// },
|
|
||||||
syncGroups: [
|
|
||||||
{
|
|
||||||
type: 'cameraPosition',
|
|
||||||
id: 'sagittalSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'ctWLSync',
|
|
||||||
source: false,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'fusionWLSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'ptFusionWLSync',
|
|
||||||
source: false,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
displaySets: [
|
|
||||||
{
|
|
||||||
id: 'ctDisplaySet',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
options: {
|
|
||||||
colormap: 'hsv',
|
|
||||||
voi: {
|
|
||||||
windowWidth: 5,
|
|
||||||
windowCenter: 2.5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
id: 'ptDisplaySet',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
viewportOptions: {
|
|
||||||
viewportId: 'fusionCoronal',
|
|
||||||
viewportType: 'volume',
|
|
||||||
orientation: 'coronal',
|
|
||||||
toolGroupId: 'fusionToolGroup',
|
|
||||||
// initialImageOptions: {
|
|
||||||
// index: 180,
|
|
||||||
// preset: 'middle', // 'first', 'last', 'middle'
|
|
||||||
// },
|
|
||||||
syncGroups: [
|
|
||||||
{
|
|
||||||
type: 'cameraPosition',
|
|
||||||
id: 'coronalSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'ctWLSync',
|
|
||||||
source: false,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'fusionWLSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'ptFusionWLSync',
|
|
||||||
source: false,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
displaySets: [
|
|
||||||
{
|
|
||||||
id: 'ctDisplaySet',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
options: {
|
|
||||||
colormap: 'hsv',
|
|
||||||
voi: {
|
|
||||||
windowWidth: 5,
|
|
||||||
windowCenter: 2.5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
id: 'ptDisplaySet',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
viewportOptions: {
|
|
||||||
viewportId: 'mipSagittal',
|
|
||||||
viewportType: 'volume',
|
|
||||||
orientation: 'sagittal',
|
|
||||||
background: [1, 1, 1],
|
|
||||||
toolGroupId: 'mipToolGroup',
|
|
||||||
syncGroups: [
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'ptWLSync',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'voi',
|
|
||||||
id: 'ptFusionWLSync',
|
|
||||||
source: true,
|
|
||||||
target: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
// Custom props can be used to set custom properties which extensions
|
|
||||||
// can react on.
|
|
||||||
customViewportProps: {
|
|
||||||
// We use viewportDisplay to filter the viewports which are displayed
|
|
||||||
// in mip and we set the scrollbar according to their rotation index
|
|
||||||
// in the cornerstone extension.
|
|
||||||
hideOverlays: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
displaySets: [
|
|
||||||
{
|
|
||||||
options: {
|
|
||||||
blendMode: 'MIP',
|
|
||||||
slabThickness: 'fullVolume',
|
|
||||||
voi: {
|
|
||||||
windowWidth: 5,
|
|
||||||
windowCenter: 2.5,
|
|
||||||
},
|
|
||||||
voiInverted: true,
|
|
||||||
},
|
|
||||||
id: 'ptDisplaySet',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
createdDate: '2021-02-23T18:32:42.850Z',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
numberOfPriorsReferenced: -1,
|
numberOfPriorsReferenced: -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getHangingProtocolModule() {
|
function getHangingProtocolModule() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: ptCT.id,
|
name: ptCT.id,
|
||||||
protocol: ptCT,
|
protocol: ptCT,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
438
extensions/tmtv/src/utils/hpViewports.ts
Normal file
438
extensions/tmtv/src/utils/hpViewports.ts
Normal file
@ -0,0 +1,438 @@
|
|||||||
|
const ctAXIAL = {
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'ctAXIAL',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'axial',
|
||||||
|
toolGroupId: 'ctToolGroup',
|
||||||
|
initialImageOptions: {
|
||||||
|
// index: 5,
|
||||||
|
preset: 'first', // 'first', 'last', 'middle'
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'axialSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ctWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'ctDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const ctSAGITTAL = {
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'ctSAGITTAL',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'sagittal',
|
||||||
|
toolGroupId: 'ctToolGroup',
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'sagittalSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ctWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'ctDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const ctCORONAL = {
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'ctCORONAL',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'coronal',
|
||||||
|
toolGroupId: 'ctToolGroup',
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'coronalSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ctWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'ctDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const ptAXIAL = {
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'ptAXIAL',
|
||||||
|
viewportType: 'volume',
|
||||||
|
background: [1, 1, 1],
|
||||||
|
orientation: 'axial',
|
||||||
|
toolGroupId: 'ptToolGroup',
|
||||||
|
initialImageOptions: {
|
||||||
|
// index: 5,
|
||||||
|
preset: 'first', // 'first', 'last', 'middle'
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'axialSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ptWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ptFusionWLSync',
|
||||||
|
source: true,
|
||||||
|
target: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
options: {
|
||||||
|
voi: {
|
||||||
|
windowWidth: 5,
|
||||||
|
windowCenter: 2.5,
|
||||||
|
},
|
||||||
|
voiInverted: true,
|
||||||
|
},
|
||||||
|
id: 'ptDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const ptSAGITTAL = {
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'ptSAGITTAL',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'sagittal',
|
||||||
|
background: [1, 1, 1],
|
||||||
|
toolGroupId: 'ptToolGroup',
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'sagittalSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ptWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ptFusionWLSync',
|
||||||
|
source: true,
|
||||||
|
target: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
options: {
|
||||||
|
voi: {
|
||||||
|
windowWidth: 5,
|
||||||
|
windowCenter: 2.5,
|
||||||
|
},
|
||||||
|
voiInverted: true,
|
||||||
|
},
|
||||||
|
id: 'ptDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const ptCORONAL = {
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'ptCORONAL',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'coronal',
|
||||||
|
background: [1, 1, 1],
|
||||||
|
toolGroupId: 'ptToolGroup',
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'coronalSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ptWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ptFusionWLSync',
|
||||||
|
source: true,
|
||||||
|
target: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
options: {
|
||||||
|
voi: {
|
||||||
|
windowWidth: 5,
|
||||||
|
windowCenter: 2.5,
|
||||||
|
},
|
||||||
|
voiInverted: true,
|
||||||
|
},
|
||||||
|
id: 'ptDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const fusionAXIAL = {
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'fusionAXIAL',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'axial',
|
||||||
|
toolGroupId: 'fusionToolGroup',
|
||||||
|
initialImageOptions: {
|
||||||
|
// index: 5,
|
||||||
|
preset: 'first', // 'first', 'last', 'middle'
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'axialSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ctWLSync',
|
||||||
|
source: false,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'fusionWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ptFusionWLSync',
|
||||||
|
source: false,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'ctDisplaySet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
options: {
|
||||||
|
colormap: 'hsv',
|
||||||
|
voi: {
|
||||||
|
windowWidth: 5,
|
||||||
|
windowCenter: 2.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: 'ptDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const fusionSAGITTAL = {
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'fusionSAGITTAL',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'sagittal',
|
||||||
|
toolGroupId: 'fusionToolGroup',
|
||||||
|
// initialImageOptions: {
|
||||||
|
// index: 180,
|
||||||
|
// preset: 'middle', // 'first', 'last', 'middle'
|
||||||
|
// },
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'sagittalSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ctWLSync',
|
||||||
|
source: false,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'fusionWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ptFusionWLSync',
|
||||||
|
source: false,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'ctDisplaySet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
options: {
|
||||||
|
colormap: 'hsv',
|
||||||
|
voi: {
|
||||||
|
windowWidth: 5,
|
||||||
|
windowCenter: 2.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: 'ptDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const fusionCORONAL = {
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'fusionCoronal',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'coronal',
|
||||||
|
toolGroupId: 'fusionToolGroup',
|
||||||
|
// initialImageOptions: {
|
||||||
|
// index: 180,
|
||||||
|
// preset: 'middle', // 'first', 'last', 'middle'
|
||||||
|
// },
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'coronalSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ctWLSync',
|
||||||
|
source: false,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'fusionWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ptFusionWLSync',
|
||||||
|
source: false,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'ctDisplaySet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
options: {
|
||||||
|
colormap: 'hsv',
|
||||||
|
voi: {
|
||||||
|
windowWidth: 5,
|
||||||
|
windowCenter: 2.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: 'ptDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mipSAGITTAL = {
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'mipSagittal',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'sagittal',
|
||||||
|
background: [1, 1, 1],
|
||||||
|
toolGroupId: 'mipToolGroup',
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ptWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ptFusionWLSync',
|
||||||
|
source: true,
|
||||||
|
target: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// Custom props can be used to set custom properties which extensions
|
||||||
|
// can react on.
|
||||||
|
customViewportProps: {
|
||||||
|
// We use viewportDisplay to filter the viewports which are displayed
|
||||||
|
// in mip and we set the scrollbar according to their rotation index
|
||||||
|
// in the cornerstone extension.
|
||||||
|
hideOverlays: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
options: {
|
||||||
|
blendMode: 'MIP',
|
||||||
|
slabThickness: 'fullVolume',
|
||||||
|
voi: {
|
||||||
|
windowWidth: 5,
|
||||||
|
windowCenter: 2.5,
|
||||||
|
},
|
||||||
|
voiInverted: true,
|
||||||
|
},
|
||||||
|
id: 'ptDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
ctAXIAL,
|
||||||
|
ctSAGITTAL,
|
||||||
|
ctCORONAL,
|
||||||
|
ptAXIAL,
|
||||||
|
ptSAGITTAL,
|
||||||
|
ptCORONAL,
|
||||||
|
fusionAXIAL,
|
||||||
|
fusionSAGITTAL,
|
||||||
|
fusionCORONAL,
|
||||||
|
mipSAGITTAL,
|
||||||
|
};
|
||||||
@ -140,7 +140,6 @@ function modeFactory({ modeConfiguration }) {
|
|||||||
toolbarService,
|
toolbarService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
toolbarService.reset();
|
|
||||||
toolGroupService.destroy();
|
toolGroupService.destroy();
|
||||||
},
|
},
|
||||||
validationTags: {
|
validationTags: {
|
||||||
|
|||||||
@ -72,6 +72,7 @@ function modeFactory() {
|
|||||||
measurementService,
|
measurementService,
|
||||||
toolbarService,
|
toolbarService,
|
||||||
toolGroupService,
|
toolGroupService,
|
||||||
|
customizationService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
measurementService.clearMeasurements();
|
measurementService.clearMeasurements();
|
||||||
@ -79,6 +80,11 @@ function modeFactory() {
|
|||||||
// Init Default and SR ToolGroups
|
// Init Default and SR ToolGroups
|
||||||
initToolGroups(extensionManager, toolGroupService, commandsManager);
|
initToolGroups(extensionManager, toolGroupService, commandsManager);
|
||||||
|
|
||||||
|
// init customizations
|
||||||
|
customizationService.addModeCustomizations([
|
||||||
|
'@ohif/extension-test.customizationModule.custom-context-menu',
|
||||||
|
]);
|
||||||
|
|
||||||
let unsubscribe;
|
let unsubscribe;
|
||||||
|
|
||||||
const activateTool = () => {
|
const activateTool = () => {
|
||||||
@ -132,7 +138,6 @@ function modeFactory() {
|
|||||||
cornerstoneViewportService,
|
cornerstoneViewportService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
toolbarService.reset();
|
|
||||||
toolGroupService.destroy();
|
toolGroupService.destroy();
|
||||||
syncGroupService.destroy();
|
syncGroupService.destroy();
|
||||||
segmentationService.destroy();
|
segmentationService.destroy();
|
||||||
@ -205,7 +210,12 @@ function modeFactory() {
|
|||||||
dicompdf.sopClassHandler,
|
dicompdf.sopClassHandler,
|
||||||
dicomsr.sopClassHandler,
|
dicomsr.sopClassHandler,
|
||||||
],
|
],
|
||||||
hotkeys: [...hotkeys.defaults.hotkeyBindings],
|
hotkeys: {
|
||||||
|
// Don't store the hotkeys for basic-test-mode under the same key
|
||||||
|
// because they get customized by tests
|
||||||
|
name: 'basic-test-hotkeys',
|
||||||
|
hotkeys: [...hotkeys.defaults.hotkeyBindings],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -297,10 +297,96 @@ const toolbarButtons = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'Layout',
|
id: 'Layout',
|
||||||
type: 'ohif.layoutSelector',
|
type: 'ohif.splitButton',
|
||||||
props: {
|
props: {
|
||||||
rows: 3,
|
groupId: 'LayoutTools',
|
||||||
columns: 3,
|
isRadio: false,
|
||||||
|
primary: {
|
||||||
|
id: 'Layout',
|
||||||
|
type: 'action',
|
||||||
|
uiType: 'ohif.layoutSelector',
|
||||||
|
icon: 'tool-layout',
|
||||||
|
label: 'Grid Layout',
|
||||||
|
props: {
|
||||||
|
rows: 4,
|
||||||
|
columns: 4,
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
commandName: 'setLayout',
|
||||||
|
commandOptions: {},
|
||||||
|
context: 'CORNERSTONE',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
icon: 'chevron-down',
|
||||||
|
label: '',
|
||||||
|
isActive: true,
|
||||||
|
tooltip: 'Hanging Protocols',
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '2x2',
|
||||||
|
type: 'action',
|
||||||
|
label: '2x2',
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
commandName: 'setHangingProtocol',
|
||||||
|
commandOptions: {
|
||||||
|
protocolId: '@ohif/hp-extension.mn',
|
||||||
|
stageId: '2x2',
|
||||||
|
},
|
||||||
|
context: 'DEFAULT',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3x1',
|
||||||
|
type: 'action',
|
||||||
|
label: '3x1',
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
commandName: 'setHangingProtocol',
|
||||||
|
commandOptions: {
|
||||||
|
protocolId: '@ohif/hp-extension.mn',
|
||||||
|
stageId: '3x1',
|
||||||
|
},
|
||||||
|
context: 'DEFAULT',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2x1',
|
||||||
|
type: 'action',
|
||||||
|
label: '2x1',
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
commandName: 'setHangingProtocol',
|
||||||
|
commandOptions: {
|
||||||
|
protocolId: '@ohif/hp-extension.mn',
|
||||||
|
stageId: '2x1',
|
||||||
|
},
|
||||||
|
context: 'DEFAULT',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1x1',
|
||||||
|
type: 'action',
|
||||||
|
label: '1x1',
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
commandName: 'setHangingProtocol',
|
||||||
|
commandOptions: {
|
||||||
|
protocolId: '@ohif/hp-extension.mn',
|
||||||
|
stageId: '1x1',
|
||||||
|
},
|
||||||
|
context: 'DEFAULT',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -312,9 +398,11 @@ const toolbarButtons = [
|
|||||||
label: 'MPR',
|
label: 'MPR',
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
commandName: 'toggleMPR',
|
commandName: 'toggleHangingProtocol',
|
||||||
commandOptions: {},
|
commandOptions: {
|
||||||
context: 'CORNERSTONE',
|
protocolId: 'mpr',
|
||||||
|
},
|
||||||
|
context: 'DEFAULT',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -159,7 +159,6 @@ function modeFactory() {
|
|||||||
_activatePanelTriggersSubscriptions.forEach(sub => sub.unsubscribe());
|
_activatePanelTriggersSubscriptions.forEach(sub => sub.unsubscribe());
|
||||||
_activatePanelTriggersSubscriptions = [];
|
_activatePanelTriggersSubscriptions = [];
|
||||||
|
|
||||||
toolbarService.reset();
|
|
||||||
toolGroupService.destroy();
|
toolGroupService.destroy();
|
||||||
syncGroupService.destroy();
|
syncGroupService.destroy();
|
||||||
segmentationService.destroy();
|
segmentationService.destroy();
|
||||||
|
|||||||
@ -215,6 +215,32 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) {
|
|||||||
|
|
||||||
toolGroupService.createToolGroupAndAddTools('mpr', tools, toolsConfig);
|
toolGroupService.createToolGroupAndAddTools('mpr', tools, toolsConfig);
|
||||||
}
|
}
|
||||||
|
function initVolume3DToolGroup(extensionManager, toolGroupService) {
|
||||||
|
const utilityModule = extensionManager.getModuleEntry(
|
||||||
|
'@ohif/extension-cornerstone.utilityModule.tools'
|
||||||
|
);
|
||||||
|
|
||||||
|
const { toolNames, Enums } = utilityModule.exports;
|
||||||
|
|
||||||
|
const tools = {
|
||||||
|
active: [
|
||||||
|
{
|
||||||
|
toolName: toolNames.TrackballRotateTool,
|
||||||
|
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toolName: toolNames.Zoom,
|
||||||
|
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toolName: toolNames.Pan,
|
||||||
|
bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
toolGroupService.createToolGroupAndAddTools('volume3d', tools);
|
||||||
|
}
|
||||||
|
|
||||||
function initToolGroups(extensionManager, toolGroupService, commandsManager) {
|
function initToolGroups(extensionManager, toolGroupService, commandsManager) {
|
||||||
initDefaultToolGroup(
|
initDefaultToolGroup(
|
||||||
@ -225,6 +251,7 @@ function initToolGroups(extensionManager, toolGroupService, commandsManager) {
|
|||||||
);
|
);
|
||||||
initSRToolGroup(extensionManager, toolGroupService, commandsManager);
|
initSRToolGroup(extensionManager, toolGroupService, commandsManager);
|
||||||
initMPRToolGroup(extensionManager, toolGroupService, commandsManager);
|
initMPRToolGroup(extensionManager, toolGroupService, commandsManager);
|
||||||
|
initVolume3DToolGroup(extensionManager, toolGroupService);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default initToolGroups;
|
export default initToolGroups;
|
||||||
|
|||||||
@ -27,18 +27,6 @@ function _createButton(type, id, icon, label, commands, tooltip, uiType) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function _createCommands(commandName, toolName, toolGroupIds) {
|
|
||||||
return toolGroupIds.map(toolGroupId => ({
|
|
||||||
/* It's a command that is being run when the button is clicked. */
|
|
||||||
commandName,
|
|
||||||
commandOptions: {
|
|
||||||
toolName,
|
|
||||||
toolGroupId,
|
|
||||||
},
|
|
||||||
context: 'CORNERSTONE',
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const _createActionButton = _createButton.bind(null, 'action');
|
const _createActionButton = _createButton.bind(null, 'action');
|
||||||
const _createToggleButton = _createButton.bind(null, 'toggle');
|
const _createToggleButton = _createButton.bind(null, 'toggle');
|
||||||
const _createToolButton = _createButton.bind(null, 'tool');
|
const _createToolButton = _createButton.bind(null, 'tool');
|
||||||
@ -67,6 +55,26 @@ function _createWwwcPreset(preset, title, subtitle) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toolGroupIds = ['default', 'mpr', 'SRToolGroup'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an array of 'setToolActive' commands for the given toolName - one for
|
||||||
|
* each toolGroupId specified in toolGroupIds.
|
||||||
|
* @param {string} toolName
|
||||||
|
* @returns {Array} an array of 'setToolActive' commands
|
||||||
|
*/
|
||||||
|
function _createSetToolActiveCommands(toolName) {
|
||||||
|
const temp = toolGroupIds.map(toolGroupId => ({
|
||||||
|
commandName: 'setToolActive',
|
||||||
|
commandOptions: {
|
||||||
|
toolGroupId,
|
||||||
|
toolName,
|
||||||
|
},
|
||||||
|
context: 'CORNERSTONE',
|
||||||
|
}));
|
||||||
|
return temp;
|
||||||
|
}
|
||||||
|
|
||||||
const toolbarButtons = [
|
const toolbarButtons = [
|
||||||
// Measurement
|
// Measurement
|
||||||
{
|
{
|
||||||
@ -211,15 +219,7 @@ const toolbarButtons = [
|
|||||||
type: 'tool',
|
type: 'tool',
|
||||||
icon: 'tool-zoom',
|
icon: 'tool-zoom',
|
||||||
label: 'Zoom',
|
label: 'Zoom',
|
||||||
commands: [
|
commands: _createSetToolActiveCommands('Zoom'),
|
||||||
{
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: {
|
|
||||||
toolName: 'Zoom',
|
|
||||||
},
|
|
||||||
context: 'CORNERSTONE',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Window Level + Presets...
|
// Window Level + Presets...
|
||||||
@ -268,15 +268,7 @@ const toolbarButtons = [
|
|||||||
type: 'tool',
|
type: 'tool',
|
||||||
icon: 'tool-move',
|
icon: 'tool-move',
|
||||||
label: 'Pan',
|
label: 'Pan',
|
||||||
commands: [
|
commands: _createSetToolActiveCommands('Pan'),
|
||||||
{
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: {
|
|
||||||
toolName: 'Pan',
|
|
||||||
},
|
|
||||||
context: 'CORNERSTONE',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -312,9 +304,11 @@ const toolbarButtons = [
|
|||||||
label: 'MPR',
|
label: 'MPR',
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
commandName: 'toggleMPR',
|
commandName: 'toggleHangingProtocol',
|
||||||
commandOptions: {},
|
commandOptions: {
|
||||||
context: 'CORNERSTONE',
|
protocolId: 'mpr',
|
||||||
|
},
|
||||||
|
context: 'DEFAULT',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -330,8 +324,8 @@ const toolbarButtons = [
|
|||||||
{
|
{
|
||||||
commandName: 'setToolActive',
|
commandName: 'setToolActive',
|
||||||
commandOptions: {
|
commandOptions: {
|
||||||
toolGroupId: 'mpr',
|
|
||||||
toolName: 'Crosshairs',
|
toolName: 'Crosshairs',
|
||||||
|
toolGroupId: 'mpr',
|
||||||
},
|
},
|
||||||
context: 'CORNERSTONE',
|
context: 'CORNERSTONE',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -136,13 +136,11 @@ function modeFactory({ modeConfiguration }) {
|
|||||||
const {
|
const {
|
||||||
toolGroupService,
|
toolGroupService,
|
||||||
syncGroupService,
|
syncGroupService,
|
||||||
toolbarService,
|
|
||||||
segmentationService,
|
segmentationService,
|
||||||
cornerstoneViewportService,
|
cornerstoneViewportService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
unsubscriptions.forEach(unsubscribe => unsubscribe());
|
unsubscriptions.forEach(unsubscribe => unsubscribe());
|
||||||
toolbarService.reset();
|
|
||||||
toolGroupService.destroy();
|
toolGroupService.destroy();
|
||||||
syncGroupService.destroy();
|
syncGroupService.destroy();
|
||||||
segmentationService.destroy();
|
segmentationService.destroy();
|
||||||
|
|||||||
@ -203,9 +203,11 @@ const toolbarButtons = [
|
|||||||
label: 'MPR',
|
label: 'MPR',
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
commandName: 'toggleMPR',
|
commandName: 'toggleHangingProtocol',
|
||||||
commandOptions: {},
|
commandOptions: {
|
||||||
context: 'CORNERSTONE',
|
protocolId: 'mpr',
|
||||||
|
},
|
||||||
|
context: 'DEFAULT',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -79,7 +79,7 @@
|
|||||||
"@types/jest": "^27.5.0",
|
"@types/jest": "^27.5.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.19.0",
|
"@typescript-eslint/eslint-plugin": "^4.19.0",
|
||||||
"@typescript-eslint/parser": "^4.19.0",
|
"@typescript-eslint/parser": "^4.19.0",
|
||||||
"autoprefixer": "10.4.4",
|
"autoprefixer": "^10.4.4",
|
||||||
"babel-eslint": "9.x",
|
"babel-eslint": "9.x",
|
||||||
"babel-loader": "^8.2.4",
|
"babel-loader": "^8.2.4",
|
||||||
"babel-plugin-inline-react-svg": "1.1.0",
|
"babel-plugin-inline-react-svg": "1.1.0",
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"cornerstone-math": "0.1.9",
|
"cornerstone-math": "0.1.9",
|
||||||
"cornerstone-wado-image-loader": "^4.2.1",
|
"cornerstone-wado-image-loader": "^4.13.0",
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"@ohif/ui": "^2.0.0"
|
"@ohif/ui": "^2.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import log from '../log.js';
|
import log from '../log.js';
|
||||||
|
import { Command, Commands } from '../types/Command';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The definition of a command
|
* The definition of a command
|
||||||
@ -105,8 +106,8 @@ export class CommandsManager {
|
|||||||
* @param {String} commandName - Command to find
|
* @param {String} commandName - Command to find
|
||||||
* @param {String} [contextName] - Specific command to look in. Defaults to current activeContexts
|
* @param {String} [contextName] - Specific command to look in. Defaults to current activeContexts
|
||||||
*/
|
*/
|
||||||
getCommand = (commandName, contextName) => {
|
getCommand = (commandName: string, contextName?: string) => {
|
||||||
let contexts = [];
|
const contexts = [];
|
||||||
|
|
||||||
if (contextName) {
|
if (contextName) {
|
||||||
const context = this.getContext(contextName);
|
const context = this.getContext(contextName);
|
||||||
@ -140,7 +141,7 @@ export class CommandsManager {
|
|||||||
* @param {Object} [options={}] - Extra options to pass the command. Like a mousedown event
|
* @param {Object} [options={}] - Extra options to pass the command. Like a mousedown event
|
||||||
* @param {String} [contextName]
|
* @param {String} [contextName]
|
||||||
*/
|
*/
|
||||||
runCommand(commandName, options = {}, contextName) {
|
public runCommand(commandName: string, options = {}, contextName?: string) {
|
||||||
const definition = this.getCommand(commandName, contextName);
|
const definition = this.getCommand(commandName, contextName);
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
log.warn(`Command "${commandName}" not found in current context`);
|
log.warn(`Command "${commandName}" not found in current context`);
|
||||||
@ -161,6 +162,50 @@ export class CommandsManager {
|
|||||||
return commandFn(commandParams);
|
return commandFn(commandParams);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run one or more commands with specified extra options.
|
||||||
|
* Returns the result of the last command run.
|
||||||
|
*
|
||||||
|
* @param toRun - A specification of one or more commands
|
||||||
|
* @param options - to include in the commands run beyond
|
||||||
|
* the commandOptions specified in the base.
|
||||||
|
*/
|
||||||
|
public run(
|
||||||
|
toRun: Command | Commands | Command[] | undefined,
|
||||||
|
options?: Record<string, unknown>
|
||||||
|
): unknown {
|
||||||
|
if (!toRun) return;
|
||||||
|
const commands =
|
||||||
|
(Array.isArray(toRun) && toRun) ||
|
||||||
|
((toRun as Command).commandName && [toRun]) ||
|
||||||
|
(Array.isArray((toRun as Commands).commands) &&
|
||||||
|
(toRun as Commands).commands);
|
||||||
|
if (!commands) {
|
||||||
|
console.log("Command isn't runnable", toRun);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result;
|
||||||
|
(commands as Command[]).forEach(
|
||||||
|
({ commandName, commandOptions, context }) => {
|
||||||
|
if (commandName) {
|
||||||
|
result = this.runCommand(
|
||||||
|
commandName,
|
||||||
|
{
|
||||||
|
...commandOptions,
|
||||||
|
...options,
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.warn('No command name supplied in', toRun);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CommandsManager;
|
export default CommandsManager;
|
||||||
|
|||||||
@ -64,19 +64,17 @@ export class HotkeysManager {
|
|||||||
*
|
*
|
||||||
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
||||||
*/
|
*/
|
||||||
setHotkeys(hotkeyDefinitions = [], key = 'hotkey-definitions') {
|
setHotkeys(hotkeyDefinitions = [], name = 'hotkey-definitions') {
|
||||||
try {
|
try {
|
||||||
const definitions = this.getValidDefinitions(hotkeyDefinitions);
|
const definitions = this.getValidDefinitions(hotkeyDefinitions);
|
||||||
if (isequal(definitions, this.hotkeyDefaults)) {
|
if (isequal(definitions, this.hotkeyDefaults)) {
|
||||||
console.log('hotkeys REMOVING unused definition', key);
|
localStorage.removeItem(name);
|
||||||
localStorage.removeItem(key);
|
|
||||||
} else {
|
} else {
|
||||||
console.log('hotkeys setting local storage', key);
|
localStorage.setItem(name, JSON.stringify(definitions));
|
||||||
localStorage.setItem(key, JSON.stringify(definitions));
|
|
||||||
}
|
}
|
||||||
definitions.forEach(definition => this.registerHotkeys(definition));
|
definitions.forEach(definition => this.registerHotkeys(definition));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const { uiNotificationService, } = this._servicesManager.services;
|
const { uiNotificationService } = this._servicesManager.services;
|
||||||
uiNotificationService.show({
|
uiNotificationService.show({
|
||||||
title: 'Hotkeys Manager',
|
title: 'Hotkeys Manager',
|
||||||
message: 'Error while setting hotkeys',
|
message: 'Error while setting hotkeys',
|
||||||
|
|||||||
@ -412,6 +412,34 @@ class MetadataProvider {
|
|||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the frameNumber information, depending on the url style
|
||||||
|
* wadors /frames/1
|
||||||
|
* wadouri &frame=1
|
||||||
|
* @param {*} imageId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getFrameInformationFromURL(imageId) {
|
||||||
|
function getInformationFromURL(informationString, separator) {
|
||||||
|
let result = '';
|
||||||
|
const splittedStr = imageId.split(informationString)[1];
|
||||||
|
if (splittedStr.includes(separator)) {
|
||||||
|
result = splittedStr.split(separator)[0];
|
||||||
|
} else {
|
||||||
|
result = splittedStr;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageId.includes('/frames')) {
|
||||||
|
return getInformationFromURL('/frames', '/');
|
||||||
|
}
|
||||||
|
if (imageId.includes('&frame=')) {
|
||||||
|
return getInformationFromURL('&frame=', '&');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
getUIDsFromImageID(imageId) {
|
getUIDsFromImageID(imageId) {
|
||||||
// TODO: adding csiv here is not really correct. Probably need to use
|
// TODO: adding csiv here is not really correct. Probably need to use
|
||||||
// metadataProvider.addImageIdToUIDs(imageId, {
|
// metadataProvider.addImageIdToUIDs(imageId, {
|
||||||
@ -445,7 +473,7 @@ class MetadataProvider {
|
|||||||
// check if the imageId starts with http:// or https:// using regex
|
// check if the imageId starts with http:// or https:// using regex
|
||||||
// Todo: handle non http imageIds
|
// Todo: handle non http imageIds
|
||||||
let imageURI;
|
let imageURI;
|
||||||
const urlRegex = /^(http|https):\/\//;
|
const urlRegex = /^(http|https|dicomfile):\/\//;
|
||||||
if (urlRegex.test(imageId)) {
|
if (urlRegex.test(imageId)) {
|
||||||
imageURI = imageId;
|
imageURI = imageId;
|
||||||
} else {
|
} else {
|
||||||
@ -453,7 +481,7 @@ class MetadataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uids = this.imageURIToUIDs.get(imageURI);
|
const uids = this.imageURIToUIDs.get(imageURI);
|
||||||
const frameNumber = imageId.split(/\/frames\//)[1];
|
let frameNumber = this.getFrameInformationFromURL(imageId) || '1';
|
||||||
|
|
||||||
if (uids && frameNumber !== undefined) {
|
if (uids && frameNumber !== undefined) {
|
||||||
return { ...uids, frameNumber };
|
return { ...uids, frameNumber };
|
||||||
|
|||||||
@ -88,6 +88,20 @@ const bindings = [
|
|||||||
// keys: ['pagedown'],
|
// keys: ['pagedown'],
|
||||||
// isEditable: true,
|
// isEditable: true,
|
||||||
// },
|
// },
|
||||||
|
{
|
||||||
|
commandName: 'nextStage',
|
||||||
|
context: 'DEFAULT',
|
||||||
|
label: 'Next Stage',
|
||||||
|
keys: ['.'],
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandName: 'previousStage',
|
||||||
|
context: 'DEFAULT',
|
||||||
|
label: 'Previous Stage',
|
||||||
|
keys: [','],
|
||||||
|
isEditable: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
commandName: 'nextImage',
|
commandName: 'nextImage',
|
||||||
label: 'Next Image',
|
label: 'Next Image',
|
||||||
|
|||||||
@ -206,40 +206,40 @@ describe('ExtensionManager.ts', () => {
|
|||||||
const extension = {
|
const extension = {
|
||||||
id: 'hello-world',
|
id: 'hello-world',
|
||||||
getViewportModule: () => {
|
getViewportModule: () => {
|
||||||
return [{}];
|
return [{ name: 'test' }];
|
||||||
},
|
},
|
||||||
getSopClassHandlerModule: () => {
|
getSopClassHandlerModule: () => {
|
||||||
return [{}];
|
return [{ name: 'test' }];
|
||||||
},
|
},
|
||||||
getPanelModule: () => {
|
getPanelModule: () => {
|
||||||
return [{}];
|
return [{ name: 'test' }];
|
||||||
},
|
},
|
||||||
getToolbarModule: () => {
|
getToolbarModule: () => {
|
||||||
return [{}];
|
return [{ name: 'test' }];
|
||||||
},
|
},
|
||||||
getCommandsModule: () => {
|
getCommandsModule: () => {
|
||||||
return [{}];
|
return [{ name: 'test' }];
|
||||||
},
|
},
|
||||||
getLayoutTemplateModule: () => {
|
getLayoutTemplateModule: () => {
|
||||||
return [{}];
|
return [{ name: 'test' }];
|
||||||
},
|
},
|
||||||
getDataSourcesModule: () => {
|
getDataSourcesModule: () => {
|
||||||
return [{}];
|
return [{ name: 'test' }];
|
||||||
},
|
},
|
||||||
getHangingProtocolModule: () => {
|
getHangingProtocolModule: () => {
|
||||||
return [{}];
|
return [{ name: 'test' }];
|
||||||
},
|
},
|
||||||
getContextModule: () => {
|
getContextModule: () => {
|
||||||
return [{}];
|
return [{ name: 'test' }];
|
||||||
},
|
},
|
||||||
getUtilityModule: () => {
|
getUtilityModule: () => {
|
||||||
return [{}];
|
return [{ name: 'test' }];
|
||||||
},
|
},
|
||||||
getCustomizationModule: () => {
|
getCustomizationModule: () => {
|
||||||
return [{}];
|
return [{ name: 'test' }];
|
||||||
},
|
},
|
||||||
getStateSyncModule: () => {
|
getStateSyncModule: () => {
|
||||||
return [{}];
|
return [{ name: 'test' }];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,10 @@ export interface ExtensionParams extends ExtensionConstructor {
|
|||||||
*/
|
*/
|
||||||
export interface Extension {
|
export interface Extension {
|
||||||
id: string;
|
id: string;
|
||||||
preRegistration?: (p: ExtensionParams) => void;
|
preRegistration?: (p: ExtensionParams) => Promise<void> | void;
|
||||||
|
onModeExit?: () => void;
|
||||||
|
getHangingProtocolModule?: (p: ExtensionParams) => unknown;
|
||||||
|
getCommandsModule?: (p: ExtensionParams) => CommandsModule;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExtensionRegister = {
|
export type ExtensionRegister = {
|
||||||
@ -44,6 +47,12 @@ export type ExtensionRegister = {
|
|||||||
create: (p: ExtensionParams) => Extension;
|
create: (p: ExtensionParams) => Extension;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CommandsModule = {
|
||||||
|
actions: Record<string, unknown>;
|
||||||
|
definitions: Record<string, unknown>;
|
||||||
|
defaultContext?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default class ExtensionManager {
|
export default class ExtensionManager {
|
||||||
private _commandsManager: CommandsManager;
|
private _commandsManager: CommandsManager;
|
||||||
private _servicesManager: ServicesManager;
|
private _servicesManager: ServicesManager;
|
||||||
@ -274,6 +283,11 @@ export default class ExtensionManager {
|
|||||||
// Default for most extension points,
|
// Default for most extension points,
|
||||||
// Just adds each entry ready for consumption by mode.
|
// Just adds each entry ready for consumption by mode.
|
||||||
extensionModule.forEach(element => {
|
extensionModule.forEach(element => {
|
||||||
|
if (!element.name) {
|
||||||
|
throw new Error(
|
||||||
|
`Extension ID ${extensionId} module ${moduleType} element has no name`
|
||||||
|
);
|
||||||
|
}
|
||||||
const id = `${extensionId}.${moduleType}.${element.name}`;
|
const id = `${extensionId}.${moduleType}.${element.name}`;
|
||||||
element.id = id;
|
element.id = id;
|
||||||
this.modulesMap[id] = element;
|
this.modulesMap[id] = element;
|
||||||
@ -331,7 +345,7 @@ export default class ExtensionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const extensionModule = getModuleFn({
|
const extensionModule = extension[getModuleFnName]({
|
||||||
appConfig: this._appConfig,
|
appConfig: this._appConfig,
|
||||||
commandsManager: this._commandsManager,
|
commandsManager: this._commandsManager,
|
||||||
servicesManager: this._servicesManager,
|
servicesManager: this._servicesManager,
|
||||||
@ -348,6 +362,7 @@ export default class ExtensionManager {
|
|||||||
|
|
||||||
return extensionModule;
|
return extensionModule;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
|
console.log(ex);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Exception thrown while trying to call ${getModuleFnName} for the ${extensionId} extension`
|
`Exception thrown while trying to call ${getModuleFnName} for the ${extensionId} extension`
|
||||||
);
|
);
|
||||||
@ -356,10 +371,10 @@ export default class ExtensionManager {
|
|||||||
|
|
||||||
_initHangingProtocolsModule = (extensionModule, extensionId) => {
|
_initHangingProtocolsModule = (extensionModule, extensionId) => {
|
||||||
const { hangingProtocolService } = this._servicesManager.services;
|
const { hangingProtocolService } = this._servicesManager.services;
|
||||||
extensionModule.forEach(({ id, protocol }) => {
|
extensionModule.forEach(({ name, protocol }) => {
|
||||||
if (protocol) {
|
if (protocol) {
|
||||||
// Only auto-register if protocol specified, otherwise let mode register
|
// Only auto-register if protocol specified, otherwise let mode register
|
||||||
hangingProtocolService.addProtocol(id, protocol);
|
hangingProtocolService.addProtocol(name, protocol);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
COMMANDS: 'commandsModule',
|
COMMANDS: 'commandsModule',
|
||||||
CUSTOMIZATION: 'customizationModule',
|
CUSTOMIZATION: 'customizationModule',
|
||||||
|
STATE_SYNC: 'stateSyncModule',
|
||||||
DATA_SOURCE: 'dataSourcesModule',
|
DATA_SOURCE: 'dataSourcesModule',
|
||||||
PANEL: 'panelModule',
|
PANEL: 'panelModule',
|
||||||
SOP_CLASS_HANDLER: 'sopClassHandlerModule',
|
SOP_CLASS_HANDLER: 'sopClassHandlerModule',
|
||||||
|
|||||||
@ -25,6 +25,7 @@ describe('Top level exports', () => {
|
|||||||
//
|
//
|
||||||
'CineService',
|
'CineService',
|
||||||
'CustomizationService',
|
'CustomizationService',
|
||||||
|
'StateSyncService',
|
||||||
'UIDialogService',
|
'UIDialogService',
|
||||||
'UIModalService',
|
'UIModalService',
|
||||||
'UINotificationService',
|
'UINotificationService',
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import {
|
|||||||
PubSubService,
|
PubSubService,
|
||||||
UserAuthenticationService,
|
UserAuthenticationService,
|
||||||
CustomizationService,
|
CustomizationService,
|
||||||
|
StateSyncService,
|
||||||
PanelService,
|
PanelService,
|
||||||
} from './services';
|
} from './services';
|
||||||
|
|
||||||
@ -61,6 +62,7 @@ const OHIF = {
|
|||||||
//
|
//
|
||||||
CineService,
|
CineService,
|
||||||
CustomizationService,
|
CustomizationService,
|
||||||
|
StateSyncService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
UIModalService,
|
UIModalService,
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
@ -99,6 +101,7 @@ export {
|
|||||||
//
|
//
|
||||||
CineService,
|
CineService,
|
||||||
CustomizationService,
|
CustomizationService,
|
||||||
|
StateSyncService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
UIModalService,
|
UIModalService,
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
|
|||||||
@ -63,7 +63,7 @@ export default class CustomizationService extends PubSubService {
|
|||||||
|
|
||||||
modeCustomizations: Record<string, Customization> = {};
|
modeCustomizations: Record<string, Customization> = {};
|
||||||
globalCustomizations: Record<string, Customization> = {};
|
globalCustomizations: Record<string, Customization> = {};
|
||||||
configuration: UICustomizationConfiguration;
|
configuration: CustomizationConfiguration;
|
||||||
|
|
||||||
constructor({ configuration, commandsManager }) {
|
constructor({ configuration, commandsManager }) {
|
||||||
super(EVENTS);
|
super(EVENTS);
|
||||||
@ -97,36 +97,6 @@ export default class CustomizationService extends PubSubService {
|
|||||||
this.modeCustomizations = {};
|
this.modeCustomizations = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {*} interaction - can be undefined to run nothing
|
|
||||||
* @param {*} extraOptions to include in the commands run
|
|
||||||
*/
|
|
||||||
recordInteraction(
|
|
||||||
interaction: Customization | void,
|
|
||||||
extraOptions?: Record<string, unknown>
|
|
||||||
): void {
|
|
||||||
if (!interaction) return;
|
|
||||||
const commandsManager = this.commandsManager;
|
|
||||||
const { commands = [] } = interaction;
|
|
||||||
|
|
||||||
commands.forEach(({ commandName, commandOptions, context }) => {
|
|
||||||
if (commandName) {
|
|
||||||
commandsManager.runCommand(
|
|
||||||
commandName,
|
|
||||||
{
|
|
||||||
interaction,
|
|
||||||
...commandOptions,
|
|
||||||
...extraOptions,
|
|
||||||
},
|
|
||||||
context
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.warn('No command name supplied in', interaction);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public getModeCustomizations(): Record<string, Customization> {
|
public getModeCustomizations(): Record<string, Customization> {
|
||||||
return this.modeCustomizations;
|
return this.modeCustomizations;
|
||||||
}
|
}
|
||||||
@ -145,6 +115,23 @@ export default class CustomizationService extends PubSubService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** This is the preferred getter for all customizations,
|
||||||
|
* getting mode customizations first and otherwise global customizations.
|
||||||
|
*
|
||||||
|
* @param customizationId - the customization id to look for
|
||||||
|
* @param defaultValue - is the default value to return. Note this value
|
||||||
|
* may have been extended with any customizationType extensions provided,
|
||||||
|
* so you cannot just use `|| defaultValue`
|
||||||
|
* @return A customization to use if one is found, or the default customization,
|
||||||
|
* both enhanced with any customizationType inheritance (see transform)
|
||||||
|
*/
|
||||||
|
public getCustomization(
|
||||||
|
customizationId: string,
|
||||||
|
defaultValue?: Customization
|
||||||
|
): Customization | void {
|
||||||
|
return this.getModeCustomization(customizationId, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
/** Mode customizations are changes to the behaviour of the extensions
|
/** Mode customizations are changes to the behaviour of the extensions
|
||||||
* when running in a given mode. Reset clears mode customizations.
|
* when running in a given mode. Reset clears mode customizations.
|
||||||
* Note that global customizations over-ride mode customizations.
|
* Note that global customizations over-ride mode customizations.
|
||||||
@ -158,7 +145,7 @@ export default class CustomizationService extends PubSubService {
|
|||||||
this.globalCustomizations[customizationId] ??
|
this.globalCustomizations[customizationId] ??
|
||||||
this.modeCustomizations[customizationId] ??
|
this.modeCustomizations[customizationId] ??
|
||||||
defaultValue;
|
defaultValue;
|
||||||
return this.applyType(customization);
|
return this.transform(customization);
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasModeCustomization(customizationId: string) {
|
public hasModeCustomization(customizationId: string) {
|
||||||
@ -167,16 +154,33 @@ export default class CustomizationService extends PubSubService {
|
|||||||
this.modeCustomizations[customizationId]
|
this.modeCustomizations[customizationId]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* get is an alias for getModeCustomization, as it is the generic getter
|
||||||
|
* which will return both mode and global customizations, and should be
|
||||||
|
* used generally.
|
||||||
|
* Note that the second parameter, defaultValue, will be expanded to include
|
||||||
|
* any customizationType values defined in it, so it is not the same as doing:
|
||||||
|
* `customizationService.get('key') || defaultValue`
|
||||||
|
* unless the defaultValue does not contain any customizationType definitions.
|
||||||
|
*/
|
||||||
|
public get = this.getModeCustomization;
|
||||||
|
|
||||||
/** Applies any inheritance due to UI Type customization */
|
/**
|
||||||
public applyType(customization: Customization): Customization {
|
* Applies any inheritance due to UI Type customization.
|
||||||
|
* This will look for customizationType in the customization object
|
||||||
|
* and if that is found, will assign all iterable values from that
|
||||||
|
* type into the new type, allowing default behaviour to be configured.
|
||||||
|
*/
|
||||||
|
public transform(customization: Customization): Customization {
|
||||||
if (!customization) return customization;
|
if (!customization) return customization;
|
||||||
const { customizationType } = customization;
|
const { customizationType } = customization;
|
||||||
if (!customizationType) return customization;
|
if (!customizationType) return customization;
|
||||||
const parent = this.getModeCustomization(customizationType);
|
const parent = this.getCustomization(customizationType);
|
||||||
return parent
|
const result = parent
|
||||||
? Object.assign(Object.create(parent), customization)
|
? Object.assign(Object.create(parent), customization)
|
||||||
: customization;
|
: customization;
|
||||||
|
// Execute an nested type information
|
||||||
|
return result.transform?.(this) || result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public addModeCustomizations(modeCustomizations): void {
|
public addModeCustomizations(modeCustomizations): void {
|
||||||
@ -203,7 +207,7 @@ export default class CustomizationService extends PubSubService {
|
|||||||
id: string,
|
id: string,
|
||||||
defaultValue?: Customization
|
defaultValue?: Customization
|
||||||
): Customization | void {
|
): Customization | void {
|
||||||
return this.applyType(this.globalCustomizations[id] ?? defaultValue);
|
return this.transform(this.globalCustomizations[id] ?? defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
setGlobalCustomization(id: string, value: Customization): void {
|
setGlobalCustomization(id: string, value: Customization): void {
|
||||||
@ -243,7 +247,7 @@ export default class CustomizationService extends PubSubService {
|
|||||||
const extensionValue = this.findExtensionValue(value);
|
const extensionValue = this.findExtensionValue(value);
|
||||||
// The child of a reference is only a set of references when an array,
|
// The child of a reference is only a set of references when an array,
|
||||||
// so call the addReference direct. It could be a secondary reference perhaps
|
// so call the addReference direct. It could be a secondary reference perhaps
|
||||||
this.addReference(extensionValue);
|
this.addReference(extensionValue.value, isGlobal, extensionValue.name);
|
||||||
} else if (Array.isArray(value)) {
|
} else if (Array.isArray(value)) {
|
||||||
this.addReferences(value, isGlobal);
|
this.addReferences(value, isGlobal);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -84,9 +84,9 @@ function _getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _getInstanceByImageId(imageId) {
|
function _getInstanceByImageId(imageId) {
|
||||||
for (let study of _model.studies) {
|
for (const study of _model.studies) {
|
||||||
for (let series of study.series) {
|
for (const series of study.series) {
|
||||||
for (let instance of series.instances) {
|
for (const instance of series.instances) {
|
||||||
if (instance.imageId === imageId) {
|
if (instance.imageId === imageId) {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
@ -236,7 +236,7 @@ const BaseImplementation = {
|
|||||||
addStudy(study) {
|
addStudy(study) {
|
||||||
const { StudyInstanceUID } = study;
|
const { StudyInstanceUID } = study;
|
||||||
|
|
||||||
let existingStudy = _model.studies.find(
|
const existingStudy = _model.studies.find(
|
||||||
study => study.StudyInstanceUID === StudyInstanceUID
|
study => study.StudyInstanceUID === StudyInstanceUID
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -29,27 +29,31 @@ const match = (
|
|||||||
let requiredFailed = false;
|
let requiredFailed = false;
|
||||||
let score = 0;
|
let score = 0;
|
||||||
|
|
||||||
|
// Allow for matching against current or prior specifically
|
||||||
|
const prior = options?.studies?.[1];
|
||||||
|
const current = options?.studies?.[0];
|
||||||
|
const instance = (metadataInstance.images || metadataInstance.others)?.[0];
|
||||||
|
const fromSrc = {
|
||||||
|
prior,
|
||||||
|
current,
|
||||||
|
instance,
|
||||||
|
...options,
|
||||||
|
options,
|
||||||
|
metadataInstance,
|
||||||
|
};
|
||||||
|
|
||||||
rules.forEach(rule => {
|
rules.forEach(rule => {
|
||||||
const { attribute } = rule;
|
const { attribute, from = 'metadataInstance' } = rule;
|
||||||
// Do not use the custom attribute from the metadataInstance since it is subject to change
|
// Do not use the custom attribute from the metadataInstance since it is subject to change
|
||||||
if (customAttributeRetrievalCallbacks.hasOwnProperty(attribute)) {
|
if (customAttributeRetrievalCallbacks.hasOwnProperty(attribute)) {
|
||||||
readValues[attribute] = customAttributeRetrievalCallbacks[
|
readValues[attribute] = customAttributeRetrievalCallbacks[
|
||||||
attribute
|
attribute
|
||||||
].callback(metadataInstance, options);
|
].callback.call(rule, metadataInstance, options);
|
||||||
} else {
|
} else {
|
||||||
readValues[attribute] =
|
readValues[attribute] =
|
||||||
metadataInstance[attribute] ??
|
fromSrc[from]?.[attribute] ?? instance?.[attribute];
|
||||||
((metadataInstance.images || metadataInstance.others || [])[0] || {})[
|
|
||||||
attribute
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
|
||||||
'Test',
|
|
||||||
attribute,
|
|
||||||
readValues[attribute],
|
|
||||||
JSON.stringify(rule.constraint)
|
|
||||||
);
|
|
||||||
// Format the constraint as required by Validate.js
|
// Format the constraint as required by Validate.js
|
||||||
const testConstraint = {
|
const testConstraint = {
|
||||||
[attribute]: rule.constraint,
|
[attribute]: rule.constraint,
|
||||||
@ -70,6 +74,14 @@ const match = (
|
|||||||
errorMessages = ['Something went wrong during validation.', e];
|
errorMessages = ['Something went wrong during validation.', e];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'Test',
|
||||||
|
`${from}.${attribute}`,
|
||||||
|
readValues[attribute],
|
||||||
|
JSON.stringify(rule.constraint),
|
||||||
|
!errorMessages
|
||||||
|
);
|
||||||
|
|
||||||
if (!errorMessages) {
|
if (!errorMessages) {
|
||||||
// If no errorMessages were returned, then validation passed.
|
// If no errorMessages were returned, then validation passed.
|
||||||
|
|
||||||
|
|||||||
@ -117,8 +117,7 @@ const studyMatchDisplaySets = [displaySet3, displaySet2, displaySet1];
|
|||||||
|
|
||||||
function checkHpsBestMatch(hps) {
|
function checkHpsBestMatch(hps) {
|
||||||
hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets });
|
hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets });
|
||||||
const { hpAlreadyApplied, viewportMatchDetails } = hps.getMatchDetails();
|
const { viewportMatchDetails } = hps.getMatchDetails();
|
||||||
expect(hpAlreadyApplied).toMatchObject(new Map([[0, false]]));
|
|
||||||
expect(viewportMatchDetails.size).toBe(1);
|
expect(viewportMatchDetails.size).toBe(1);
|
||||||
expect(viewportMatchDetails.get(0)).toMatchObject({
|
expect(viewportMatchDetails.get(0)).toMatchObject({
|
||||||
viewportOptions: {
|
viewportOptions: {
|
||||||
@ -131,9 +130,11 @@ function checkHpsBestMatch(hps) {
|
|||||||
// ds2 fails to match required and ds3 fails to match an optional.
|
// ds2 fails to match required and ds3 fails to match an optional.
|
||||||
displaySetsInfo: [
|
displaySetsInfo: [
|
||||||
{
|
{
|
||||||
SeriesInstanceUID: 'ds1',
|
|
||||||
displaySetInstanceUID: 'displaySet1',
|
displaySetInstanceUID: 'displaySet1',
|
||||||
displaySetOptions: {},
|
displaySetOptions: {
|
||||||
|
id: 'displaySetSelector',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -191,14 +192,6 @@ describe('HangingProtocolService', () => {
|
|||||||
it('matches best image match', () => {
|
it('matches best image match', () => {
|
||||||
checkHpsBestMatch(hangingProtocolService);
|
checkHpsBestMatch(hangingProtocolService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses services manager', () => {
|
|
||||||
hangingProtocolService.run({
|
|
||||||
studies: [studyMatch],
|
|
||||||
displaySets: studyMatchDisplaySets,
|
|
||||||
});
|
|
||||||
expect(mockedFunction).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -81,6 +81,16 @@ export default class ProtocolEngine {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* finds the match results against the given display set or
|
||||||
|
* study instance by testing the given rules against this, and using
|
||||||
|
* the provided options for testing.
|
||||||
|
*
|
||||||
|
* @param {*} metaData to match against as primary value
|
||||||
|
* @param {*} rules to apply
|
||||||
|
* @param {*} options are additional values that can be used for matching
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
findMatch(metaData, rules, options) {
|
findMatch(metaData, rules, options) {
|
||||||
return HPMatcher.match(
|
return HPMatcher.match(
|
||||||
metaData,
|
metaData,
|
||||||
@ -109,7 +119,7 @@ export default class ProtocolEngine {
|
|||||||
let rules = protocol.protocolMatchingRules.slice();
|
let rules = protocol.protocolMatchingRules.slice();
|
||||||
if (!rules || !rules.length) {
|
if (!rules || !rules.length) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'ProtocolEngine::findMatchByStudy no matching rules - specify protocolMatchingRules',
|
'ProtocolEngine::findMatchByStudy no matching rules - specify protocolMatchingRules for',
|
||||||
protocol.id
|
protocol.id
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -14,6 +14,41 @@ validate.validators.doesNotEqual = function(value, options, key) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ignore case contains.
|
||||||
|
// options testValue MUST be in lower case already, otherwise it won't match
|
||||||
|
validate.validators.containsI = function (value, options, key) {
|
||||||
|
const testValue = options?.value ?? options;
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (
|
||||||
|
value.some(
|
||||||
|
item => !validate.validators.containsI(item.toLowerCase(), options, key)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return `No item of ${value.join(',')} contains ${JSON.stringify(
|
||||||
|
testValue
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
if (Array.isArray(testValue)) {
|
||||||
|
if (
|
||||||
|
testValue.some(
|
||||||
|
subTest => !validate.validators.containsI(value, subTest, key)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return `${key} must contain at least one of ${testValue.join(',')}`;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
testValue &&
|
||||||
|
value.indexOf &&
|
||||||
|
value.toLowerCase().indexOf(testValue) === -1
|
||||||
|
) {
|
||||||
|
return key + 'must contain any case of' + testValue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
validate.validators.contains = function(value, options, key) {
|
validate.validators.contains = function(value, options, key) {
|
||||||
const testValue = options?.value ?? options;
|
const testValue = options?.value ?? options;
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
|
|||||||
@ -3,9 +3,10 @@ import validate from './validator.js';
|
|||||||
describe('validator', () => {
|
describe('validator', () => {
|
||||||
const attributeMap = {
|
const attributeMap = {
|
||||||
str: 'string',
|
str: 'string',
|
||||||
|
upper: 'UPPER',
|
||||||
num: 3,
|
num: 3,
|
||||||
nullValue: null,
|
nullValue: null,
|
||||||
list: ['abc', 'def'],
|
list: ['abc', 'def', 'GHI'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
@ -35,6 +36,37 @@ describe('validator', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('containsI', () => {
|
||||||
|
it('returns match any list contains case insensitive', () => {
|
||||||
|
expect(
|
||||||
|
validate(attributeMap, { upper: { containsI: ['bye', 'pre'] } }, [
|
||||||
|
options,
|
||||||
|
])
|
||||||
|
).not.toBeUndefined();
|
||||||
|
expect(
|
||||||
|
validate(attributeMap, { list: { containsI: 'hi' } }, [options])
|
||||||
|
).toBeUndefined();
|
||||||
|
expect(
|
||||||
|
validate(attributeMap, { list: { containsI: ['hi', 'bye'] } }, [
|
||||||
|
options,
|
||||||
|
])
|
||||||
|
).toBeUndefined();
|
||||||
|
expect(
|
||||||
|
validate(attributeMap, { list: { containsI: ['bye', 'hi'] } }, [
|
||||||
|
options,
|
||||||
|
])
|
||||||
|
).toBeUndefined();
|
||||||
|
expect(
|
||||||
|
validate(attributeMap, { list: { containsI: ['ig', 'hi'] } }, [options])
|
||||||
|
).toBeUndefined();
|
||||||
|
expect(
|
||||||
|
validate(attributeMap, { upper: { containsI: ['bye', 'per'] } }, [
|
||||||
|
options,
|
||||||
|
])
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('equals', () => {
|
describe('equals', () => {
|
||||||
it('returned undefined on equals', () => {
|
it('returned undefined on equals', () => {
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@ -459,7 +459,12 @@ class MeasurementService extends PubSubService {
|
|||||||
log.warn(`Measurement ID not found. Generating UID: ${internalUID}`);
|
log.warn(`Measurement ID not found. Generating UID: ${internalUID}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const annotationData = data.annotation.data;
|
||||||
|
|
||||||
const newMeasurement = {
|
const newMeasurement = {
|
||||||
|
finding: annotationData.finding,
|
||||||
|
findingSites: annotationData.findingSites,
|
||||||
|
site: annotationData.findingSites?.[0],
|
||||||
...measurement,
|
...measurement,
|
||||||
modifiedTimestamp: Math.floor(Date.now() / 1000),
|
modifiedTimestamp: Math.floor(Date.now() / 1000),
|
||||||
uid: internalUID,
|
uid: internalUID,
|
||||||
@ -472,7 +477,7 @@ class MeasurementService extends PubSubService {
|
|||||||
measurement: newMeasurement,
|
measurement: newMeasurement,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log.info(`Measurement added.`, newMeasurement);
|
log.info('Measurement added', newMeasurement);
|
||||||
this.measurements[internalUID] = newMeasurement;
|
this.measurements[internalUID] = newMeasurement;
|
||||||
this._broadcastEvent(this.EVENTS.RAW_MEASUREMENT_ADDED, {
|
this._broadcastEvent(this.EVENTS.RAW_MEASUREMENT_ADDED, {
|
||||||
source,
|
source,
|
||||||
@ -519,9 +524,14 @@ class MeasurementService extends PubSubService {
|
|||||||
let measurement = {};
|
let measurement = {};
|
||||||
try {
|
try {
|
||||||
const sourceMappings = this.mappings[source.uid];
|
const sourceMappings = this.mappings[source.uid];
|
||||||
const { toMeasurementSchema } = sourceMappings.find(
|
const sourceMapping = sourceMappings.find(
|
||||||
mapping => mapping.annotationType === annotationType
|
mapping => mapping.annotationType === annotationType
|
||||||
);
|
);
|
||||||
|
if (!sourceMapping) {
|
||||||
|
console.log('No source mapping', source);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { toMeasurementSchema } = sourceMapping;
|
||||||
|
|
||||||
/* Convert measurement */
|
/* Convert measurement */
|
||||||
measurement = toMeasurementSchema(sourceAnnotationDetail);
|
measurement = toMeasurementSchema(sourceAnnotationDetail);
|
||||||
@ -548,26 +558,27 @@ class MeasurementService extends PubSubService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const oldMeasurement = this.measurements[internalUID];
|
||||||
|
|
||||||
const newMeasurement = {
|
const newMeasurement = {
|
||||||
|
...oldMeasurement,
|
||||||
...measurement,
|
...measurement,
|
||||||
modifiedTimestamp: Math.floor(Date.now() / 1000),
|
modifiedTimestamp: Math.floor(Date.now() / 1000),
|
||||||
uid: internalUID,
|
uid: internalUID,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.measurements[internalUID]) {
|
if (oldMeasurement) {
|
||||||
// TODO: Ultimately, each annotation should have a selected flag right from the soure.
|
// TODO: Ultimately, each annotation should have a selected flag right from the soure.
|
||||||
// For now, it is just added in OHIF here and in setMeasurementSelected.
|
// For now, it is just added in OHIF here and in setMeasurementSelected.
|
||||||
newMeasurement.selected = this.measurements[internalUID].selected;
|
|
||||||
this.measurements[internalUID] = newMeasurement;
|
this.measurements[internalUID] = newMeasurement;
|
||||||
if (isUpdate) {
|
if (isUpdate) {
|
||||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
|
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||||
source,
|
source,
|
||||||
measurement: newMeasurement,
|
measurement: newMeasurement,
|
||||||
notYetUpdatedAtSource: false,
|
notYetUpdatedAtSource: false,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log.info('Measurement added.', newMeasurement);
|
log.info('Measurement added.', newMeasurement);
|
||||||
this.measurements[internalUID] = newMeasurement;
|
|
||||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_ADDED, {
|
this._broadcastEvent(this.EVENTS.MEASUREMENT_ADDED, {
|
||||||
source,
|
source,
|
||||||
measurement: newMeasurement,
|
measurement: newMeasurement,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user