commit
5b7dc9e9c3
@ -2,7 +2,8 @@
|
||||
<!-- markdownlint-disable -->
|
||||
<div align="center">
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
@ -31,8 +31,6 @@ async function _hydrateSEGDisplaySet({
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
viewportGridService.setDisplaySetsForViewports(updatedViewports);
|
||||
|
||||
// Todo: fix this after we have a better way for stack viewport segmentations
|
||||
|
||||
// check every viewport in the viewports to see if the displaySetInstanceUID
|
||||
@ -50,7 +48,7 @@ async function _hydrateSEGDisplaySet({
|
||||
);
|
||||
|
||||
if (shouldDisplaySeg) {
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
updatedViewports.push({
|
||||
viewportIndex: index,
|
||||
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
|
||||
viewportOptions: {
|
||||
@ -62,6 +60,9 @@ async function _hydrateSEGDisplaySet({
|
||||
}
|
||||
});
|
||||
|
||||
// Do the entire update at once
|
||||
viewportGridService.setDisplaySetsForViewports(updatedViewports);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -45,8 +45,8 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"classnames": "^2.3.2",
|
||||
"@cornerstonejs/adapters": "^0.4.1",
|
||||
"@cornerstonejs/core": "^0.33.2",
|
||||
"@cornerstonejs/tools": "^0.50.2"
|
||||
"@cornerstonejs/adapters": "^0.6.0",
|
||||
"@cornerstonejs/core": "^0.40.0",
|
||||
"@cornerstonejs/tools": "^0.60.1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ function getFilteredCornerstoneToolState(
|
||||
);
|
||||
const toolData = imageIdSpecificToolState[toolType].data;
|
||||
|
||||
let finding;
|
||||
let { finding } = measurementDataI;
|
||||
const findingSites = [];
|
||||
|
||||
// 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, {
|
||||
finding,
|
||||
findingSites,
|
||||
@ -73,7 +77,7 @@ function getFilteredCornerstoneToolState(
|
||||
for (let i = 0; i < framesOfReference.length; i++) {
|
||||
const frameOfReference = framesOfReference[i];
|
||||
|
||||
const frameOfReferenceAnnotations = annotationManager.getFrameOfReferenceAnnotations(
|
||||
const frameOfReferenceAnnotations = annotationManager.getAnnotations(
|
||||
frameOfReference
|
||||
);
|
||||
|
||||
|
||||
@ -11,6 +11,25 @@ const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
||||
|
||||
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.
|
||||
*
|
||||
@ -20,8 +39,16 @@ export default function hydrateStructuredReport(
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
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(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
@ -171,6 +198,15 @@ export default function hydrateStructuredReport(
|
||||
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
|
||||
);
|
||||
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(
|
||||
m => m.annotationType === annotationType
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
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 {
|
||||
@ -27,6 +27,7 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
dataSource,
|
||||
displaySets,
|
||||
viewportIndex,
|
||||
viewportOptions,
|
||||
viewportLabel,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
@ -215,6 +216,7 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
// override the activeImageDisplaySetData
|
||||
displaySets={[activeImageDisplaySetData]}
|
||||
viewportOptions={{
|
||||
...viewportOptions,
|
||||
toolGroupId: `${SR_TOOLGROUP_BASE_NAME}`,
|
||||
}}
|
||||
onElementEnabled={onElementEnabled}
|
||||
@ -419,6 +421,9 @@ OHIFCornerstoneSRViewport.propTypes = {
|
||||
dataSource: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
customProps: PropTypes.object,
|
||||
viewportOptions: PropTypes.object,
|
||||
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
|
||||
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
|
||||
};
|
||||
|
||||
OHIFCornerstoneSRViewport.defaultProps = {
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^3.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",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
@ -43,10 +43,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^0.4.1",
|
||||
"@cornerstonejs/core": "^0.33.2",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^0.14.1",
|
||||
"@cornerstonejs/tools": "^0.50.2",
|
||||
"@cornerstonejs/adapters": "^0.6.0",
|
||||
"@cornerstonejs/core": "^0.40.0",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^0.16.0",
|
||||
"@cornerstonejs/tools": "^0.60.1",
|
||||
"@kitware/vtk.js": "26.5.6",
|
||||
"html2canvas": "^1.4.1",
|
||||
"lodash.debounce": "4.0.8",
|
||||
|
||||
@ -128,6 +128,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
cornerstoneViewportService,
|
||||
cornerstoneCacheService,
|
||||
viewportGridService,
|
||||
stateSyncService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const cineHandler = () => {
|
||||
@ -211,6 +212,33 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
}
|
||||
}, [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 viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
|
||||
viewportIndex
|
||||
@ -288,6 +316,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
setImageScrollBarHeight();
|
||||
|
||||
return () => {
|
||||
storePresentation();
|
||||
|
||||
cleanUpServices();
|
||||
|
||||
cornerstoneViewportService.disableElement(viewportIndex);
|
||||
@ -360,11 +390,26 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
initialImageIndex
|
||||
);
|
||||
|
||||
storePresentation();
|
||||
|
||||
const {
|
||||
lutPresentationStore,
|
||||
positionPresentationStore,
|
||||
} = stateSyncService.getState();
|
||||
const { presentationIds } = viewportOptions;
|
||||
const presentations = {
|
||||
positionPresentation:
|
||||
positionPresentationStore[presentationIds?.positionPresentationId],
|
||||
lutPresentation:
|
||||
lutPresentationStore[presentationIds?.lutPresentationId],
|
||||
};
|
||||
|
||||
cornerstoneViewportService.setViewportData(
|
||||
viewportIndex,
|
||||
viewportData,
|
||||
viewportOptions,
|
||||
displaySetOptions
|
||||
displaySetOptions,
|
||||
presentations
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -117,8 +117,11 @@ function CustomizableViewportOverlay({
|
||||
viewportIndex,
|
||||
servicesManager,
|
||||
}) {
|
||||
const { toolbarService, cornerstoneViewportService, customizationService } =
|
||||
servicesManager.services;
|
||||
const {
|
||||
toolbarService,
|
||||
cornerstoneViewportService,
|
||||
customizationService,
|
||||
} = servicesManager.services;
|
||||
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
|
||||
const [scale, setScale] = useState(1);
|
||||
const [activeTools, setActiveTools] = useState([]);
|
||||
@ -202,10 +205,9 @@ function CustomizableViewportOverlay({
|
||||
previousCamera.parallelScale !== camera.parallelScale ||
|
||||
previousCamera.scale !== camera.scale
|
||||
) {
|
||||
const viewport =
|
||||
cornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
if (!viewport) {
|
||||
return;
|
||||
@ -283,7 +285,7 @@ function CustomizableViewportOverlay({
|
||||
} else if (item.customizationType === 'ohif.overlayItem.instanceNumber') {
|
||||
return <InstanceNumberOverlayItem {...overlayItemProps} />;
|
||||
} else {
|
||||
const renderItem = customizationService.applyType(item);
|
||||
const renderItem = customizationService.transform(item);
|
||||
|
||||
if (typeof renderItem.content === 'function') {
|
||||
return renderItem.content(overlayItemProps);
|
||||
@ -450,8 +452,9 @@ function _getInstanceNumberFromVolume(
|
||||
const volume = volumes[0];
|
||||
const { direction, imageIds } = volume;
|
||||
|
||||
const cornerstoneViewport =
|
||||
cornerstoneViewportService.getCornerstoneViewportByIndex(viewportIndex);
|
||||
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
if (!cornerstoneViewport) {
|
||||
return;
|
||||
|
||||
@ -118,6 +118,10 @@ function ViewportOrientationMarkers({
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
if (!ohifViewport) {
|
||||
console.log('ViewportOrientationMarkers::No viewport');
|
||||
return null;
|
||||
}
|
||||
const backgroundColor = ohifViewport.getViewportOptions().background;
|
||||
|
||||
// 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 callInputDialog from './utils/callInputDialog';
|
||||
import { setColormap } from './utils/colormap/transferFunctionHelpers';
|
||||
import toggleMPRHangingProtocol from './utils/mpr/toggleMPRHangingProtocol';
|
||||
import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
|
||||
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
|
||||
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
||||
|
||||
const commandsModule = ({ servicesManager }) => {
|
||||
function commandsModule({ servicesManager, commandsManager }) {
|
||||
const {
|
||||
viewportGridService,
|
||||
toolGroupService,
|
||||
@ -27,10 +27,14 @@ const commandsModule = ({ servicesManager }) => {
|
||||
toolbarService,
|
||||
uiDialogService,
|
||||
cornerstoneViewportService,
|
||||
hangingProtocolService,
|
||||
uiNotificationService,
|
||||
customizationService,
|
||||
measurementService,
|
||||
hangingProtocolService,
|
||||
} = (servicesManager as ServicesManager).services;
|
||||
|
||||
const { measurementServiceSource } = this;
|
||||
|
||||
function _getActiveViewportEnabledElement() {
|
||||
return getActiveViewportEnabledElement(viewportGridService);
|
||||
}
|
||||
@ -70,9 +74,165 @@ const commandsModule = ({ servicesManager }) => {
|
||||
}
|
||||
|
||||
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 }) => {
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(
|
||||
viewportId
|
||||
@ -128,6 +288,14 @@ const commandsModule = ({ servicesManager }) => {
|
||||
});
|
||||
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 }) => {
|
||||
if (toolName === 'Crosshairs') {
|
||||
const activeViewportToolGroup = _getToolGroup(null);
|
||||
@ -150,7 +318,7 @@ const commandsModule = ({ servicesManager }) => {
|
||||
};
|
||||
|
||||
const toolGroup = _getToolGroup(toolGroupId);
|
||||
const toolGroupViewportIds = toolGroup.getViewportIds();
|
||||
const toolGroupViewportIds = toolGroup?.getViewportIds?.();
|
||||
|
||||
// if toolGroup has been destroyed, or its viewports have been removed
|
||||
if (!toolGroupViewportIds || !toolGroupViewportIds.length) {
|
||||
@ -171,6 +339,17 @@ const commandsModule = ({ servicesManager }) => {
|
||||
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();
|
||||
|
||||
if (activeToolName) {
|
||||
@ -404,16 +583,6 @@ const commandsModule = ({ servicesManager }) => {
|
||||
(activeViewportIndex - 1 + viewports.length) % viewports.length;
|
||||
viewportGridService.setActiveViewportIndex(nextViewportIndex);
|
||||
},
|
||||
setHangingProtocol: ({ protocolId }) => {
|
||||
hangingProtocolService.setProtocol(protocolId);
|
||||
},
|
||||
toggleMPR: ({ toggledState }) => {
|
||||
toggleMPRHangingProtocol({
|
||||
toggledState,
|
||||
servicesManager,
|
||||
getToolGroup: _getToolGroup,
|
||||
});
|
||||
},
|
||||
toggleStackImageSync: ({ toggledState }) => {
|
||||
toggleStackImageSync({
|
||||
getEnabledElement,
|
||||
@ -446,11 +615,53 @@ const commandsModule = ({ servicesManager }) => {
|
||||
};
|
||||
|
||||
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: {
|
||||
commandFn: actions.setWindowLevel,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
toolbarServiceRecordInteraction: {
|
||||
commandFn: actions.toolbarServiceRecordInteraction,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setToolActive: {
|
||||
commandFn: actions.setToolActive,
|
||||
storeContexts: [],
|
||||
@ -554,16 +765,6 @@ const commandsModule = ({ servicesManager }) => {
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setHangingProtocol: {
|
||||
commandFn: actions.setHangingProtocol,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
toggleMPR: {
|
||||
commandFn: actions.toggleMPR,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
toggleStackImageSync: {
|
||||
commandFn: actions.toggleStackImageSync,
|
||||
storeContexts: [],
|
||||
@ -581,6 +782,6 @@ const commandsModule = ({ servicesManager }) => {
|
||||
definitions,
|
||||
defaultContext: 'CORNERSTONE',
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default commandsModule;
|
||||
|
||||
@ -1,16 +1,46 @@
|
||||
const mpr = {
|
||||
id: 'mpr',
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
const mpr: Types.HangingProtocol.Protocol = {
|
||||
locked: true,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'mpr',
|
||||
createdDate: '2021-02-23T19:22:08.894Z',
|
||||
modifiedDate: '2022-10-04T19:22:08.894Z',
|
||||
modifiedDate: '2023-02-17',
|
||||
availableTo: {},
|
||||
editableBy: {},
|
||||
// Unknown number of priors referenced - so just match any study
|
||||
numberOfPriorsReferenced: 0,
|
||||
protocolMatchingRules: [],
|
||||
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: {
|
||||
mprDisplaySet: {
|
||||
activeDisplaySet: {
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
weight: 1,
|
||||
@ -27,8 +57,7 @@ const mpr = {
|
||||
},
|
||||
stages: [
|
||||
{
|
||||
id: 'mpr3Stage',
|
||||
name: 'mpr',
|
||||
name: 'MPR 1x3',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
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: [
|
||||
{
|
||||
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() {
|
||||
return [
|
||||
{
|
||||
id: 'mpr',
|
||||
name: '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 init from './init';
|
||||
import commandsModule from './commandsModule';
|
||||
import getCommandsModule from './commandsModule';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import ToolGroupService from './services/ToolGroupService';
|
||||
import SyncGroupService from './services/SyncGroupService';
|
||||
@ -26,6 +26,7 @@ import { registerColormap } from './utils/colormap/transferFunctionHelpers';
|
||||
import { id } from './id';
|
||||
import * as csWADOImageLoader from './initWADOImageLoader.js';
|
||||
import { measurementMappingUtils } from './utils/measurementServiceMappings';
|
||||
import { PublicViewportOptions } from './services/ViewportService/Viewport';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(
|
||||
@ -50,7 +51,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
*/
|
||||
id,
|
||||
|
||||
onModeExit: () => {
|
||||
onModeExit: (): void => {
|
||||
// Empty out the image load and retrieval pools to prevent memory leaks
|
||||
// on the mode exits
|
||||
Object.values(cs3DEnums.RequestType).forEach(type => {
|
||||
@ -67,12 +68,10 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
*
|
||||
* @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools`
|
||||
*/
|
||||
async preRegistration({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
configuration = {},
|
||||
appConfig,
|
||||
}) {
|
||||
preRegistration: function (
|
||||
props: Types.Extensions.ExtensionParams
|
||||
): Promise<void> {
|
||||
const { servicesManager } = props;
|
||||
// Todo: we should be consistent with how services get registered. Use REGISTRATION static method for all
|
||||
servicesManager.registerService(
|
||||
CornerstoneViewportService(servicesManager)
|
||||
@ -86,20 +85,21 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
CornerstoneCacheService.REGISTRATION(servicesManager)
|
||||
);
|
||||
|
||||
await init({ servicesManager, commandsManager, configuration, appConfig });
|
||||
return init.call(this, props);
|
||||
},
|
||||
|
||||
getHangingProtocolModule,
|
||||
getViewportModule({ servicesManager, commandsManager }) {
|
||||
const ExtendedOHIFCornerstoneViewport = props => {
|
||||
// const onNewImageHandler = jumpData => {
|
||||
// commandsManager.runCommand('jumpToImage', jumpData);
|
||||
// };
|
||||
const { ToolbarService } = servicesManager.services;
|
||||
const { toolbarService } = (servicesManager as ServicesManager).services;
|
||||
|
||||
return (
|
||||
<OHIFCornerstoneViewport
|
||||
{...props}
|
||||
ToolbarService={ToolbarService}
|
||||
ToolbarService={toolbarService}
|
||||
servicesManager={servicesManager}
|
||||
commandsManager={commandsManager}
|
||||
/>
|
||||
@ -113,13 +113,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
},
|
||||
];
|
||||
},
|
||||
getCommandsModule({ servicesManager, commandsManager, extensionManager }) {
|
||||
return commandsModule({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
});
|
||||
},
|
||||
getCommandsModule,
|
||||
getUtilityModule({ servicesManager }) {
|
||||
return [
|
||||
{
|
||||
@ -150,5 +144,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
},
|
||||
};
|
||||
|
||||
export default cornerstoneExtension;
|
||||
export type { PublicViewportOptions };
|
||||
export { measurementMappingUtils };
|
||||
export default cornerstoneExtension;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import React from 'react';
|
||||
import { ContextMenuMeasurements } from '@ohif/ui';
|
||||
|
||||
import * as cornerstone from '@cornerstonejs/core';
|
||||
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||
@ -21,15 +20,12 @@ import initWADOImageLoader from './initWADOImageLoader';
|
||||
import initCornerstoneTools from './initCornerstoneTools';
|
||||
|
||||
import { connectToolsToMeasurementService } from './initMeasurementService';
|
||||
import callInputDialog from './utils/callInputDialog';
|
||||
import initCineService from './initCineService';
|
||||
import interleaveCenterLoader from './utils/interleaveCenterLoader';
|
||||
import nthLoader from './utils/nthLoader';
|
||||
import interleaveTopToBottom from './utils/interleaveTopToBottom';
|
||||
|
||||
const cs3DToolsEvents = Enums.Events;
|
||||
|
||||
let CONTEXT_MENU_OPEN = false;
|
||||
import initContextMenu from './initContextMenu';
|
||||
import initDoubleClick from './initDoubleClick';
|
||||
|
||||
// TODO: Cypress tests are currently grabbing this from the window?
|
||||
window.cornerstone = cornerstone;
|
||||
@ -42,11 +38,19 @@ export default async function init({
|
||||
commandsManager,
|
||||
configuration,
|
||||
appConfig,
|
||||
}) {
|
||||
}: Types.Extensions.ExtensionParams): Promise<void> {
|
||||
await cs3DInit();
|
||||
|
||||
// For debugging e2e tests that are failing on CI
|
||||
cornerstone.setUseCPURendering(Boolean(appConfig.useCPURendering));
|
||||
cornerstone.setConfiguration({
|
||||
...cornerstone.getConfiguration(),
|
||||
rendering: {
|
||||
...cornerstone.getConfiguration().rendering,
|
||||
strictZSpacingForVolumeViewport:
|
||||
appConfig.strictZSpacingForVolumeViewport,
|
||||
},
|
||||
});
|
||||
|
||||
// For debugging large datasets
|
||||
const MAX_CACHE_SIZE_1GB = 1073741824;
|
||||
@ -65,6 +69,7 @@ export default async function init({
|
||||
const {
|
||||
userAuthenticationService,
|
||||
measurementService,
|
||||
customizationService,
|
||||
displaySetService,
|
||||
uiDialogService,
|
||||
uiModalService,
|
||||
@ -74,6 +79,7 @@ export default async function init({
|
||||
hangingProtocolService,
|
||||
toolGroupService,
|
||||
viewportGridService,
|
||||
stateSyncService,
|
||||
} = servicesManager.services;
|
||||
|
||||
window.services = servicesManager.services;
|
||||
@ -97,6 +103,22 @@ export default async function init({
|
||||
_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 =
|
||||
cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;
|
||||
|
||||
@ -144,118 +166,12 @@ export default async function init({
|
||||
initWADOImageLoader(userAuthenticationService, appConfig);
|
||||
|
||||
/* Measurement Service */
|
||||
const measurementServiceSource = connectToolsToMeasurementService(
|
||||
this.measurementServiceSource = connectToolsToMeasurementService(
|
||||
servicesManager
|
||||
);
|
||||
|
||||
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
|
||||
hangingProtocolService.subscribe(
|
||||
hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,
|
||||
@ -266,32 +182,41 @@ export default async function init({
|
||||
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(
|
||||
viewport,
|
||||
volumeInputArray
|
||||
volumeInputArray,
|
||||
presentations
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
* Because click gives us the native "mouse up", buttons will always be `0`
|
||||
* Need to fallback to event.which;
|
||||
*
|
||||
*/
|
||||
const contextMenuHandleClick = evt => {
|
||||
const mouseUpEvent = evt.detail.event;
|
||||
const isRightClick = mouseUpEvent.which === 3;
|
||||
initContextMenu({
|
||||
cornerstoneViewportService,
|
||||
customizationService,
|
||||
commandsManager,
|
||||
});
|
||||
|
||||
const clickMethodHandler = isRightClick ? onRightClick : resetContextMenu;
|
||||
clickMethodHandler(evt);
|
||||
};
|
||||
|
||||
// const cancelContextMenuIfOpen = evt => {
|
||||
// if (CONTEXT_MENU_OPEN) {
|
||||
// resetContextMenu();
|
||||
// }
|
||||
// };
|
||||
initDoubleClick({
|
||||
customizationService,
|
||||
commandsManager,
|
||||
});
|
||||
|
||||
const newStackCallback = evt => {
|
||||
const { element } = evt.detail;
|
||||
@ -326,12 +251,6 @@ export default async function init({
|
||||
|
||||
function elementEnabledHandler(evt) {
|
||||
const { element } = evt.detail;
|
||||
|
||||
element.addEventListener(
|
||||
cs3DToolsEvents.MOUSE_CLICK,
|
||||
contextMenuHandleClick
|
||||
);
|
||||
|
||||
element.addEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
|
||||
|
||||
eventTarget.addEventListener(
|
||||
@ -343,11 +262,6 @@ export default async function init({
|
||||
function elementDisabledHandler(evt) {
|
||||
const { element } = evt.detail;
|
||||
|
||||
element.removeEventListener(
|
||||
cs3DToolsEvents.MOUSE_CLICK,
|
||||
contextMenuHandleClick
|
||||
);
|
||||
|
||||
element.removeEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
|
||||
|
||||
// TODO - consider removing the callback when all elements are gone
|
||||
@ -369,8 +283,8 @@ export default async function init({
|
||||
|
||||
viewportGridService.subscribe(
|
||||
viewportGridService.EVENTS.ACTIVE_VIEWPORT_INDEX_CHANGED,
|
||||
({ viewportIndex }) => {
|
||||
const viewportId = `viewport-${viewportIndex}`;
|
||||
({ viewportIndex, viewportId }) => {
|
||||
viewportId = viewportId || `viewport-${viewportIndex}`;
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup || !toolGroup._toolInstances?.['ReferenceLines']) {
|
||||
@ -427,9 +341,9 @@ function _showCPURenderingModal(uiModalService, hangingProtocolService) {
|
||||
};
|
||||
|
||||
const { unsubscribe } = hangingProtocolService.subscribe(
|
||||
hangingProtocolService.EVENTS.HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT,
|
||||
({ progress }) => {
|
||||
const done = callback(progress);
|
||||
hangingProtocolService.EVENTS.PROTOCOL_CHANGED,
|
||||
() => {
|
||||
const done = callback(100);
|
||||
|
||||
if (done) {
|
||||
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,
|
||||
annotation,
|
||||
ReferenceLinesTool,
|
||||
TrackballRotateTool,
|
||||
} from '@cornerstonejs/tools';
|
||||
|
||||
import CalibrationLineTool from './tools/CalibrationLineTool';
|
||||
@ -51,6 +52,7 @@ export default function initCornerstoneTools(configuration = {}) {
|
||||
addTool(SegmentationDisplayTool);
|
||||
addTool(ReferenceLinesTool);
|
||||
addTool(CalibrationLineTool);
|
||||
addTool(TrackballRotateTool);
|
||||
|
||||
// Modify annotation tools to use dashed lines on SR
|
||||
const annotationStyle = {
|
||||
@ -90,6 +92,7 @@ const toolNames = {
|
||||
SegmentationDisplay: SegmentationDisplayTool.toolName,
|
||||
ReferenceLines: ReferenceLinesTool.toolName,
|
||||
CalibrationLine: CalibrationLineTool.toolName,
|
||||
TrackballRotateTool: TrackballRotateTool.toolName,
|
||||
};
|
||||
|
||||
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(
|
||||
dataSource,
|
||||
displaySets,
|
||||
initialImageIndex
|
||||
initialImageIndex,
|
||||
cs3DViewportType
|
||||
);
|
||||
}
|
||||
|
||||
if (cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
viewportData = await this._getVolumeViewportData(dataSource, displaySets);
|
||||
if (
|
||||
cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC ||
|
||||
cs3DViewportType === Enums.ViewportType.VOLUME_3D
|
||||
) {
|
||||
viewportData = await this._getVolumeViewportData(
|
||||
dataSource,
|
||||
displaySets,
|
||||
cs3DViewportType
|
||||
);
|
||||
}
|
||||
|
||||
viewportData.viewportType = cs3DViewportType;
|
||||
@ -100,7 +108,8 @@ class CornerstoneCacheService {
|
||||
|
||||
const newViewportData = await this._getVolumeViewportData(
|
||||
dataSource,
|
||||
displaySets
|
||||
displaySets,
|
||||
viewportData.viewportType
|
||||
);
|
||||
|
||||
return newViewportData;
|
||||
@ -109,7 +118,8 @@ class CornerstoneCacheService {
|
||||
private _getStackViewportData(
|
||||
dataSource,
|
||||
displaySets,
|
||||
initialImageIndex
|
||||
initialImageIndex,
|
||||
viewportType: Enums.ViewportType
|
||||
): StackViewportData {
|
||||
// For Stack Viewport we don't have fusion currently
|
||||
const displaySet = displaySets[0];
|
||||
@ -126,7 +136,7 @@ class CornerstoneCacheService {
|
||||
const { displaySetInstanceUID, StudyInstanceUID } = displaySet;
|
||||
|
||||
const StackViewportData: StackViewportData = {
|
||||
viewportType: Enums.ViewportType.STACK,
|
||||
viewportType,
|
||||
data: {
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
@ -143,7 +153,8 @@ class CornerstoneCacheService {
|
||||
|
||||
private async _getVolumeViewportData(
|
||||
dataSource,
|
||||
displaySets
|
||||
displaySets,
|
||||
viewportType: Enums.ViewportType
|
||||
): Promise<VolumeViewportData> {
|
||||
// Todo: Check the cache for multiple scenarios to see if we need to
|
||||
// decache the volume data from other viewports or not
|
||||
@ -207,7 +218,7 @@ class CornerstoneCacheService {
|
||||
}
|
||||
|
||||
return {
|
||||
viewportType: Enums.ViewportType.ORTHOGRAPHIC,
|
||||
viewportType,
|
||||
data: volumeData,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
|
||||
import { pubSubServiceInterface } from '@ohif/core';
|
||||
import {
|
||||
utilities as cstUtils,
|
||||
segmentation as cstSegmentation,
|
||||
CONSTANTS as cstConstants,
|
||||
Enums as csToolsEnums,
|
||||
Types as cstTypes,
|
||||
} from '@cornerstonejs/tools';
|
||||
import {
|
||||
eventTarget,
|
||||
cache,
|
||||
eventTarget,
|
||||
getEnabledElementByIds,
|
||||
metaData,
|
||||
Types,
|
||||
utilities as csUtils,
|
||||
volumeLoader,
|
||||
Types,
|
||||
metaData,
|
||||
getEnabledElementByIds,
|
||||
} 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 { easeInOutBell } from '../../utils/transitions';
|
||||
import {
|
||||
@ -202,7 +202,7 @@ class SegmentationService {
|
||||
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
|
||||
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||||
segmentation,
|
||||
@ -1591,13 +1591,20 @@ class SegmentationService {
|
||||
|
||||
segmentInfo.isVisible = isVisible;
|
||||
|
||||
cstSegmentation.config.visibility.setVisibilityForSegmentIndex(
|
||||
cstSegmentation.config.visibility.setSegmentVisibility(
|
||||
toolGroupId,
|
||||
segmentationRepresentationUID,
|
||||
segmentIndex,
|
||||
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) {
|
||||
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||||
segmentation,
|
||||
@ -1917,28 +1924,27 @@ class SegmentationService {
|
||||
representation => representation.segmentationId === segmentationId
|
||||
);
|
||||
|
||||
const visibility = cstSegmentation.config.visibility.getSegmentationVisibility(
|
||||
toolGroupId,
|
||||
representation.segmentationRepresentationUID
|
||||
);
|
||||
const { segmentsHidden } = representation;
|
||||
|
||||
const currentVisibility = segmentsHidden.size === 0 ? true : false;
|
||||
const newVisibility = !currentVisibility;
|
||||
|
||||
cstSegmentation.config.visibility.setSegmentationVisibility(
|
||||
toolGroupId,
|
||||
representation.segmentationRepresentationUID,
|
||||
!visibility
|
||||
newVisibility
|
||||
);
|
||||
|
||||
// set all segments to visible as well
|
||||
const segments = this.getSegmentation(segmentationId).segments;
|
||||
Object.keys(segments).forEach(segmentIndex => {
|
||||
if (segmentIndex !== '0') {
|
||||
this._setSegmentVisibility(
|
||||
segmentationId,
|
||||
Number(segmentIndex),
|
||||
!visibility,
|
||||
toolGroupId
|
||||
);
|
||||
}
|
||||
// update segments visibility
|
||||
const { segmentation } = this._getSegmentationInfo(
|
||||
segmentationId,
|
||||
toolGroupId
|
||||
);
|
||||
|
||||
const segments = segmentation.segments.filter(Boolean);
|
||||
|
||||
segments.forEach(segment => {
|
||||
segment.isVisible = newVisibility;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { pubSubServiceInterface } from '@ohif/core';
|
||||
import { PubSubService } from '@ohif/core';
|
||||
import {
|
||||
RenderingEngine,
|
||||
StackViewport,
|
||||
@ -6,7 +6,10 @@ import {
|
||||
getRenderingEngine,
|
||||
utilities as csUtils,
|
||||
VolumeViewport,
|
||||
VolumeViewport3D,
|
||||
cache,
|
||||
utilities,
|
||||
CONSTANTS,
|
||||
} from '@cornerstonejs/core';
|
||||
|
||||
import { utilities as csToolsUtils } from '@cornerstonejs/tools';
|
||||
@ -21,6 +24,7 @@ import {
|
||||
StackViewportData,
|
||||
VolumeViewportData,
|
||||
} from '../../types/CornerstoneCacheService';
|
||||
import { Presentation, Presentations } from '../../types/Presentation';
|
||||
import {
|
||||
setColormap,
|
||||
setLowerUpperColorTransferFunction,
|
||||
@ -37,18 +41,14 @@ const EVENTS = {
|
||||
* Handles cornerstone viewport logic including enabling, disabling, and
|
||||
* updating the viewport.
|
||||
*/
|
||||
class CornerstoneViewportService implements IViewportService {
|
||||
class CornerstoneViewportService extends PubSubService
|
||||
implements IViewportService {
|
||||
renderingEngine: Types.IRenderingEngine | null;
|
||||
viewportsInfo: Map<number, ViewportInfo>;
|
||||
viewportsInfo: Map<number, ViewportInfo> = new Map();
|
||||
viewportsById: Map<string, ViewportInfo> = new Map();
|
||||
viewportGridResizeObserver: ResizeObserver | null;
|
||||
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
|
||||
enableResizeDetector: true;
|
||||
resizeRefreshRateMs: 200;
|
||||
@ -56,15 +56,10 @@ class CornerstoneViewportService implements IViewportService {
|
||||
servicesManager = null;
|
||||
|
||||
constructor(servicesManager) {
|
||||
super(EVENTS);
|
||||
this.renderingEngine = null;
|
||||
this.viewportGridResizeObserver = null;
|
||||
this.viewportsInfo = new Map();
|
||||
//
|
||||
this.listeners = {};
|
||||
this.EVENTS = EVENTS;
|
||||
this.servicesManager = servicesManager;
|
||||
Object.assign(this, pubSubServiceInterface);
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,12 +72,23 @@ class CornerstoneViewportService implements IViewportService {
|
||||
viewportOptions: PublicViewportOptions,
|
||||
elementRef: HTMLDivElement
|
||||
) {
|
||||
const viewportInfo = new ViewportInfo(
|
||||
viewportIndex,
|
||||
this.getViewportId(viewportIndex)
|
||||
);
|
||||
// Use the provided viewportId
|
||||
// Not providing a viewportId is frowned upon because it does weird things
|
||||
// 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);
|
||||
this.viewportsInfo.set(viewportIndex, viewportInfo);
|
||||
this.viewportsById.set(viewportId, viewportInfo);
|
||||
}
|
||||
|
||||
public getViewportIds(): string[] {
|
||||
@ -96,7 +102,7 @@ class CornerstoneViewportService implements IViewportService {
|
||||
}
|
||||
|
||||
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
|
||||
* 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
|
||||
*/
|
||||
public disableElement(viewportIndex: number) {
|
||||
public disableElement(viewportIndex: number): void {
|
||||
const viewportInfo = this.viewportsInfo.get(viewportIndex);
|
||||
if (!viewportInfo) {
|
||||
return;
|
||||
@ -163,6 +174,39 @@ class CornerstoneViewportService implements IViewportService {
|
||||
|
||||
this.viewportsInfo.get(viewportIndex).destroy();
|
||||
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,
|
||||
viewportData: StackViewportData | VolumeViewportData,
|
||||
publicViewportOptions: PublicViewportOptions,
|
||||
publicDisplaySetOptions: DisplaySetOptions[]
|
||||
publicDisplaySetOptions: DisplaySetOptions[],
|
||||
presentations?: Presentations
|
||||
): void {
|
||||
const renderingEngine = this.getRenderingEngine();
|
||||
const viewportInfo = this.viewportsInfo.get(viewportIndex);
|
||||
|
||||
if (!publicViewportOptions.viewportId) {
|
||||
publicViewportOptions.viewportId = this.getViewportId(viewportIndex);
|
||||
const viewportId =
|
||||
publicViewportOptions.viewportId || this.getViewportId(viewportIndex);
|
||||
if (!viewportId) {
|
||||
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
|
||||
// as the one we are trying to set, we need to disable the old one
|
||||
// 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);
|
||||
if (!viewportInfo) {
|
||||
throw new Error('Viewport info not defined');
|
||||
}
|
||||
|
||||
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);
|
||||
@ -217,12 +259,6 @@ class CornerstoneViewportService implements IViewportService {
|
||||
viewportInfo.setDisplaySetOptions(displaySetOptions);
|
||||
viewportInfo.setViewportData(viewportData);
|
||||
|
||||
this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, {
|
||||
viewportData,
|
||||
viewportIndex,
|
||||
});
|
||||
|
||||
viewportId = viewportInfo.getViewportId();
|
||||
const element = viewportInfo.getElement();
|
||||
const type = viewportInfo.getViewportType();
|
||||
const background = viewportInfo.getBackground();
|
||||
@ -245,7 +281,16 @@ class CornerstoneViewportService implements IViewportService {
|
||||
renderingEngine.enableElement(viewportInput);
|
||||
|
||||
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(
|
||||
@ -308,8 +353,9 @@ class CornerstoneViewportService implements IViewportService {
|
||||
_setStackViewport(
|
||||
viewport: Types.IStackViewport,
|
||||
viewportData: StackViewportData,
|
||||
viewportInfo: ViewportInfo
|
||||
) {
|
||||
viewportInfo: ViewportInfo,
|
||||
presentations: Presentations
|
||||
): void {
|
||||
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
||||
|
||||
const {
|
||||
@ -320,29 +366,38 @@ class CornerstoneViewportService implements IViewportService {
|
||||
|
||||
this.viewportsDisplaySets.set(viewport.id, [displaySetInstanceUID]);
|
||||
|
||||
let initialImageIndexToUse = initialImageIndex;
|
||||
let initialImageIndexToUse =
|
||||
presentations?.positionPresentation?.initialImageIndex ??
|
||||
initialImageIndex;
|
||||
|
||||
if (!initialImageIndexToUse) {
|
||||
if (
|
||||
initialImageIndexToUse === undefined ||
|
||||
initialImageIndexToUse === null
|
||||
) {
|
||||
initialImageIndexToUse =
|
||||
this._getInitialImageIndexForStackViewport(viewportInfo, imageIds) || 0;
|
||||
}
|
||||
|
||||
const { voi, voiInverted } = displaySetOptions[0];
|
||||
const properties = {};
|
||||
if (voi && (voi.windowWidth || voi.windowCenter)) {
|
||||
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
|
||||
voi.windowWidth,
|
||||
voi.windowCenter
|
||||
);
|
||||
properties.voiRange = { lower, upper };
|
||||
}
|
||||
const properties = { ...presentations.lutPresentation?.properties };
|
||||
if (!presentations.lutPresentation?.properties) {
|
||||
const { voi, voiInverted } = displaySetOptions[0];
|
||||
if (voi && (voi.windowWidth || voi.windowCenter)) {
|
||||
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
|
||||
voi.windowWidth,
|
||||
voi.windowCenter
|
||||
);
|
||||
properties.voiRange = { lower, upper };
|
||||
}
|
||||
|
||||
if (voiInverted !== undefined) {
|
||||
properties.invert = voiInverted;
|
||||
if (voiInverted !== undefined) {
|
||||
properties.invert = voiInverted;
|
||||
}
|
||||
}
|
||||
|
||||
viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
|
||||
viewport.setProperties(properties);
|
||||
const camera = presentations.positionPresentation?.camera;
|
||||
if (camera) viewport.setCamera(camera);
|
||||
});
|
||||
}
|
||||
|
||||
@ -397,7 +452,8 @@ class CornerstoneViewportService implements IViewportService {
|
||||
async _setVolumeViewport(
|
||||
viewport: Types.IVolumeViewport,
|
||||
viewportData: VolumeViewportData,
|
||||
viewportInfo: ViewportInfo
|
||||
viewportInfo: ViewportInfo,
|
||||
presentations: Presentations
|
||||
): Promise<void> {
|
||||
// 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
|
||||
@ -423,6 +479,7 @@ class CornerstoneViewportService implements IViewportService {
|
||||
displaySetInstanceUIDs.push(displaySetInstanceUID);
|
||||
|
||||
if (!volume) {
|
||||
console.log('Volume display set not found');
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -460,14 +517,24 @@ class CornerstoneViewportService implements IViewportService {
|
||||
}
|
||||
|
||||
volumeToLoad.forEach(volume => {
|
||||
volume.load();
|
||||
if (!volume.loadStatus.loaded && !volume.loadStatus.loading) {
|
||||
volume.load();
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
displaySetService,
|
||||
segmentationService,
|
||||
@ -475,6 +542,7 @@ class CornerstoneViewportService implements IViewportService {
|
||||
} = this.servicesManager.services;
|
||||
|
||||
await viewport.setVolumes(volumeInputArray);
|
||||
this.setPresentations(viewport, presentations);
|
||||
|
||||
// load any secondary displaySets
|
||||
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
|
||||
@ -557,6 +625,10 @@ class CornerstoneViewportService implements IViewportService {
|
||||
|
||||
const viewportInfo = this.getViewportInfo(viewport.id);
|
||||
|
||||
if (!viewportInfo) {
|
||||
console.warn('Viewport info not defined for', viewport.id);
|
||||
}
|
||||
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.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
|
||||
// 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 viewportId = viewportInfo.getViewportId();
|
||||
@ -614,7 +690,12 @@ class CornerstoneViewportService implements IViewportService {
|
||||
}
|
||||
|
||||
_getVOICallbacks(volumeId, displaySetOptions) {
|
||||
const { voi, voiInverted: inverted, colormap } = displaySetOptions;
|
||||
const {
|
||||
voi,
|
||||
voiInverted: inverted,
|
||||
colormap,
|
||||
presetName,
|
||||
} = displaySetOptions;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
_setDisplaySets(
|
||||
viewport: StackViewport | VolumeViewport,
|
||||
viewportData: StackViewportData | VolumeViewportData,
|
||||
viewportInfo: ViewportInfo
|
||||
viewportInfo: ViewportInfo,
|
||||
presentations: Presentations = {}
|
||||
): void {
|
||||
if (viewport instanceof StackViewport) {
|
||||
this._setStackViewport(
|
||||
viewport,
|
||||
viewportData as StackViewportData,
|
||||
viewportInfo
|
||||
viewportInfo,
|
||||
presentations
|
||||
);
|
||||
} else if (viewport instanceof VolumeViewport) {
|
||||
} else if (
|
||||
viewport instanceof VolumeViewport ||
|
||||
viewport instanceof VolumeViewport3D
|
||||
) {
|
||||
this._setVolumeViewport(
|
||||
viewport,
|
||||
viewportData as VolumeViewportData,
|
||||
viewportInfo
|
||||
viewportInfo,
|
||||
presentations
|
||||
);
|
||||
} else {
|
||||
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 {
|
||||
name: '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 as UITypes } from '@ohif/ui';
|
||||
import {
|
||||
StackViewportData,
|
||||
VolumeViewportData,
|
||||
} from '../../types/CornerstoneCacheService';
|
||||
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
|
||||
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
|
||||
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
||||
import JumpPresets from '../../utils/JumpPresets';
|
||||
import { SyncGroup } from '../SyncGroupService/SyncGroupService';
|
||||
import {
|
||||
StackViewportData,
|
||||
VolumeViewportData,
|
||||
} from '../../types/CornerstoneCacheService';
|
||||
|
||||
export type InitialImageOptions = {
|
||||
index?: number;
|
||||
@ -15,10 +16,13 @@ export type InitialImageOptions = {
|
||||
};
|
||||
|
||||
export type ViewportOptions = {
|
||||
id?: string;
|
||||
viewportType: Enums.ViewportType;
|
||||
toolGroupId: string;
|
||||
viewportId: string;
|
||||
orientation?: Types.Orientation;
|
||||
// Presentation ID to store/load presentation state from
|
||||
presentationIds?: UITypes.PresentationIds;
|
||||
orientation?: Enums.OrientationAxis;
|
||||
background?: Types.Point3;
|
||||
syncGroups?: SyncGroup[];
|
||||
initialImageOptions?: InitialImageOptions;
|
||||
@ -31,10 +35,12 @@ export type ViewportOptions = {
|
||||
};
|
||||
|
||||
export type PublicViewportOptions = {
|
||||
id?: string;
|
||||
viewportType?: string;
|
||||
toolGroupId?: string;
|
||||
presentationIds?: UITypes.PresentationIds;
|
||||
viewportId?: string;
|
||||
orientation?: string;
|
||||
orientation?: Enums.OrientationAxis;
|
||||
background?: Types.Point3;
|
||||
syncGroups?: SyncGroup[];
|
||||
initialImageOptions?: InitialImageOptions;
|
||||
@ -42,20 +48,32 @@ export type PublicViewportOptions = {
|
||||
allowUnmatchedView?: boolean;
|
||||
};
|
||||
|
||||
export type DisplaySetSelector = {
|
||||
id?: string;
|
||||
options?: 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;
|
||||
voiInverted?: boolean;
|
||||
blendMode?: string;
|
||||
slabThickness?: number;
|
||||
colormap?: string;
|
||||
presetName?: string;
|
||||
};
|
||||
|
||||
export type DisplaySetOptions = {
|
||||
id?: string;
|
||||
voi?: VOI;
|
||||
voiInverted: boolean;
|
||||
blendMode?: Enums.BlendModes;
|
||||
slabThickness?: number;
|
||||
colormap?: string;
|
||||
presetName?: string;
|
||||
};
|
||||
|
||||
type VOI = {
|
||||
@ -68,7 +86,6 @@ export type DisplaySet = {
|
||||
};
|
||||
|
||||
const STACK = 'stack';
|
||||
const VOLUME = 'volume';
|
||||
const DEFAULT_TOOLGROUP_ID = 'default';
|
||||
|
||||
class ViewportInfo {
|
||||
@ -136,7 +153,7 @@ class ViewportInfo {
|
||||
}
|
||||
|
||||
public setPublicDisplaySetOptions(
|
||||
publicDisplaySetOptions: Array<PublicDisplaySetOptions>
|
||||
publicDisplaySetOptions: PublicDisplaySetOptions[] | DisplaySetSelector[]
|
||||
): void {
|
||||
// map the displaySetOptions and check if they are undefined then set them to default values
|
||||
const displaySetOptions = this.mapDisplaySetOptions(
|
||||
@ -167,7 +184,10 @@ class ViewportInfo {
|
||||
viewportOptionsEntry: PublicViewportOptions
|
||||
): void {
|
||||
let viewportType = viewportOptionsEntry.viewportType;
|
||||
let toolGroupId = viewportOptionsEntry.toolGroupId;
|
||||
const {
|
||||
toolGroupId = DEFAULT_TOOLGROUP_ID,
|
||||
presentationIds,
|
||||
} = viewportOptionsEntry;
|
||||
let orientation;
|
||||
|
||||
if (!viewportType) {
|
||||
@ -179,14 +199,8 @@ class ViewportInfo {
|
||||
}
|
||||
|
||||
// map SAGITTAL, AXIAL, CORONAL orientation to be used by cornerstone
|
||||
if (viewportOptionsEntry.viewportType?.toLowerCase() === VOLUME) {
|
||||
if (viewportOptionsEntry.viewportType?.toLowerCase() !== STACK) {
|
||||
orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation);
|
||||
} else {
|
||||
orientation = Enums.OrientationAxis.AXIAL;
|
||||
}
|
||||
|
||||
if (!toolGroupId) {
|
||||
toolGroupId = DEFAULT_TOOLGROUP_ID;
|
||||
}
|
||||
|
||||
this.setViewportOptions({
|
||||
@ -195,6 +209,7 @@ class ViewportInfo {
|
||||
viewportType: viewportType as Enums.ViewportType,
|
||||
orientation,
|
||||
toolGroupId,
|
||||
presentationIds,
|
||||
});
|
||||
}
|
||||
|
||||
@ -232,7 +247,7 @@ class ViewportInfo {
|
||||
return this.viewportOptions.background || [0, 0, 0];
|
||||
}
|
||||
|
||||
public getOrientation(): Types.Orientation {
|
||||
public getOrientation(): Enums.OrientationAxis {
|
||||
return this.viewportOptions.orientation;
|
||||
}
|
||||
|
||||
@ -240,12 +255,15 @@ class ViewportInfo {
|
||||
return this.viewportOptions.initialImageOptions;
|
||||
}
|
||||
|
||||
// Handle incoming public display set options or a display set select
|
||||
// with a contained options.
|
||||
private mapDisplaySetOptions(
|
||||
publicDisplaySetOptions: Array<PublicDisplaySetOptions>
|
||||
options: PublicDisplaySetOptions[] | DisplaySetSelector[] = [{}]
|
||||
): Array<DisplaySetOptions> {
|
||||
const displaySetOptions: Array<DisplaySetOptions> = [];
|
||||
|
||||
publicDisplaySetOptions.forEach(option => {
|
||||
options.forEach(item => {
|
||||
let option = item?.options || item;
|
||||
if (!option) {
|
||||
option = {
|
||||
blendMode: undefined,
|
||||
@ -263,6 +281,7 @@ class ViewportInfo {
|
||||
colormap: option.colormap,
|
||||
slabThickness: option.slabThickness,
|
||||
blendMode,
|
||||
presetName: option.presetName,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { metaData } from '@cornerstonejs/core';
|
||||
import { LengthTool } from '@cornerstonejs/tools';
|
||||
import { calibrateImageSpacing } from '@cornerstonejs/tools/dist/esm/utilities';
|
||||
import { LengthTool, utilities } from '@cornerstonejs/tools';
|
||||
import callInputDialog from '../utils/callInputDialog';
|
||||
import getActiveViewportEnabledElement from '../utils/getActiveViewportEnabledElement';
|
||||
|
||||
const { calibrateImageSpacing } = utilities;
|
||||
|
||||
/**
|
||||
* 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 { log } from '@ohif/core';
|
||||
|
||||
const AXIAL = 'axial';
|
||||
const SAGITTAL = 'sagittal';
|
||||
|
||||
@ -2,18 +2,25 @@ import { Enums } from '@cornerstonejs/core';
|
||||
|
||||
const STACK = 'stack';
|
||||
const VOLUME = 'volume';
|
||||
const ORTHOGRAPHIC = 'orthographic';
|
||||
const VOLUME_3D = 'volume3d';
|
||||
|
||||
export default function getCornerstoneViewportType(
|
||||
viewportType: string
|
||||
): Enums.ViewportType {
|
||||
if (viewportType.toLowerCase() === STACK) {
|
||||
const lowerViewportType = viewportType.toLowerCase();
|
||||
if (lowerViewportType === STACK) {
|
||||
return Enums.ViewportType.STACK;
|
||||
}
|
||||
|
||||
if (viewportType.toLowerCase() === VOLUME) {
|
||||
if (lowerViewportType === VOLUME || lowerViewportType === ORTHOGRAPHIC) {
|
||||
return Enums.ViewportType.ORTHOGRAPHIC;
|
||||
}
|
||||
|
||||
if (lowerViewportType === VOLUME_3D) {
|
||||
return Enums.ViewportType.VOLUME_3D;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`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({
|
||||
data: { viewportId, volumeInputArray },
|
||||
displaySetsMatchDetails,
|
||||
viewportMatchDetails: matchDetails,
|
||||
}) {
|
||||
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);
|
||||
|
||||
@ -29,6 +28,7 @@ export default function interleaveNthLoader({
|
||||
const volume = cache.getVolume(volumeId);
|
||||
|
||||
if (!volume) {
|
||||
console.log("interleaveNthLoader::No volume, can't load it");
|
||||
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();
|
||||
// get volumes from cache
|
||||
const volumes = volumeIds.map(volumeId => {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"license": "MIT",
|
||||
"repository": "OHIF/Viewers",
|
||||
"main": "dist/index.umd.js",
|
||||
"module": "src/index.js",
|
||||
"module": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"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 => {
|
||||
const { SeriesInstanceUID } = aSeries;
|
||||
|
||||
aSeries.instances.forEach(instance => {
|
||||
const isMultiframe = aSeries.instances[0].NumberOfFrames > 1;
|
||||
|
||||
aSeries.instances.forEach((instance, index) => {
|
||||
const {
|
||||
url: imageId,
|
||||
StudyInstanceUID,
|
||||
@ -153,6 +155,7 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex: isMultiframe ? index : 1,
|
||||
});
|
||||
});
|
||||
|
||||
@ -185,7 +188,7 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
displaySet.images.forEach(instance => {
|
||||
const NumberOfFrames = instance.NumberOfFrames;
|
||||
if (NumberOfFrames > 1) {
|
||||
for (let i = 0; i < NumberOfFrames; i++) {
|
||||
for (let i = 1; i <= NumberOfFrames; i++) {
|
||||
const imageId = this.getImageIdsForInstance({
|
||||
instance,
|
||||
frame: i,
|
||||
|
||||
@ -104,7 +104,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
||||
query: {
|
||||
studies: {
|
||||
mapParams: mapParams.bind(),
|
||||
search: async function (origParams) {
|
||||
search: async function(origParams) {
|
||||
const headers = userAuthenticationService.getAuthorizationHeader();
|
||||
if (headers) {
|
||||
qidoDicomWebClient.headers = headers;
|
||||
@ -129,7 +129,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
||||
},
|
||||
series: {
|
||||
// mapParams: mapParams.bind(),
|
||||
search: async function (studyInstanceUid) {
|
||||
search: async function(studyInstanceUid) {
|
||||
const headers = userAuthenticationService.getAuthorizationHeader();
|
||||
if (headers) {
|
||||
qidoDicomWebClient.headers = headers;
|
||||
|
||||
@ -218,6 +218,7 @@ export default function PanelMeasurementTable({
|
||||
>
|
||||
<MeasurementTable
|
||||
title="Measurements"
|
||||
servicesManager={servicesManager}
|
||||
data={displayMeasurements}
|
||||
onClick={jumpToImage}
|
||||
onEdit={onMeasurementItemEditHandler}
|
||||
@ -248,14 +249,46 @@ function _getMappedMeasurements(measurementService) {
|
||||
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) {
|
||||
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 {
|
||||
uid,
|
||||
label: label || '(empty)',
|
||||
label,
|
||||
baseLabel,
|
||||
measurementType: type,
|
||||
displayText: displayText || [],
|
||||
displayText,
|
||||
baseDisplayText,
|
||||
isActive: selected,
|
||||
finding,
|
||||
findingSites,
|
||||
};
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ function PanelStudyBrowser({
|
||||
uiNotificationService.show({
|
||||
title: 'Thumbnail Double Click',
|
||||
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',
|
||||
duration: 3000,
|
||||
});
|
||||
@ -303,6 +303,7 @@ function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
|
||||
seriesDate: ds.SeriesDate,
|
||||
seriesTime: ds.SeriesTime,
|
||||
numInstances: ds.numImageFrames,
|
||||
countIcon: ds.countIcon,
|
||||
StudyInstanceUID: ds.StudyInstanceUID,
|
||||
componentType,
|
||||
imageSrc,
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
LayoutSelector as OHIFLayoutSelector,
|
||||
ToolbarButton,
|
||||
useViewportGrid,
|
||||
} from '@ohif/ui';
|
||||
import { LayoutSelector as OHIFLayoutSelector, ToolbarButton } from '@ohif/ui';
|
||||
|
||||
import { ServicesManager } from '@ohif/core';
|
||||
|
||||
@ -16,8 +12,6 @@ function LayoutSelector({
|
||||
...rest
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [disableSelector, setDisableSelector] = useState(false);
|
||||
const [viewportGridState, viewportGridService] = useViewportGrid();
|
||||
|
||||
const {
|
||||
hangingProtocolService,
|
||||
@ -50,43 +44,19 @@ function LayoutSelector({
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
/* Reset to default layout when component unmounts */
|
||||
return () => {
|
||||
viewportGridService.setLayout({ numCols: 1, numRows: 1 });
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onInteractionHandler = () => setIsOpen(!isOpen);
|
||||
const DropdownContent = isOpen ? OHIFLayoutSelector : null;
|
||||
|
||||
const onSelectionHandler = ({ numRows, numCols }) => {
|
||||
// TODO Introduce a service to persist the state of the current hanging protocol/app.
|
||||
|
||||
// TODO Here the layout change will amount to a change of hanging protocol as specified by the extension for this layout selector tool
|
||||
// followed by the change of the grid itself.
|
||||
if (hangingProtocolService.getActiveProtocol().protocol.id === 'mpr') {
|
||||
toolbarService.recordInteraction({
|
||||
groupId: 'MPR',
|
||||
itemId: 'MPR',
|
||||
interactionType: 'toggle',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'toggleMPR',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// When a new layout is selected, keep any extra/offscreen viewports
|
||||
// so that if any of those viewports were populated via the UI then they
|
||||
// will be maintained in case those viewports are redisplayed later.
|
||||
viewportGridService.setLayout({
|
||||
numRows,
|
||||
numCols,
|
||||
keepExtraViewports: true,
|
||||
const onSelectionHandler = props => {
|
||||
toolbarService.recordInteraction({
|
||||
interactionType: 'action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setViewportGridLayout',
|
||||
commandOptions: { ...props },
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
@ -107,7 +77,7 @@ function LayoutSelector({
|
||||
/>
|
||||
)
|
||||
}
|
||||
isActive={disableSelector ? false : isOpen}
|
||||
isActive={isOpen}
|
||||
type="toggle"
|
||||
/>
|
||||
);
|
||||
|
||||
@ -14,7 +14,12 @@ import {
|
||||
LoadingIndicatorProgress,
|
||||
} from '@ohif/ui';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { hotkeys } from '@ohif/core';
|
||||
import {
|
||||
ServicesManager,
|
||||
HangingProtocolService,
|
||||
hotkeys,
|
||||
CommandsManager,
|
||||
} from '@ohif/core';
|
||||
import { useAppConfig } from '@state';
|
||||
import Toolbar from '../Toolbar/Toolbar';
|
||||
|
||||
@ -33,7 +38,7 @@ function ViewerLayout({
|
||||
rightPanels = [],
|
||||
leftPanelDefaultClosed = false,
|
||||
rightPanelDefaultClosed = false,
|
||||
}) {
|
||||
}): React.FunctionComponent {
|
||||
const [appConfig] = useAppConfig();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
@ -169,15 +174,13 @@ function ViewerLayout({
|
||||
|
||||
useEffect(() => {
|
||||
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
|
||||
// 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.
|
||||
({ progress }) => {
|
||||
if (progress === 100) {
|
||||
setShowLoadingIndicator(false);
|
||||
}
|
||||
() => {
|
||||
setShowLoadingIndicator(false);
|
||||
}
|
||||
);
|
||||
|
||||
@ -265,7 +268,8 @@ ViewerLayout.propTypes = {
|
||||
extensionManager: PropTypes.shape({
|
||||
getModuleEntry: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
commandsManager: PropTypes.object,
|
||||
commandsManager: PropTypes.instanceOf(CommandsManager),
|
||||
servicesManager: PropTypes.instanceOf(ServicesManager),
|
||||
// From modes
|
||||
leftPanels: 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 DataSourceSelector from './Panels/DataSourceSelector';
|
||||
|
||||
@ -82,7 +83,6 @@ export default function getCustomizationModule() {
|
||||
*/
|
||||
{
|
||||
id: 'ohif.overlayItem',
|
||||
uiType: 'uiType',
|
||||
content: function (props) {
|
||||
if (this.condition && !this.condition(props)) return null;
|
||||
|
||||
@ -91,8 +91,8 @@ export default function getCustomizationModule() {
|
||||
instance && this.attribute
|
||||
? instance[this.attribute]
|
||||
: this.contentF && typeof this.contentF === 'function'
|
||||
? this.contentF(props)
|
||||
: null;
|
||||
? this.contentF(props)
|
||||
: null;
|
||||
if (!value) return null;
|
||||
|
||||
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 = {
|
||||
id: 'default',
|
||||
locked: true,
|
||||
// Don't store this hanging protocol as it applies to the currently active
|
||||
// display set by default
|
||||
// cacheId: null,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'Default',
|
||||
createdDate: '2021-02-23T19:22:08.894Z',
|
||||
@ -9,6 +12,25 @@ const defaultProtocol = {
|
||||
editableBy: {},
|
||||
protocolMatchingRules: [],
|
||||
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: {
|
||||
defaultDisplaySetId: {
|
||||
// Unused currently
|
||||
@ -24,12 +46,12 @@ const defaultProtocol = {
|
||||
},
|
||||
},
|
||||
],
|
||||
studyMatchingRules: [],
|
||||
// Can be used to select matching studies
|
||||
// studyMatchingRules: [],
|
||||
},
|
||||
},
|
||||
stages: [
|
||||
{
|
||||
id: 'hYbmMy3b7pz7GLiaT',
|
||||
name: 'default',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
@ -41,6 +63,7 @@ const defaultProtocol = {
|
||||
viewports: [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'stack',
|
||||
toolGroupId: 'default',
|
||||
// initialImageOptions: {
|
||||
// index: 180,
|
||||
@ -56,14 +79,218 @@ const defaultProtocol = {
|
||||
],
|
||||
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() {
|
||||
return [
|
||||
{
|
||||
id: defaultProtocol.id,
|
||||
name: defaultProtocol.id,
|
||||
protocol: defaultProtocol,
|
||||
},
|
||||
];
|
||||
|
||||
@ -29,6 +29,7 @@ const makeDisplaySet = instances => {
|
||||
SeriesDescription: instance.SeriesDescription || '',
|
||||
Modality: instance.Modality,
|
||||
isMultiFrame: isMultiFrame(instance),
|
||||
countIcon: displayReconstructableInfo.value ? 'icon-mpr' : undefined,
|
||||
numImageFrames: instances.length,
|
||||
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`,
|
||||
isReconstructable: displayReconstructableInfo.value,
|
||||
|
||||
@ -1,32 +1,34 @@
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
import getDataSourcesModule from './getDataSourcesModule.js';
|
||||
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
||||
import getPanelModule from './getPanelModule';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule.js';
|
||||
import getToolbarModule from './getToolbarModule';
|
||||
import commandsModule from './commandsModule';
|
||||
import getCommandsModule from './commandsModule';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import getStudiesForPatientByStudyInstanceUID from './Panels/getStudiesForPatientByStudyInstanceUID';
|
||||
import getCustomizationModule from './getCustomizationModule';
|
||||
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.
|
||||
*/
|
||||
id,
|
||||
preRegistration: ({ servicesManager, configuration = {} }) => {
|
||||
init({ servicesManager, configuration });
|
||||
},
|
||||
preRegistration,
|
||||
getDataSourcesModule,
|
||||
getLayoutTemplateModule,
|
||||
getPanelModule,
|
||||
getHangingProtocolModule,
|
||||
getSopClassHandlerModule,
|
||||
getToolbarModule,
|
||||
getCommandsModule({ servicesManager, commandsManager }) {
|
||||
return commandsModule({ servicesManager, commandsManager });
|
||||
},
|
||||
getCommandsModule,
|
||||
getUtilityModule({ servicesManager }) {
|
||||
return [
|
||||
{
|
||||
@ -42,3 +44,5 @@ const defaultExtension = {
|
||||
};
|
||||
|
||||
export default defaultExtension;
|
||||
|
||||
export { ContextMenuController, CustomizeableContextMenuTypes };
|
||||
@ -10,7 +10,8 @@ const metadataProvider = classes.MetadataProvider;
|
||||
* @param {Object} servicesManager
|
||||
* @param {Object} configuration
|
||||
*/
|
||||
export default function init({ servicesManager, configuration }) {
|
||||
export default function init({ servicesManager, configuration = {} }): void {
|
||||
const { stateSyncService } = servicesManager.services;
|
||||
// Add
|
||||
DicomMetadataStore.subscribe(
|
||||
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
|
||||
@ -23,6 +24,34 @@ export default function init({ servicesManager, configuration }) {
|
||||
DicomMetadataStore.EVENTS.SERIES_UPDATED,
|
||||
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 }) => {
|
||||
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": {
|
||||
"@ohif/core": "^3.0.0",
|
||||
"classnames": "^2.3.2",
|
||||
"@cornerstonejs/core": "^0.33.2",
|
||||
"@cornerstonejs/tools": "^0.50.2",
|
||||
"@cornerstonejs/core": "^0.40.0",
|
||||
"@cornerstonejs/tools": "^0.60.1",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "^3.0.0",
|
||||
"dcmjs": "^0.29.4",
|
||||
"lodash.debounce": "^4.17.21",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
||||
@ -7,12 +7,12 @@ const RESPONSE = {
|
||||
};
|
||||
|
||||
function promptBeginTracking({ servicesManager, extensionManager }, ctx, evt) {
|
||||
const { UIViewportDialogService } = servicesManager.services;
|
||||
const { uiViewportDialogService } = servicesManager.services;
|
||||
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
|
||||
|
||||
return new Promise(async function(resolve, reject) {
|
||||
let promptResult = await _askTrackMeasurements(
|
||||
UIViewportDialogService,
|
||||
uiViewportDialogService,
|
||||
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) {
|
||||
const message = 'Track measurements for this series?';
|
||||
const actions = [
|
||||
@ -49,11 +49,11 @@ function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
|
||||
},
|
||||
];
|
||||
const onSubmit = result => {
|
||||
UIViewportDialogService.hide();
|
||||
uiViewportDialogService.hide();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
UIViewportDialogService.show({
|
||||
uiViewportDialogService.show({
|
||||
viewportIndex,
|
||||
id: 'measurement-tracking-prompt-begin-tracking',
|
||||
type: 'info',
|
||||
@ -61,7 +61,7 @@ function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
|
||||
actions,
|
||||
onSubmit,
|
||||
onOutsideClick: () => {
|
||||
UIViewportDialogService.hide();
|
||||
uiViewportDialogService.hide();
|
||||
resolve(RESPONSE.CANCEL);
|
||||
},
|
||||
});
|
||||
|
||||
@ -16,7 +16,7 @@ function promptHydrateStructuredReport(
|
||||
evt
|
||||
) {
|
||||
const {
|
||||
UIViewportDialogService,
|
||||
uiViewportDialogService,
|
||||
displaySetService,
|
||||
} = servicesManager.services;
|
||||
const { viewportIndex, displaySetInstanceUID } = evt;
|
||||
@ -26,7 +26,7 @@ function promptHydrateStructuredReport(
|
||||
|
||||
return new Promise(async function(resolve, reject) {
|
||||
const promptResult = await _askTrackMeasurements(
|
||||
UIViewportDialogService,
|
||||
uiViewportDialogService,
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
@ -55,7 +55,7 @@ function promptHydrateStructuredReport(
|
||||
});
|
||||
}
|
||||
|
||||
function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
|
||||
function _askTrackMeasurements(uiViewportDialogService, viewportIndex) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
const message =
|
||||
'Do you want to continue tracking measurements for this study?';
|
||||
@ -72,18 +72,18 @@ function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
|
||||
},
|
||||
];
|
||||
const onSubmit = result => {
|
||||
UIViewportDialogService.hide();
|
||||
uiViewportDialogService.hide();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
UIViewportDialogService.show({
|
||||
uiViewportDialogService.show({
|
||||
viewportIndex,
|
||||
type: 'info',
|
||||
message,
|
||||
actions,
|
||||
onSubmit,
|
||||
onOutsideClick: () => {
|
||||
UIViewportDialogService.hide();
|
||||
uiViewportDialogService.hide();
|
||||
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) {
|
||||
const message =
|
||||
'Do you want to add this measurement to the existing report?';
|
||||
@ -51,18 +51,18 @@ function _askShouldAddMeasurements(UIViewportDialogService, viewportIndex) {
|
||||
},
|
||||
];
|
||||
const onSubmit = result => {
|
||||
UIViewportDialogService.hide();
|
||||
uiViewportDialogService.hide();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
UIViewportDialogService.show({
|
||||
uiViewportDialogService.show({
|
||||
viewportIndex,
|
||||
type: 'info',
|
||||
message,
|
||||
actions,
|
||||
onSubmit,
|
||||
onOutsideClick: () => {
|
||||
UIViewportDialogService.hide();
|
||||
uiViewportDialogService.hide();
|
||||
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 {
|
||||
StudySummary,
|
||||
@ -11,6 +11,7 @@ import { DicomMetadataStore, utils } from '@ohif/core';
|
||||
import { useDebounce } from '@hooks';
|
||||
import ActionButtons from './ActionButtons';
|
||||
import { useTrackedMeasurements } from '../../getContextModule';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
const { downloadCSVReport } = utils;
|
||||
const { formatDate } = utils;
|
||||
@ -45,6 +46,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
DISPLAY_STUDY_SUMMARY_INITIAL_VALUE
|
||||
);
|
||||
const [displayMeasurements, setDisplayMeasurements] = useState([]);
|
||||
const measurementsPanelRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const measurements = measurementService.getMeasurements();
|
||||
@ -125,6 +127,12 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
subscriptions.push(
|
||||
measurementService.subscribe(evt, () => {
|
||||
setMeasurementsUpdated(Date.now().toString());
|
||||
if (evt === added) {
|
||||
debounce(() => {
|
||||
measurementsPanelRef.current.scrollTop =
|
||||
measurementsPanelRef.current.scrollHeight;
|
||||
}, 300)();
|
||||
}
|
||||
}).unsubscribe
|
||||
);
|
||||
});
|
||||
@ -241,6 +249,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
<>
|
||||
<div
|
||||
className="overflow-x-hidden overflow-y-auto invisible-scrollbar"
|
||||
ref={measurementsPanelRef}
|
||||
data-cy={'trackedMeasurements-panel'}
|
||||
>
|
||||
{displayStudySummary.key && (
|
||||
@ -253,6 +262,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
<MeasurementTable
|
||||
title="Measurements"
|
||||
data={displayMeasurementsWithoutFindings}
|
||||
servicesManager={servicesManager}
|
||||
onClick={jumpToImage}
|
||||
onEdit={onMeasurementItemEditHandler}
|
||||
/>
|
||||
@ -260,6 +270,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
<MeasurementTable
|
||||
title="Additional Findings"
|
||||
data={additionalFindings}
|
||||
servicesManager={servicesManager}
|
||||
onClick={jumpToImage}
|
||||
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 {
|
||||
uid: measurement.uid,
|
||||
label: measurement.label || '(empty)',
|
||||
measurementType: measurement.type,
|
||||
displayText: displayText || [],
|
||||
isActive: measurement.selected,
|
||||
uid,
|
||||
label,
|
||||
baseLabel,
|
||||
measurementType: type,
|
||||
displayText,
|
||||
baseDisplayText,
|
||||
isActive: selected,
|
||||
finding,
|
||||
findingSites,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -453,6 +453,7 @@ function _mapDisplaySets(
|
||||
modality: ds.Modality,
|
||||
seriesDate: formatDate(ds.SeriesDate),
|
||||
numInstances: ds.numImageFrames,
|
||||
countIcon: ds.countIcon,
|
||||
StudyInstanceUID: ds.StudyInstanceUID,
|
||||
componentType,
|
||||
imageSrc,
|
||||
|
||||
@ -80,7 +80,7 @@ function TrackedCornerstoneViewport(props) {
|
||||
return;
|
||||
}
|
||||
|
||||
annotation.config.style.setViewportToolStyles(`viewport-${viewportIndex}`, {
|
||||
annotation.config.style.setViewportToolStyles(viewportId, {
|
||||
global: {
|
||||
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 { 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 = {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
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;
|
||||
|
||||
@ -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 = {
|
||||
id: '@ohif/extension-tmtv.hangingProtocolModule.ptCT',
|
||||
locked: true,
|
||||
@ -32,7 +248,6 @@ const ptCT = {
|
||||
ctDisplaySet: {
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
weight: 1,
|
||||
attribute: 'Modality',
|
||||
constraint: {
|
||||
equals: {
|
||||
@ -42,7 +257,6 @@ const ptCT = {
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
weight: 1,
|
||||
attribute: 'isReconstructable',
|
||||
constraint: {
|
||||
equals: {
|
||||
@ -75,7 +289,6 @@ const ptCT = {
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
weight: 1,
|
||||
attribute: 'isReconstructable',
|
||||
constraint: {
|
||||
equals: {
|
||||
@ -103,508 +316,14 @@ const ptCT = {
|
||||
},
|
||||
},
|
||||
|
||||
stages: [
|
||||
{
|
||||
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',
|
||||
},
|
||||
],
|
||||
stages: [stage1, stage2, stage3, stage4],
|
||||
numberOfPriorsReferenced: -1,
|
||||
};
|
||||
|
||||
function getHangingProtocolModule() {
|
||||
return [
|
||||
{
|
||||
id: ptCT.id,
|
||||
name: ptCT.id,
|
||||
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,
|
||||
} = servicesManager.services;
|
||||
|
||||
toolbarService.reset();
|
||||
toolGroupService.destroy();
|
||||
},
|
||||
validationTags: {
|
||||
|
||||
@ -72,6 +72,7 @@ function modeFactory() {
|
||||
measurementService,
|
||||
toolbarService,
|
||||
toolGroupService,
|
||||
customizationService,
|
||||
} = servicesManager.services;
|
||||
|
||||
measurementService.clearMeasurements();
|
||||
@ -79,6 +80,11 @@ function modeFactory() {
|
||||
// Init Default and SR ToolGroups
|
||||
initToolGroups(extensionManager, toolGroupService, commandsManager);
|
||||
|
||||
// init customizations
|
||||
customizationService.addModeCustomizations([
|
||||
'@ohif/extension-test.customizationModule.custom-context-menu',
|
||||
]);
|
||||
|
||||
let unsubscribe;
|
||||
|
||||
const activateTool = () => {
|
||||
@ -132,7 +138,6 @@ function modeFactory() {
|
||||
cornerstoneViewportService,
|
||||
} = servicesManager.services;
|
||||
|
||||
toolbarService.reset();
|
||||
toolGroupService.destroy();
|
||||
syncGroupService.destroy();
|
||||
segmentationService.destroy();
|
||||
@ -205,7 +210,12 @@ function modeFactory() {
|
||||
dicompdf.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',
|
||||
type: 'ohif.layoutSelector',
|
||||
type: 'ohif.splitButton',
|
||||
props: {
|
||||
rows: 3,
|
||||
columns: 3,
|
||||
groupId: 'LayoutTools',
|
||||
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',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'toggleMPR',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
commandName: 'toggleHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: 'mpr',
|
||||
},
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -159,7 +159,6 @@ function modeFactory() {
|
||||
_activatePanelTriggersSubscriptions.forEach(sub => sub.unsubscribe());
|
||||
_activatePanelTriggersSubscriptions = [];
|
||||
|
||||
toolbarService.reset();
|
||||
toolGroupService.destroy();
|
||||
syncGroupService.destroy();
|
||||
segmentationService.destroy();
|
||||
|
||||
@ -215,6 +215,32 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) {
|
||||
|
||||
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) {
|
||||
initDefaultToolGroup(
|
||||
@ -225,6 +251,7 @@ function initToolGroups(extensionManager, toolGroupService, commandsManager) {
|
||||
);
|
||||
initSRToolGroup(extensionManager, toolGroupService, commandsManager);
|
||||
initMPRToolGroup(extensionManager, toolGroupService, commandsManager);
|
||||
initVolume3DToolGroup(extensionManager, toolGroupService);
|
||||
}
|
||||
|
||||
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 _createToggleButton = _createButton.bind(null, 'toggle');
|
||||
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 = [
|
||||
// Measurement
|
||||
{
|
||||
@ -211,15 +219,7 @@ const toolbarButtons = [
|
||||
type: 'tool',
|
||||
icon: 'tool-zoom',
|
||||
label: 'Zoom',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Zoom',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
commands: _createSetToolActiveCommands('Zoom'),
|
||||
},
|
||||
},
|
||||
// Window Level + Presets...
|
||||
@ -268,15 +268,7 @@ const toolbarButtons = [
|
||||
type: 'tool',
|
||||
icon: 'tool-move',
|
||||
label: 'Pan',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'Pan',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
commands: _createSetToolActiveCommands('Pan'),
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -312,9 +304,11 @@ const toolbarButtons = [
|
||||
label: 'MPR',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'toggleMPR',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
commandName: 'toggleHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: 'mpr',
|
||||
},
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -330,8 +324,8 @@ const toolbarButtons = [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolGroupId: 'mpr',
|
||||
toolName: 'Crosshairs',
|
||||
toolGroupId: 'mpr',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
|
||||
@ -136,13 +136,11 @@ function modeFactory({ modeConfiguration }) {
|
||||
const {
|
||||
toolGroupService,
|
||||
syncGroupService,
|
||||
toolbarService,
|
||||
segmentationService,
|
||||
cornerstoneViewportService,
|
||||
} = servicesManager.services;
|
||||
|
||||
unsubscriptions.forEach(unsubscribe => unsubscribe());
|
||||
toolbarService.reset();
|
||||
toolGroupService.destroy();
|
||||
syncGroupService.destroy();
|
||||
segmentationService.destroy();
|
||||
|
||||
@ -203,9 +203,11 @@ const toolbarButtons = [
|
||||
label: 'MPR',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'toggleMPR',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
commandName: 'toggleHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: 'mpr',
|
||||
},
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -79,7 +79,7 @@
|
||||
"@types/jest": "^27.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.19.0",
|
||||
"@typescript-eslint/parser": "^4.19.0",
|
||||
"autoprefixer": "10.4.4",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"babel-eslint": "9.x",
|
||||
"babel-loader": "^8.2.4",
|
||||
"babel-plugin-inline-react-svg": "1.1.0",
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cornerstone-math": "0.1.9",
|
||||
"cornerstone-wado-image-loader": "^4.2.1",
|
||||
"cornerstone-wado-image-loader": "^4.13.0",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"@ohif/ui": "^2.0.0"
|
||||
},
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import log from '../log.js';
|
||||
import { Command, Commands } from '../types/Command';
|
||||
|
||||
/**
|
||||
* The definition of a command
|
||||
@ -105,8 +106,8 @@ export class CommandsManager {
|
||||
* @param {String} commandName - Command to find
|
||||
* @param {String} [contextName] - Specific command to look in. Defaults to current activeContexts
|
||||
*/
|
||||
getCommand = (commandName, contextName) => {
|
||||
let contexts = [];
|
||||
getCommand = (commandName: string, contextName?: string) => {
|
||||
const contexts = [];
|
||||
|
||||
if (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 {String} [contextName]
|
||||
*/
|
||||
runCommand(commandName, options = {}, contextName) {
|
||||
public runCommand(commandName: string, options = {}, contextName?: string) {
|
||||
const definition = this.getCommand(commandName, contextName);
|
||||
if (!definition) {
|
||||
log.warn(`Command "${commandName}" not found in current context`);
|
||||
@ -161,6 +162,50 @@ export class CommandsManager {
|
||||
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;
|
||||
|
||||
@ -64,19 +64,17 @@ export class HotkeysManager {
|
||||
*
|
||||
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
||||
*/
|
||||
setHotkeys(hotkeyDefinitions = [], key = 'hotkey-definitions') {
|
||||
setHotkeys(hotkeyDefinitions = [], name = 'hotkey-definitions') {
|
||||
try {
|
||||
const definitions = this.getValidDefinitions(hotkeyDefinitions);
|
||||
if (isequal(definitions, this.hotkeyDefaults)) {
|
||||
console.log('hotkeys REMOVING unused definition', key);
|
||||
localStorage.removeItem(key);
|
||||
localStorage.removeItem(name);
|
||||
} else {
|
||||
console.log('hotkeys setting local storage', key);
|
||||
localStorage.setItem(key, JSON.stringify(definitions));
|
||||
localStorage.setItem(name, JSON.stringify(definitions));
|
||||
}
|
||||
definitions.forEach(definition => this.registerHotkeys(definition));
|
||||
} catch (error) {
|
||||
const { uiNotificationService, } = this._servicesManager.services;
|
||||
const { uiNotificationService } = this._servicesManager.services;
|
||||
uiNotificationService.show({
|
||||
title: 'Hotkeys Manager',
|
||||
message: 'Error while setting hotkeys',
|
||||
|
||||
@ -412,6 +412,34 @@ class MetadataProvider {
|
||||
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) {
|
||||
// TODO: adding csiv here is not really correct. Probably need to use
|
||||
// metadataProvider.addImageIdToUIDs(imageId, {
|
||||
@ -445,7 +473,7 @@ class MetadataProvider {
|
||||
// check if the imageId starts with http:// or https:// using regex
|
||||
// Todo: handle non http imageIds
|
||||
let imageURI;
|
||||
const urlRegex = /^(http|https):\/\//;
|
||||
const urlRegex = /^(http|https|dicomfile):\/\//;
|
||||
if (urlRegex.test(imageId)) {
|
||||
imageURI = imageId;
|
||||
} else {
|
||||
@ -453,7 +481,7 @@ class MetadataProvider {
|
||||
}
|
||||
|
||||
const uids = this.imageURIToUIDs.get(imageURI);
|
||||
const frameNumber = imageId.split(/\/frames\//)[1];
|
||||
let frameNumber = this.getFrameInformationFromURL(imageId) || '1';
|
||||
|
||||
if (uids && frameNumber !== undefined) {
|
||||
return { ...uids, frameNumber };
|
||||
|
||||
@ -88,6 +88,20 @@ const bindings = [
|
||||
// keys: ['pagedown'],
|
||||
// isEditable: true,
|
||||
// },
|
||||
{
|
||||
commandName: 'nextStage',
|
||||
context: 'DEFAULT',
|
||||
label: 'Next Stage',
|
||||
keys: ['.'],
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
commandName: 'previousStage',
|
||||
context: 'DEFAULT',
|
||||
label: 'Previous Stage',
|
||||
keys: [','],
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
commandName: 'nextImage',
|
||||
label: 'Next Image',
|
||||
|
||||
@ -206,40 +206,40 @@ describe('ExtensionManager.ts', () => {
|
||||
const extension = {
|
||||
id: 'hello-world',
|
||||
getViewportModule: () => {
|
||||
return [{}];
|
||||
return [{ name: 'test' }];
|
||||
},
|
||||
getSopClassHandlerModule: () => {
|
||||
return [{}];
|
||||
return [{ name: 'test' }];
|
||||
},
|
||||
getPanelModule: () => {
|
||||
return [{}];
|
||||
return [{ name: 'test' }];
|
||||
},
|
||||
getToolbarModule: () => {
|
||||
return [{}];
|
||||
return [{ name: 'test' }];
|
||||
},
|
||||
getCommandsModule: () => {
|
||||
return [{}];
|
||||
return [{ name: 'test' }];
|
||||
},
|
||||
getLayoutTemplateModule: () => {
|
||||
return [{}];
|
||||
return [{ name: 'test' }];
|
||||
},
|
||||
getDataSourcesModule: () => {
|
||||
return [{}];
|
||||
return [{ name: 'test' }];
|
||||
},
|
||||
getHangingProtocolModule: () => {
|
||||
return [{}];
|
||||
return [{ name: 'test' }];
|
||||
},
|
||||
getContextModule: () => {
|
||||
return [{}];
|
||||
return [{ name: 'test' }];
|
||||
},
|
||||
getUtilityModule: () => {
|
||||
return [{}];
|
||||
return [{ name: 'test' }];
|
||||
},
|
||||
getCustomizationModule: () => {
|
||||
return [{}];
|
||||
return [{ name: 'test' }];
|
||||
},
|
||||
getStateSyncModule: () => {
|
||||
return [{}];
|
||||
return [{ name: 'test' }];
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -36,7 +36,10 @@ export interface ExtensionParams extends ExtensionConstructor {
|
||||
*/
|
||||
export interface Extension {
|
||||
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 = {
|
||||
@ -44,6 +47,12 @@ export type ExtensionRegister = {
|
||||
create: (p: ExtensionParams) => Extension;
|
||||
};
|
||||
|
||||
export type CommandsModule = {
|
||||
actions: Record<string, unknown>;
|
||||
definitions: Record<string, unknown>;
|
||||
defaultContext?: string;
|
||||
};
|
||||
|
||||
export default class ExtensionManager {
|
||||
private _commandsManager: CommandsManager;
|
||||
private _servicesManager: ServicesManager;
|
||||
@ -274,6 +283,11 @@ export default class ExtensionManager {
|
||||
// Default for most extension points,
|
||||
// Just adds each entry ready for consumption by mode.
|
||||
extensionModule.forEach(element => {
|
||||
if (!element.name) {
|
||||
throw new Error(
|
||||
`Extension ID ${extensionId} module ${moduleType} element has no name`
|
||||
);
|
||||
}
|
||||
const id = `${extensionId}.${moduleType}.${element.name}`;
|
||||
element.id = id;
|
||||
this.modulesMap[id] = element;
|
||||
@ -331,7 +345,7 @@ export default class ExtensionManager {
|
||||
}
|
||||
|
||||
try {
|
||||
const extensionModule = getModuleFn({
|
||||
const extensionModule = extension[getModuleFnName]({
|
||||
appConfig: this._appConfig,
|
||||
commandsManager: this._commandsManager,
|
||||
servicesManager: this._servicesManager,
|
||||
@ -348,6 +362,7 @@ export default class ExtensionManager {
|
||||
|
||||
return extensionModule;
|
||||
} catch (ex) {
|
||||
console.log(ex);
|
||||
throw new Error(
|
||||
`Exception thrown while trying to call ${getModuleFnName} for the ${extensionId} extension`
|
||||
);
|
||||
@ -356,10 +371,10 @@ export default class ExtensionManager {
|
||||
|
||||
_initHangingProtocolsModule = (extensionModule, extensionId) => {
|
||||
const { hangingProtocolService } = this._servicesManager.services;
|
||||
extensionModule.forEach(({ id, protocol }) => {
|
||||
extensionModule.forEach(({ name, protocol }) => {
|
||||
if (protocol) {
|
||||
// Only auto-register if protocol specified, otherwise let mode register
|
||||
hangingProtocolService.addProtocol(id, protocol);
|
||||
hangingProtocolService.addProtocol(name, protocol);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export default {
|
||||
COMMANDS: 'commandsModule',
|
||||
CUSTOMIZATION: 'customizationModule',
|
||||
STATE_SYNC: 'stateSyncModule',
|
||||
DATA_SOURCE: 'dataSourcesModule',
|
||||
PANEL: 'panelModule',
|
||||
SOP_CLASS_HANDLER: 'sopClassHandlerModule',
|
||||
|
||||
@ -25,6 +25,7 @@ describe('Top level exports', () => {
|
||||
//
|
||||
'CineService',
|
||||
'CustomizationService',
|
||||
'StateSyncService',
|
||||
'UIDialogService',
|
||||
'UIModalService',
|
||||
'UINotificationService',
|
||||
|
||||
@ -29,6 +29,7 @@ import {
|
||||
PubSubService,
|
||||
UserAuthenticationService,
|
||||
CustomizationService,
|
||||
StateSyncService,
|
||||
PanelService,
|
||||
} from './services';
|
||||
|
||||
@ -61,6 +62,7 @@ const OHIF = {
|
||||
//
|
||||
CineService,
|
||||
CustomizationService,
|
||||
StateSyncService,
|
||||
UIDialogService,
|
||||
UIModalService,
|
||||
UINotificationService,
|
||||
@ -99,6 +101,7 @@ export {
|
||||
//
|
||||
CineService,
|
||||
CustomizationService,
|
||||
StateSyncService,
|
||||
UIDialogService,
|
||||
UIModalService,
|
||||
UINotificationService,
|
||||
|
||||
@ -63,7 +63,7 @@ export default class CustomizationService extends PubSubService {
|
||||
|
||||
modeCustomizations: Record<string, Customization> = {};
|
||||
globalCustomizations: Record<string, Customization> = {};
|
||||
configuration: UICustomizationConfiguration;
|
||||
configuration: CustomizationConfiguration;
|
||||
|
||||
constructor({ configuration, commandsManager }) {
|
||||
super(EVENTS);
|
||||
@ -97,36 +97,6 @@ export default class CustomizationService extends PubSubService {
|
||||
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> {
|
||||
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
|
||||
* when running in a given mode. Reset clears mode customizations.
|
||||
* Note that global customizations over-ride mode customizations.
|
||||
@ -158,7 +145,7 @@ export default class CustomizationService extends PubSubService {
|
||||
this.globalCustomizations[customizationId] ??
|
||||
this.modeCustomizations[customizationId] ??
|
||||
defaultValue;
|
||||
return this.applyType(customization);
|
||||
return this.transform(customization);
|
||||
}
|
||||
|
||||
public hasModeCustomization(customizationId: string) {
|
||||
@ -167,16 +154,33 @@ export default class CustomizationService extends PubSubService {
|
||||
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;
|
||||
const { customizationType } = customization;
|
||||
if (!customizationType) return customization;
|
||||
const parent = this.getModeCustomization(customizationType);
|
||||
return parent
|
||||
const parent = this.getCustomization(customizationType);
|
||||
const result = parent
|
||||
? Object.assign(Object.create(parent), customization)
|
||||
: customization;
|
||||
// Execute an nested type information
|
||||
return result.transform?.(this) || result;
|
||||
}
|
||||
|
||||
public addModeCustomizations(modeCustomizations): void {
|
||||
@ -203,7 +207,7 @@ export default class CustomizationService extends PubSubService {
|
||||
id: string,
|
||||
defaultValue?: Customization
|
||||
): Customization | void {
|
||||
return this.applyType(this.globalCustomizations[id] ?? defaultValue);
|
||||
return this.transform(this.globalCustomizations[id] ?? defaultValue);
|
||||
}
|
||||
|
||||
setGlobalCustomization(id: string, value: Customization): void {
|
||||
@ -243,7 +247,7 @@ export default class CustomizationService extends PubSubService {
|
||||
const extensionValue = this.findExtensionValue(value);
|
||||
// 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
|
||||
this.addReference(extensionValue);
|
||||
this.addReference(extensionValue.value, isGlobal, extensionValue.name);
|
||||
} else if (Array.isArray(value)) {
|
||||
this.addReferences(value, isGlobal);
|
||||
} else {
|
||||
|
||||
@ -84,9 +84,9 @@ function _getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) {
|
||||
}
|
||||
|
||||
function _getInstanceByImageId(imageId) {
|
||||
for (let study of _model.studies) {
|
||||
for (let series of study.series) {
|
||||
for (let instance of series.instances) {
|
||||
for (const study of _model.studies) {
|
||||
for (const series of study.series) {
|
||||
for (const instance of series.instances) {
|
||||
if (instance.imageId === imageId) {
|
||||
return instance;
|
||||
}
|
||||
@ -236,7 +236,7 @@ const BaseImplementation = {
|
||||
addStudy(study) {
|
||||
const { StudyInstanceUID } = study;
|
||||
|
||||
let existingStudy = _model.studies.find(
|
||||
const existingStudy = _model.studies.find(
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
|
||||
@ -29,27 +29,31 @@ const match = (
|
||||
let requiredFailed = false;
|
||||
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 => {
|
||||
const { attribute } = rule;
|
||||
const { attribute, from = 'metadataInstance' } = rule;
|
||||
// Do not use the custom attribute from the metadataInstance since it is subject to change
|
||||
if (customAttributeRetrievalCallbacks.hasOwnProperty(attribute)) {
|
||||
readValues[attribute] = customAttributeRetrievalCallbacks[
|
||||
attribute
|
||||
].callback(metadataInstance, options);
|
||||
].callback.call(rule, metadataInstance, options);
|
||||
} else {
|
||||
readValues[attribute] =
|
||||
metadataInstance[attribute] ??
|
||||
((metadataInstance.images || metadataInstance.others || [])[0] || {})[
|
||||
attribute
|
||||
];
|
||||
fromSrc[from]?.[attribute] ?? instance?.[attribute];
|
||||
}
|
||||
|
||||
console.log(
|
||||
'Test',
|
||||
attribute,
|
||||
readValues[attribute],
|
||||
JSON.stringify(rule.constraint)
|
||||
);
|
||||
// Format the constraint as required by Validate.js
|
||||
const testConstraint = {
|
||||
[attribute]: rule.constraint,
|
||||
@ -70,6 +74,14 @@ const match = (
|
||||
errorMessages = ['Something went wrong during validation.', e];
|
||||
}
|
||||
|
||||
console.log(
|
||||
'Test',
|
||||
`${from}.${attribute}`,
|
||||
readValues[attribute],
|
||||
JSON.stringify(rule.constraint),
|
||||
!errorMessages
|
||||
);
|
||||
|
||||
if (!errorMessages) {
|
||||
// If no errorMessages were returned, then validation passed.
|
||||
|
||||
|
||||
@ -117,8 +117,7 @@ const studyMatchDisplaySets = [displaySet3, displaySet2, displaySet1];
|
||||
|
||||
function checkHpsBestMatch(hps) {
|
||||
hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets });
|
||||
const { hpAlreadyApplied, viewportMatchDetails } = hps.getMatchDetails();
|
||||
expect(hpAlreadyApplied).toMatchObject(new Map([[0, false]]));
|
||||
const { viewportMatchDetails } = hps.getMatchDetails();
|
||||
expect(viewportMatchDetails.size).toBe(1);
|
||||
expect(viewportMatchDetails.get(0)).toMatchObject({
|
||||
viewportOptions: {
|
||||
@ -131,9 +130,11 @@ function checkHpsBestMatch(hps) {
|
||||
// ds2 fails to match required and ds3 fails to match an optional.
|
||||
displaySetsInfo: [
|
||||
{
|
||||
SeriesInstanceUID: 'ds1',
|
||||
displaySetInstanceUID: 'displaySet1',
|
||||
displaySetOptions: {},
|
||||
displaySetOptions: {
|
||||
id: 'displaySetSelector',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -191,14 +192,6 @@ describe('HangingProtocolService', () => {
|
||||
it('matches best image match', () => {
|
||||
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) {
|
||||
return HPMatcher.match(
|
||||
metaData,
|
||||
@ -109,7 +119,7 @@ export default class ProtocolEngine {
|
||||
let rules = protocol.protocolMatchingRules.slice();
|
||||
if (!rules || !rules.length) {
|
||||
console.warn(
|
||||
'ProtocolEngine::findMatchByStudy no matching rules - specify protocolMatchingRules',
|
||||
'ProtocolEngine::findMatchByStudy no matching rules - specify protocolMatchingRules for',
|
||||
protocol.id
|
||||
);
|
||||
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) {
|
||||
const testValue = options?.value ?? options;
|
||||
if (Array.isArray(value)) {
|
||||
|
||||
@ -3,9 +3,10 @@ import validate from './validator.js';
|
||||
describe('validator', () => {
|
||||
const attributeMap = {
|
||||
str: 'string',
|
||||
upper: 'UPPER',
|
||||
num: 3,
|
||||
nullValue: null,
|
||||
list: ['abc', 'def'],
|
||||
list: ['abc', 'def', 'GHI'],
|
||||
};
|
||||
|
||||
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', () => {
|
||||
it('returned undefined on equals', () => {
|
||||
expect(
|
||||
|
||||
@ -459,7 +459,12 @@ class MeasurementService extends PubSubService {
|
||||
log.warn(`Measurement ID not found. Generating UID: ${internalUID}`);
|
||||
}
|
||||
|
||||
const annotationData = data.annotation.data;
|
||||
|
||||
const newMeasurement = {
|
||||
finding: annotationData.finding,
|
||||
findingSites: annotationData.findingSites,
|
||||
site: annotationData.findingSites?.[0],
|
||||
...measurement,
|
||||
modifiedTimestamp: Math.floor(Date.now() / 1000),
|
||||
uid: internalUID,
|
||||
@ -472,7 +477,7 @@ class MeasurementService extends PubSubService {
|
||||
measurement: newMeasurement,
|
||||
});
|
||||
} else {
|
||||
log.info(`Measurement added.`, newMeasurement);
|
||||
log.info('Measurement added', newMeasurement);
|
||||
this.measurements[internalUID] = newMeasurement;
|
||||
this._broadcastEvent(this.EVENTS.RAW_MEASUREMENT_ADDED, {
|
||||
source,
|
||||
@ -519,9 +524,14 @@ class MeasurementService extends PubSubService {
|
||||
let measurement = {};
|
||||
try {
|
||||
const sourceMappings = this.mappings[source.uid];
|
||||
const { toMeasurementSchema } = sourceMappings.find(
|
||||
const sourceMapping = sourceMappings.find(
|
||||
mapping => mapping.annotationType === annotationType
|
||||
);
|
||||
if (!sourceMapping) {
|
||||
console.log('No source mapping', source);
|
||||
return;
|
||||
}
|
||||
const { toMeasurementSchema } = sourceMapping;
|
||||
|
||||
/* Convert measurement */
|
||||
measurement = toMeasurementSchema(sourceAnnotationDetail);
|
||||
@ -548,26 +558,27 @@ class MeasurementService extends PubSubService {
|
||||
);
|
||||
}
|
||||
|
||||
const oldMeasurement = this.measurements[internalUID];
|
||||
|
||||
const newMeasurement = {
|
||||
...oldMeasurement,
|
||||
...measurement,
|
||||
modifiedTimestamp: Math.floor(Date.now() / 1000),
|
||||
uid: internalUID,
|
||||
};
|
||||
|
||||
if (this.measurements[internalUID]) {
|
||||
if (oldMeasurement) {
|
||||
// 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.
|
||||
newMeasurement.selected = this.measurements[internalUID].selected;
|
||||
this.measurements[internalUID] = newMeasurement;
|
||||
if (isUpdate) {
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||
source,
|
||||
measurement: newMeasurement,
|
||||
notYetUpdatedAtSource: false,
|
||||
});
|
||||
} else {
|
||||
log.info('Measurement added.', newMeasurement);
|
||||
this.measurements[internalUID] = newMeasurement;
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_ADDED, {
|
||||
source,
|
||||
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