feat(customizableOverlay): Add customizable overlay info (#3061)
* overlay customization * custom overlay definition and examples * minor fix - import statement difference between cornerstone and cornerstone3D * move "VOI" and "Zoom" calulation to the ViewportOverlay component, make OverlayItem fairly dull * type specifications * follow up fixes for CustomizationService changes on upstream * [fix] various fixes related to custom overlays (formatting, toggling) * [fix] various fixes related to custom overlays (formatting, toggling) * right side overlay panels - align right * [fix] remove "notification" from toggleOverlay command * comments * [refactor] remove unused imports * documentation of customizable overlay configuration * prettify * review of documentation of customization service * fix useCallback() dependencies * fix useCallback dependencies * documentation for customization service * documentation for customization service
This commit is contained in:
parent
b9dbc5b3a5
commit
e4e62e9e14
@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import ViewportImageScrollbar from './ViewportImageScrollbar';
|
import ViewportImageScrollbar from './ViewportImageScrollbar';
|
||||||
import ViewportOverlay from './ViewportOverlay';
|
import CustomizableViewportOverlay from './CustomizableViewportOverlay';
|
||||||
import ViewportOrientationMarkers from './ViewportOrientationMarkers';
|
import ViewportOrientationMarkers from './ViewportOrientationMarkers';
|
||||||
import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator';
|
import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator';
|
||||||
|
|
||||||
@ -36,9 +36,8 @@ function CornerstoneOverlays(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (viewportData) {
|
if (viewportData) {
|
||||||
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
|
const viewportInfo =
|
||||||
viewportIndex
|
cornerstoneViewportService.getViewportInfoByIndex(viewportIndex);
|
||||||
);
|
|
||||||
|
|
||||||
if (viewportInfo?.viewportOptions?.customViewportProps?.hideOverlays) {
|
if (viewportInfo?.viewportOptions?.customViewportProps?.hideOverlays) {
|
||||||
return null;
|
return null;
|
||||||
@ -56,17 +55,20 @@ function CornerstoneOverlays(props) {
|
|||||||
scrollbarHeight={scrollbarHeight}
|
scrollbarHeight={scrollbarHeight}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
/>
|
/>
|
||||||
<ViewportOverlay
|
|
||||||
|
<CustomizableViewportOverlay
|
||||||
imageSliceData={imageSliceData}
|
imageSliceData={imageSliceData}
|
||||||
viewportData={viewportData}
|
viewportData={viewportData}
|
||||||
viewportIndex={viewportIndex}
|
viewportIndex={viewportIndex}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
element={element}
|
element={element}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ViewportImageSliceLoadingIndicator
|
<ViewportImageSliceLoadingIndicator
|
||||||
viewportData={viewportData}
|
viewportData={viewportData}
|
||||||
element={element}
|
element={element}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ViewportOrientationMarkers
|
<ViewportOrientationMarkers
|
||||||
imageSliceData={imageSliceData}
|
imageSliceData={imageSliceData}
|
||||||
element={element}
|
element={element}
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
custom overlay panels: top-left, top-right, bottom-left and bottom-right
|
||||||
|
If any text to be displayed on the overlay is too long to hold on a single
|
||||||
|
line, it will be truncated with ellipsis in the end.
|
||||||
|
*/
|
||||||
|
.viewport-overlay {
|
||||||
|
max-width: 40%;
|
||||||
|
}
|
||||||
|
.viewport-overlay span {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewport-overlay.left-viewport {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewport-overlay.right-viewport-scrollbar {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.viewport-overlay.right-viewport-scrollbar .flex.flex-row {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
@ -0,0 +1,489 @@
|
|||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { vec3 } from 'gl-matrix';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { metaData, Enums, utilities } from '@cornerstonejs/core';
|
||||||
|
import { ViewportOverlay } from '@ohif/ui';
|
||||||
|
import {
|
||||||
|
formatPN,
|
||||||
|
formatDICOMDate,
|
||||||
|
formatDICOMTime,
|
||||||
|
formatNumberPrecision,
|
||||||
|
} from './utils';
|
||||||
|
import { InstanceMetadata } from 'platform/core/src/types';
|
||||||
|
import { ServicesManager } from '@ohif/core';
|
||||||
|
import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
|
||||||
|
|
||||||
|
import './CustomizableViewportOverlay.css';
|
||||||
|
|
||||||
|
const EPSILON = 1e-4;
|
||||||
|
|
||||||
|
interface OverlayItemProps {
|
||||||
|
element: any;
|
||||||
|
viewportData: any;
|
||||||
|
imageSliceData: ImageSliceData;
|
||||||
|
viewportIndex: number | null;
|
||||||
|
servicesManager: ServicesManager;
|
||||||
|
instance: InstanceMetadata;
|
||||||
|
customization: any;
|
||||||
|
formatters: {
|
||||||
|
formatPN: (val) => string;
|
||||||
|
formatDate: (val) => string;
|
||||||
|
formatTime: (val) => string;
|
||||||
|
formatNumberPrecision: (val, number) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// calculated values
|
||||||
|
voi: {
|
||||||
|
windowWidth: number;
|
||||||
|
windowCenter: number;
|
||||||
|
};
|
||||||
|
instanceNumber?: number;
|
||||||
|
scale?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Window Level / Center Overlay item
|
||||||
|
*/
|
||||||
|
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
|
||||||
|
const { windowWidth, windowCenter } = voi;
|
||||||
|
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="overlay-item flex flex-row"
|
||||||
|
style={{ color: (customization && customization.color) || undefined }}
|
||||||
|
>
|
||||||
|
<span className="mr-1 shrink-0">W:</span>
|
||||||
|
<span className="ml-1 mr-2 font-light shrink-0">
|
||||||
|
{windowWidth.toFixed(0)}
|
||||||
|
</span>
|
||||||
|
<span className="mr-1 shrink-0">L:</span>
|
||||||
|
<span className="ml-1 font-light shrink-0">
|
||||||
|
{windowCenter.toFixed(0)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zoom Level Overlay item
|
||||||
|
*/
|
||||||
|
function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="overlay-item flex flex-row"
|
||||||
|
style={{ color: (customization && customization.color) || undefined }}
|
||||||
|
>
|
||||||
|
<span className="mr-1 shrink-0">Zoom:</span>
|
||||||
|
<span className="font-light">{scale.toFixed(2)}x</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instance Number Overlay Item
|
||||||
|
*/
|
||||||
|
function InstanceNumberOverlayItem({
|
||||||
|
instanceNumber,
|
||||||
|
imageSliceData,
|
||||||
|
customization,
|
||||||
|
}: OverlayItemProps) {
|
||||||
|
const { imageIndex, numberOfSlices } = imageSliceData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="overlay-item flex flex-row"
|
||||||
|
style={{ color: (customization && customization.color) || undefined }}
|
||||||
|
>
|
||||||
|
<span className="mr-1 shrink-0">I:</span>
|
||||||
|
<span className="font-light">
|
||||||
|
{instanceNumber !== undefined && instanceNumber !== null
|
||||||
|
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
|
||||||
|
: `${imageIndex + 1}/${numberOfSlices}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customizable Viewport Overlay
|
||||||
|
*/
|
||||||
|
function CustomizableViewportOverlay({
|
||||||
|
element,
|
||||||
|
viewportData,
|
||||||
|
imageSliceData,
|
||||||
|
viewportIndex,
|
||||||
|
servicesManager,
|
||||||
|
}) {
|
||||||
|
const { toolbarService, cornerstoneViewportService, customizationService } =
|
||||||
|
servicesManager.services;
|
||||||
|
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
|
||||||
|
const [scale, setScale] = useState(1);
|
||||||
|
const [activeTools, setActiveTools] = useState([]);
|
||||||
|
const { imageIndex } = imageSliceData;
|
||||||
|
|
||||||
|
const topLeftCustomization = customizationService.getModeCustomization(
|
||||||
|
'cornerstoneOverlayTopLeft'
|
||||||
|
);
|
||||||
|
const topRightCustomization = customizationService.getModeCustomization(
|
||||||
|
'cornerstoneOverlayTopRight'
|
||||||
|
);
|
||||||
|
const bottomLeftCustomization = customizationService.getModeCustomization(
|
||||||
|
'cornerstoneOverlayBottomLeft'
|
||||||
|
);
|
||||||
|
const bottomRightCustomization = customizationService.getModeCustomization(
|
||||||
|
'cornerstoneOverlayBottomRight'
|
||||||
|
);
|
||||||
|
|
||||||
|
const instance = useMemo(() => {
|
||||||
|
if (viewportData != null) {
|
||||||
|
return _getViewportInstance(viewportData, imageIndex);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [viewportData, imageIndex]);
|
||||||
|
|
||||||
|
const instanceNumber = useMemo(() => {
|
||||||
|
if (viewportData != null) {
|
||||||
|
return _getInstanceNumber(
|
||||||
|
viewportData,
|
||||||
|
viewportIndex,
|
||||||
|
imageIndex,
|
||||||
|
cornerstoneViewportService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [viewportData, viewportIndex, imageIndex, cornerstoneViewportService]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial toolbar state
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveTools(toolbarService.getActiveTools());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updating the VOI when the viewport changes its voi
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const updateVOI = eventDetail => {
|
||||||
|
const { range } = eventDetail.detail;
|
||||||
|
|
||||||
|
if (!range) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { lower, upper } = range;
|
||||||
|
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(
|
||||||
|
lower,
|
||||||
|
upper
|
||||||
|
);
|
||||||
|
|
||||||
|
setVOI({ windowCenter, windowWidth });
|
||||||
|
};
|
||||||
|
|
||||||
|
element.addEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
|
||||||
|
};
|
||||||
|
}, [viewportIndex, viewportData, voi, element]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updating the scale when the viewport changes its zoom
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const updateScale = eventDetail => {
|
||||||
|
const { previousCamera, camera } = eventDetail.detail;
|
||||||
|
|
||||||
|
if (
|
||||||
|
previousCamera.parallelScale !== camera.parallelScale ||
|
||||||
|
previousCamera.scale !== camera.scale
|
||||||
|
) {
|
||||||
|
const viewport =
|
||||||
|
cornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||||
|
viewportIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!viewport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageData = viewport.getImageData();
|
||||||
|
|
||||||
|
if (!imageData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (camera.scale) {
|
||||||
|
setScale(camera.scale);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { spacing } = imageData;
|
||||||
|
// convert parallel scale to scale
|
||||||
|
const scale =
|
||||||
|
(element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
|
||||||
|
setScale(scale);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
element.addEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
|
||||||
|
};
|
||||||
|
}, [viewportIndex, viewportData, cornerstoneViewportService, element]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updating the active tools when the toolbar changes
|
||||||
|
*/
|
||||||
|
// Todo: this should act on the toolGroups instead of the toolbar state
|
||||||
|
useEffect(() => {
|
||||||
|
const { unsubscribe } = toolbarService.subscribe(
|
||||||
|
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||||
|
() => {
|
||||||
|
setActiveTools(toolbarService.getActiveTools());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [toolbarService]);
|
||||||
|
|
||||||
|
const _renderOverlayItem = useCallback(
|
||||||
|
item => {
|
||||||
|
const overlayItemProps: OverlayItemProps = {
|
||||||
|
element,
|
||||||
|
viewportData,
|
||||||
|
imageSliceData,
|
||||||
|
viewportIndex,
|
||||||
|
servicesManager,
|
||||||
|
customization: item,
|
||||||
|
formatters: {
|
||||||
|
formatPN: formatPN,
|
||||||
|
formatDate: formatDICOMDate,
|
||||||
|
formatTime: formatDICOMTime,
|
||||||
|
formatNumberPrecision: formatNumberPrecision,
|
||||||
|
},
|
||||||
|
instance,
|
||||||
|
// calculated
|
||||||
|
voi,
|
||||||
|
scale,
|
||||||
|
instanceNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (item.customizationType === 'ohif.overlayItem.windowLevel') {
|
||||||
|
return <VOIOverlayItem {...overlayItemProps} />;
|
||||||
|
} else if (item.customizationType === 'ohif.overlayItem.zoomLevel') {
|
||||||
|
return <ZoomOverlayItem {...overlayItemProps} />;
|
||||||
|
} else if (item.customizationType === 'ohif.overlayItem.instanceNumber') {
|
||||||
|
return <InstanceNumberOverlayItem {...overlayItemProps} />;
|
||||||
|
} else {
|
||||||
|
const renderItem = customizationService.applyType(item);
|
||||||
|
|
||||||
|
if (typeof renderItem.content === 'function') {
|
||||||
|
return renderItem.content(overlayItemProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
element,
|
||||||
|
viewportData,
|
||||||
|
imageSliceData,
|
||||||
|
viewportIndex,
|
||||||
|
servicesManager,
|
||||||
|
customizationService,
|
||||||
|
instance,
|
||||||
|
voi,
|
||||||
|
scale,
|
||||||
|
instanceNumber,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getTopLeftContent = useCallback(() => {
|
||||||
|
const items = topLeftCustomization?.items || [
|
||||||
|
{
|
||||||
|
id: 'WindowLevel',
|
||||||
|
customizationType: 'ohif.overlayItem.windowLevel',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<div key={`topLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}, [topLeftCustomization, _renderOverlayItem]);
|
||||||
|
|
||||||
|
const getTopRightContent = useCallback(() => {
|
||||||
|
const items = topRightCustomization?.items || [
|
||||||
|
{
|
||||||
|
id: 'InstanceNmber',
|
||||||
|
customizationType: 'ohif.overlayItem.instanceNumber',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<div key={`topRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}, [topRightCustomization, _renderOverlayItem]);
|
||||||
|
|
||||||
|
const getBottomLeftContent = useCallback(() => {
|
||||||
|
const items = bottomLeftCustomization?.items || [];
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<div key={`bottomLeftOverlayItem_${i}`}>
|
||||||
|
{_renderOverlayItem(item)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}, [bottomLeftCustomization, _renderOverlayItem]);
|
||||||
|
|
||||||
|
const getBottomRightContent = useCallback(() => {
|
||||||
|
const items = bottomRightCustomization?.items || [];
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<div key={`bottomRightOverlayItem_${i}`}>
|
||||||
|
{_renderOverlayItem(item)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}, [bottomRightCustomization, _renderOverlayItem]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ViewportOverlay
|
||||||
|
topLeft={getTopLeftContent()}
|
||||||
|
topRight={getTopRightContent()}
|
||||||
|
bottomLeft={getBottomLeftContent()}
|
||||||
|
bottomRight={getBottomRightContent()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getViewportInstance(viewportData, imageIndex) {
|
||||||
|
let imageId = null;
|
||||||
|
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||||
|
imageId = viewportData.data.imageIds[imageIndex];
|
||||||
|
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||||
|
const volumes = viewportData.volumes;
|
||||||
|
if (volumes && volumes.length == 1) {
|
||||||
|
const volume = volumes[0];
|
||||||
|
imageId = volume.imageIds[imageIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return imageId ? metaData.get('instance', imageId) || {} : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getInstanceNumber(
|
||||||
|
viewportData,
|
||||||
|
viewportIndex,
|
||||||
|
imageIndex,
|
||||||
|
cornerstoneViewportService
|
||||||
|
) {
|
||||||
|
let instanceNumber;
|
||||||
|
|
||||||
|
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||||
|
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
|
||||||
|
|
||||||
|
if (!instanceNumber && instanceNumber !== 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||||
|
instanceNumber = _getInstanceNumberFromVolume(
|
||||||
|
viewportData,
|
||||||
|
imageIndex,
|
||||||
|
viewportIndex,
|
||||||
|
cornerstoneViewportService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return instanceNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getInstanceNumberFromStack(viewportData, imageIndex) {
|
||||||
|
const imageIds = viewportData.data.imageIds;
|
||||||
|
const imageId = imageIds[imageIndex];
|
||||||
|
|
||||||
|
if (!imageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const generalImageModule = metaData.get('generalImageModule', imageId) || {};
|
||||||
|
const { instanceNumber } = generalImageModule;
|
||||||
|
|
||||||
|
const stackSize = imageIds.length;
|
||||||
|
|
||||||
|
if (stackSize <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseInt(instanceNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since volume viewports can be in any view direction, they can render
|
||||||
|
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
|
||||||
|
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
|
||||||
|
function _getInstanceNumberFromVolume(
|
||||||
|
viewportData,
|
||||||
|
imageIndex,
|
||||||
|
viewportIndex,
|
||||||
|
cornerstoneViewportService
|
||||||
|
) {
|
||||||
|
const volumes = viewportData.volumes;
|
||||||
|
|
||||||
|
// Todo: support fusion of acquisition plane which has instanceNumber
|
||||||
|
if (!volumes || volumes.length > 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const volume = volumes[0];
|
||||||
|
const { direction, imageIds } = volume;
|
||||||
|
|
||||||
|
const cornerstoneViewport =
|
||||||
|
cornerstoneViewportService.getCornerstoneViewportByIndex(viewportIndex);
|
||||||
|
|
||||||
|
if (!cornerstoneViewport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const camera = cornerstoneViewport.getCamera();
|
||||||
|
const { viewPlaneNormal } = camera;
|
||||||
|
// checking if camera is looking at the acquisition plane (defined by the direction on the volume)
|
||||||
|
|
||||||
|
const scanAxisNormal = direction.slice(6, 9);
|
||||||
|
|
||||||
|
// check if viewPlaneNormal is parallel to scanAxisNormal
|
||||||
|
const cross = vec3.cross(vec3.create(), viewPlaneNormal, scanAxisNormal);
|
||||||
|
const isAcquisitionPlane = vec3.length(cross) < EPSILON;
|
||||||
|
|
||||||
|
if (isAcquisitionPlane) {
|
||||||
|
const imageId = imageIds[imageIndex];
|
||||||
|
|
||||||
|
if (!imageId) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { instanceNumber } =
|
||||||
|
metaData.get('generalImageModule', imageId) || {};
|
||||||
|
return parseInt(instanceNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomizableViewportOverlay.propTypes = {
|
||||||
|
viewportData: PropTypes.object,
|
||||||
|
imageIndex: PropTypes.number,
|
||||||
|
viewportIndex: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomizableViewportOverlay;
|
||||||
98
extensions/cornerstone/src/Viewport/Overlays/utils.ts
Normal file
98
extensions/cornerstone/src/Viewport/Overlays/utils.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import { metaData } from '@cornerstonejs/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if value is valid.
|
||||||
|
*
|
||||||
|
* @param {number} value
|
||||||
|
* @returns {boolean} is valid.
|
||||||
|
*/
|
||||||
|
export function isValidNumber(value) {
|
||||||
|
return typeof value === 'number' && !isNaN(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats number precision.
|
||||||
|
*
|
||||||
|
* @param {number} number
|
||||||
|
* @param {number} precision
|
||||||
|
* @returns {number} formatted number.
|
||||||
|
*/
|
||||||
|
export function formatNumberPrecision(number, precision = 0) {
|
||||||
|
if (number !== null) {
|
||||||
|
return parseFloat(number).toFixed(precision);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats DICOM date.
|
||||||
|
*
|
||||||
|
* @param {string} date
|
||||||
|
* @param {string} strFormat
|
||||||
|
* @returns {string} formatted date.
|
||||||
|
*/
|
||||||
|
export function formatDICOMDate(date, strFormat = 'MMM D, YYYY') {
|
||||||
|
return moment(date, 'YYYYMMDD').format(strFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DICOM Time is stored as HHmmss.SSS, where:
|
||||||
|
* HH 24 hour time:
|
||||||
|
* m mm 0..59 Minutes
|
||||||
|
* s ss 0..59 Seconds
|
||||||
|
* S SS SSS 0..999 Fractional seconds
|
||||||
|
*
|
||||||
|
* Goal: '24:12:12'
|
||||||
|
*
|
||||||
|
* @param {*} time
|
||||||
|
* @param {string} strFormat
|
||||||
|
* @returns {string} formatted name.
|
||||||
|
*/
|
||||||
|
export function formatDICOMTime(time, strFormat = 'HH:mm:ss') {
|
||||||
|
return moment(time, 'HH:mm:ss').format(strFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a patient name for display purposes
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @returns {string} formatted name.
|
||||||
|
*/
|
||||||
|
export function formatPN(name) {
|
||||||
|
if (!name) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleaned = name
|
||||||
|
.split('^')
|
||||||
|
.filter(s => !!s)
|
||||||
|
.join(', ')
|
||||||
|
.trim();
|
||||||
|
return cleaned === ',' || cleaned === '' ? '' : cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets compression type
|
||||||
|
*
|
||||||
|
* @param {number} imageId
|
||||||
|
* @returns {string} comrpession type.
|
||||||
|
*/
|
||||||
|
export function getCompression(imageId) {
|
||||||
|
const generalImageModule = metaData.get('generalImageModule', imageId) || {};
|
||||||
|
const {
|
||||||
|
lossyImageCompression,
|
||||||
|
lossyImageCompressionRatio,
|
||||||
|
lossyImageCompressionMethod,
|
||||||
|
} = generalImageModule;
|
||||||
|
|
||||||
|
if (lossyImageCompression === '01' && lossyImageCompressionRatio !== '') {
|
||||||
|
const compressionMethod = lossyImageCompressionMethod || 'Lossy: ';
|
||||||
|
const compressionRatio = formatNumberPrecision(
|
||||||
|
lossyImageCompressionRatio,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
return compressionMethod + compressionRatio + ' : 1';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Lossless / Uncompressed';
|
||||||
|
}
|
||||||
@ -47,6 +47,18 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
|||||||
title: 'DICOM Tag Browser',
|
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 = {
|
const definitions = {
|
||||||
|
|||||||
@ -40,5 +40,76 @@ export default function getCustomizationModule() {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
value: [
|
||||||
|
/**
|
||||||
|
* Customization Component Type definition for overlay items.
|
||||||
|
* Overlay items are texts (or other components) that will be displayed
|
||||||
|
* on a Viewport Overlay, which contains the information panels on the
|
||||||
|
* four corners of a viewport.
|
||||||
|
*
|
||||||
|
* @definition of a overlay item using this type
|
||||||
|
* The value to be displayed is defined by
|
||||||
|
* - setting DICOM image instance's property to this field,
|
||||||
|
* - or defining contentF()
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* id: string - unique id for the overlay item
|
||||||
|
* customizationType: string - indicates customization type definition to this
|
||||||
|
* label: string - Label, to be displayed for the item
|
||||||
|
* title: string - Tooltip, for the item
|
||||||
|
* color: string - Color of the text
|
||||||
|
* condition: ({ instance }) => boolean - decides whether to display the overlay item or not
|
||||||
|
* attribute: string - property name of the DICOM image instance
|
||||||
|
* contentF: ({ instance, formatters }) => string | component,
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* {
|
||||||
|
* id: 'PatientNameOverlay',
|
||||||
|
* customizationType: 'ohif.overlayItem',
|
||||||
|
* label: 'PN:',
|
||||||
|
* title: 'Patient Name',
|
||||||
|
* color: 'yellow',
|
||||||
|
* condition: ({ instance }) => instance && instance.PatientName && instance.PatientName.Alphabetic,
|
||||||
|
* attribute: 'PatientName',
|
||||||
|
* contentF: ({ instance, formatters: { formatPN } }) => `${formatPN(instance.PatientName.Alphabetic)} ${(instance.PatientSex ? '(' + instance.PatientSex + ')' : '')}`,
|
||||||
|
* },
|
||||||
|
*
|
||||||
|
* @see CustomizableViewportOverlay
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
id: 'ohif.overlayItem',
|
||||||
|
uiType: 'uiType',
|
||||||
|
content: function (props) {
|
||||||
|
if (this.condition && !this.condition(props)) return null;
|
||||||
|
|
||||||
|
const { instance } = props;
|
||||||
|
const value =
|
||||||
|
instance && this.attribute
|
||||||
|
? instance[this.attribute]
|
||||||
|
: this.contentF && typeof this.contentF === 'function'
|
||||||
|
? this.contentF(props)
|
||||||
|
: null;
|
||||||
|
if (!value) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="overlay-item flex flex-row"
|
||||||
|
style={{ color: this.color || undefined }}
|
||||||
|
title={this.title || ''}
|
||||||
|
>
|
||||||
|
{this.label && (
|
||||||
|
<span className="mr-1 shrink-0">{this.label}</span>
|
||||||
|
)}
|
||||||
|
<span className="font-light">{value}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -161,6 +161,13 @@ export default class CustomizationService extends PubSubService {
|
|||||||
return this.applyType(customization);
|
return this.applyType(customization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public hasModeCustomization(customizationId: string) {
|
||||||
|
return (
|
||||||
|
this.globalCustomizations[customizationId] ||
|
||||||
|
this.modeCustomizations[customizationId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Applies any inheritance due to UI Type customization */
|
/** Applies any inheritance due to UI Type customization */
|
||||||
public applyType(customization: Customization): Customization {
|
public applyType(customization: Customization): Customization {
|
||||||
if (!customization) return customization;
|
if (!customization) return customization;
|
||||||
|
|||||||
BIN
platform/docs/docs/assets/img/customizable-overlay.png
Normal file
BIN
platform/docs/docs/assets/img/customizable-overlay.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
@ -17,135 +17,149 @@ This service is a UI service in that part of the registration allows for registe
|
|||||||
UI components and types to deal with, but it does not directly provide an UI
|
UI components and types to deal with, but it does not directly provide an UI
|
||||||
displayable elements unless customized to do so.
|
displayable elements unless customized to do so.
|
||||||
|
|
||||||
## Registering Customizations
|
<b>Note:</b> Customization Service itself doesn't implement the actual customization,
|
||||||
|
but rather just provide mechanism to register reusable prototypes, to configure
|
||||||
|
those prototypes with actual configurations, and to use the configured objects
|
||||||
|
(components, data, whatever).
|
||||||
|
Actual implementation of the customization is totally up to the component that
|
||||||
|
supports customization. (for example, `CustomizableViewportOverlay` component uses
|
||||||
|
`CustomizationService` to implement viewport overlay that is easily customizable
|
||||||
|
from configuration.)
|
||||||
|
|
||||||
|
## Registering customizable modules (or defining customization prototypes)
|
||||||
|
|
||||||
|
Extensions and Modes can register customization templates they support.
|
||||||
|
It is done by adding `getCustomizationModule()` in the extension or mode definition.
|
||||||
|
|
||||||
|
Below is the protocol of the `getCustomizationModule()`, if defined in Typescript.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
getCustomizationModule() : { name: string, value: any }[]
|
||||||
|
```
|
||||||
|
|
||||||
|
If the name is 'default', it is the Default customization, which is loaded
|
||||||
|
automatically when the extension or mode is loaded.
|
||||||
|
|
||||||
|
In the `value` of each customizations, you will define customization prototype(s).
|
||||||
|
These customization prototype(s) can be considered like "Prototype" in Javascript.
|
||||||
|
These can be used to extend the customization definitions from configurations.
|
||||||
|
Default cutomizations will be often used to define all the customization prototypes,
|
||||||
|
as they will be loaded automatically along with the defining extension or mode.
|
||||||
|
|
||||||
|
|
||||||
|
For example, the `@ohif/extension-default` extension defines,
|
||||||
|
|
||||||
|
```js
|
||||||
|
getCustomizationModule: () => [
|
||||||
|
//...
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
id: 'ohif.overlayItem',
|
||||||
|
uiType: 'uiType',
|
||||||
|
content: function (props) {
|
||||||
|
if (this.condition && !this.condition(props)) return null;
|
||||||
|
|
||||||
|
const { instance } = props;
|
||||||
|
const value =
|
||||||
|
instance && this.attribute
|
||||||
|
? instance[this.attribute]
|
||||||
|
: this.contentF && typeof this.contentF === 'function'
|
||||||
|
? this.contentF(props)
|
||||||
|
: null;
|
||||||
|
if (!value) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="overlay-item flex flex-row"
|
||||||
|
style={{ color: this.color || undefined }}
|
||||||
|
title={this.title || ''}
|
||||||
|
>
|
||||||
|
{this.label && (
|
||||||
|
<span className="mr-1 shrink-0">{this.label}</span>
|
||||||
|
)}
|
||||||
|
<span className="font-light">{value}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
//...
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
And this `ohif.overlayItem` object will be used as a prototype to define items
|
||||||
|
to be displayed on `CustomizableViewportOverlay`. See the next section.
|
||||||
|
|
||||||
|
## Configuring customizations
|
||||||
|
|
||||||
There are several ways to register customizations. The
|
There are several ways to register customizations. The
|
||||||
`APP_CONFIG.customizationService`
|
`APP_CONFIG.customizationService`
|
||||||
field is used as a per-configuration entry. This object can list single
|
field is used as a per-configuration entry. This object can list single
|
||||||
configurations by id, or it can list sets of customizations by referring to
|
configurations by id, or it can list sets of customizations by referring to
|
||||||
the `customizationModule` in an extension. For example, the fictitious
|
the `customizationModule` in an extension.
|
||||||
customization 'customIcons' might be defined as below in the APP_CONFIG:
|
|
||||||
|
NOTE that these definitions from APP_CONFIG will be loaded by default, just like
|
||||||
|
extension/modes default customization.
|
||||||
|
|
||||||
|
Below is the example configuration for `CustomizableViewportOverlay` component
|
||||||
|
customization, using the customization prototype `ohif.overlayItem` defined in
|
||||||
|
`ohif/extension-defaul` extension.:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
window.config = {
|
window.config = {
|
||||||
...,
|
//...
|
||||||
customizationService: [
|
|
||||||
{
|
// in the APP_CONFIG file set the top right area to show the patient name
|
||||||
id: 'customIcons',
|
// using PN: as a prefix when the study has a non-empty patient name.
|
||||||
backArrow: 'https://customIcons.org/backArrow.svg',
|
customizationService: {
|
||||||
|
cornerstoneOverlayTopRight: {
|
||||||
|
id: 'cornerstoneOverlayTopRight',
|
||||||
|
customizationType: 'ohif.cornerstoneOverlay',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'PatientNameOverlay',
|
||||||
|
// Note the overlayItem as a parent type - this provides the
|
||||||
|
// rendering functionality to read the attribute and use the label.
|
||||||
|
customizationType: 'ohif.overlayItem',
|
||||||
|
attribute: 'PatientName',
|
||||||
|
label: 'PN:',
|
||||||
|
title: 'Patient Name',
|
||||||
|
color: 'yellow',
|
||||||
|
condition: ({ instance }) =>
|
||||||
|
instance &&
|
||||||
|
instance.PatientName &&
|
||||||
|
instance.PatientName.Alphabetic,
|
||||||
|
contentF: ({ instance, formatters: { formatPN } }) =>
|
||||||
|
formatPN(instance.PatientName.Alphabetic) +
|
||||||
|
' ' +
|
||||||
|
(instance.PatientSex ? '(' + instance.PatientSex + ')' : ''),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
...
|
|
||||||
|
//...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
As well, extensions can register default customizations by providing a 'default'
|
In the customization configuration, you can use `customizationType` fields to
|
||||||
name key within the extension. These are simply customizations loaded when
|
define the prototype that customization object should inherit from.
|
||||||
the extension is loaded. For example, the previous customization could have
|
The `customizationType` field is simply the id of another customization object.
|
||||||
been added in an extension as:
|
|
||||||
|
|
||||||
```js
|
|
||||||
getCustomizationModule: () => [
|
|
||||||
{
|
|
||||||
name: 'default',
|
|
||||||
value: {
|
|
||||||
id: 'customIcons',
|
|
||||||
backArrow: 'https://customIcons.org/backArrow.svg',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
```
|
|
||||||
|
|
||||||
Note the name of this is default (thus loaded automatically instead of by
|
## Implementing customization using CustomizationService
|
||||||
reference), and the value is a customization of customIcons.
|
|
||||||
|
|
||||||
The type and parameters of a customization are defined by the user of the
|
### Mode Customizations
|
||||||
customization, based on the customization id. For example, `cornerstoneOverlay`
|
|
||||||
is a customization that is a React component, so it requires a react content,
|
|
||||||
and optionally contentProps which are used to supply values to the content.
|
|
||||||
|
|
||||||
The extension can also supply a default parent instance to inherit values from.
|
|
||||||
This allows the content or other parameters to be pre-filled, and only the
|
|
||||||
required values changed. The parent to use is specified by the `customizationType` field,
|
|
||||||
and is simply the id of another customization object. An example of this might
|
|
||||||
be a demographics overlay field, where the base version needs an actual component,
|
|
||||||
while the typed version just needs the attribute and label to use.
|
|
||||||
|
|
||||||
```js
|
|
||||||
getCustomizationModule: () => [
|
|
||||||
{
|
|
||||||
name: 'default',
|
|
||||||
value: [
|
|
||||||
// This first value defines the base type
|
|
||||||
{
|
|
||||||
id: imageDemographicOverlay,
|
|
||||||
content: function({image}) {
|
|
||||||
return (<p>{image[this.attribute]}</p>);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// The second one defines an instance.
|
|
||||||
// It may or may not use the previous type definition - it will use it
|
|
||||||
// if nothing replaces the previous definition, otherwise it will use the new one.
|
|
||||||
{
|
|
||||||
id: PatientIDOverlayItem,
|
|
||||||
customizationType: 'imageDemographicOverlay',
|
|
||||||
attribute: 'PatientID',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Mode-specific customizations are no different from the global ones,
|
Mode-specific customizations are no different from the global ones,
|
||||||
except that the mode customizations are cleared before the mode `onModeEnter`
|
except that the mode customizations are cleared before the mode `onModeEnter`
|
||||||
is called, and they can have new values registered in the `onModeEnter`
|
is called, and they can have new values registered in the `onModeEnter`
|
||||||
|
|
||||||
The following example shows first the registration of the default instances,
|
|
||||||
and then shows how they might be used.
|
|
||||||
|
|
||||||
```js
|
|
||||||
// In the cornerstone extension getCustomizationModule:
|
|
||||||
const getCustomizationModule = () => ([
|
|
||||||
{
|
|
||||||
name: 'default',
|
|
||||||
value: [
|
|
||||||
{
|
|
||||||
id: 'ohif.cornerstoneOverlay',
|
|
||||||
content: CornerstoneOverlay,
|
|
||||||
// Requires items on instances
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ohif.overlayItem',
|
|
||||||
content: CornerstoneOverlayItem,
|
|
||||||
// Requires attribute and label on instances
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, in the configuration file one might have a custom overlay definition:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// in the APP_CONFIG file set the top right area to show the patient name
|
|
||||||
// using PN: as a prefix when the study has a non-empty patient name.
|
|
||||||
customizationService: {
|
|
||||||
cornerstoneOverlayTopRight: {
|
|
||||||
id: 'cornerstoneOverlayTopRight',
|
|
||||||
customizationType: 'ohif.cornerstoneOverlay',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'PatientNameOverlay',
|
|
||||||
// Note the ohif.overlayItem is a prototype instance for this object
|
|
||||||
// The ohif.overlayItem is defined up above
|
|
||||||
customizationType: 'ohif.overlayItem',
|
|
||||||
attribute: 'PatientName',
|
|
||||||
label: 'PN:',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
In the mode customization, the overlay is then further customized
|
In the mode customization, the overlay is then further customized
|
||||||
with a bottom-right overlay, which extends the customizationService configuration.
|
with a bottom-right overlay, which extends the customizationService configuration.
|
||||||
|
|
||||||
@ -170,7 +184,6 @@ onModeEnter() {
|
|||||||
customizationService.addModeCustomizations(bottomRight);
|
customizationService.addModeCustomizations(bottomRight);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mode Customizations
|
|
||||||
The mode customizations are retrieved via the `getModeCustomization` function,
|
The mode customizations are retrieved via the `getModeCustomization` function,
|
||||||
providing an id, and optionally a default value. The retrieval will return,
|
providing an id, and optionally a default value. The retrieval will return,
|
||||||
in order:
|
in order:
|
||||||
@ -201,11 +214,13 @@ uses commands lists):
|
|||||||
uiConfigurationService.recordInteraction(cornerstoneContextMenu, extraProps);
|
uiConfigurationService.recordInteraction(cornerstoneContextMenu, extraProps);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Global Customizations
|
### Global Customizations
|
||||||
|
|
||||||
Global customizations are retrieved in the same was as mode customizations, except
|
Global customizations are retrieved in the same was as mode customizations, except
|
||||||
that the `getGlobalCustomization` is called instead of the mode call.
|
that the `getGlobalCustomization` is called instead of the mode call.
|
||||||
|
|
||||||
## Types
|
### Types
|
||||||
|
|
||||||
Some types for the customization service are provided by the `@ohif/ui` types
|
Some types for the customization service are provided by the `@ohif/ui` types
|
||||||
export. Additionally, extensions can provide a Types export with custom
|
export. Additionally, extensions can provide a Types export with custom
|
||||||
typing, allowing for better typing for the extension specific capabilities.
|
typing, allowing for better typing for the extension specific capabilities.
|
||||||
@ -223,7 +238,8 @@ const customContextMenu: Types.UIContextMenu =
|
|||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
## Inheritance
|
### Inheritance
|
||||||
|
|
||||||
JavaScript property inheritance can be supplied by defining customizations
|
JavaScript property inheritance can be supplied by defining customizations
|
||||||
with id corresponding to the customizationType value. For example:
|
with id corresponding to the customizationType value. For example:
|
||||||
|
|
||||||
@ -256,10 +272,12 @@ const overlayItem: Types.UIOverlayItem = {
|
|||||||
```
|
```
|
||||||
|
|
||||||
# Customizations
|
# Customizations
|
||||||
|
|
||||||
This section can be used to specify various customization capabilities.
|
This section can be used to specify various customization capabilities.
|
||||||
|
|
||||||
|
|
||||||
## Text color for StudyBrowser tabs
|
## Text color for StudyBrowser tabs
|
||||||
|
|
||||||
This is the recommended pattern for deep customization of class attributes,
|
This is the recommended pattern for deep customization of class attributes,
|
||||||
making it fine grained, and have it apply a set of attributes, mostly from
|
making it fine grained, and have it apply a set of attributes, mostly from
|
||||||
tailwind. In this case it is a double indirection, as the buttons class
|
tailwind. In this case it is a double indirection, as the buttons class
|
||||||
@ -310,6 +328,156 @@ customizationService: [
|
|||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Customizable Viewport Overlay
|
||||||
|
|
||||||
|
Below is the full example configuration of the customizable viewport overlay and the screenshot of the result overlay.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// this is one of the configuration files in `platform/viewer/public/config/*.js`
|
||||||
|
window.config = {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
customizationService: {
|
||||||
|
cornerstoneOverlayTopLeft: {
|
||||||
|
id: 'cornerstoneOverlayTopLeft',
|
||||||
|
customizationType: 'ohif.cornerstoneOverlay',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'WindowLevel',
|
||||||
|
customizationType: 'ohif.overlayItem.windowLevel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'PatientName',
|
||||||
|
customizationType: 'ohif.overlayItem',
|
||||||
|
label: '',
|
||||||
|
color: 'green',
|
||||||
|
background: 'white',
|
||||||
|
condition: ({ instance }) =>
|
||||||
|
instance && instance.PatientName && instance.PatientName.Alphabetic,
|
||||||
|
contentF: ({ instance, formatters: { formatPN } }) =>
|
||||||
|
formatPN(instance.PatientName.Alphabetic) +
|
||||||
|
' ' +
|
||||||
|
(instance.PatientSex ? '(' + instance.PatientSex + ')' : ''),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Species',
|
||||||
|
customizationType: 'ohif.overlayItem',
|
||||||
|
label: 'Species:',
|
||||||
|
condition: ({ instance }) =>
|
||||||
|
instance && instance.PatientSpeciesDescription,
|
||||||
|
contentF: ({ instance }) =>
|
||||||
|
instance.PatientSpeciesDescription +
|
||||||
|
'/' +
|
||||||
|
instance.PatientBreedDescription,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'PID',
|
||||||
|
customizationType: 'ohif.overlayItem',
|
||||||
|
label: 'PID:',
|
||||||
|
title: 'Patient PID',
|
||||||
|
condition: ({ instance }) => instance && instance.PatientID,
|
||||||
|
contentF: ({ instance }) => instance.PatientID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'PatientBirthDate',
|
||||||
|
customizationType: 'ohif.overlayItem',
|
||||||
|
label: 'DOB:',
|
||||||
|
title: "Patient's Date of birth",
|
||||||
|
condition: ({ instance }) => instance && instance.PatientBirthDate,
|
||||||
|
contentF: ({ instance }) => instance.PatientBirthDate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'OtherPid',
|
||||||
|
customizationType: 'ohif.overlayItem',
|
||||||
|
label: 'Other PID:',
|
||||||
|
title: 'Other Patient IDs',
|
||||||
|
condition: ({ instance }) => instance && instance.OtherPatientIDs,
|
||||||
|
contentF: ({ instance, formatters: { formatPN } }) =>
|
||||||
|
formatPN(instance.OtherPatientIDs),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
cornerstoneOverlayTopRight: {
|
||||||
|
id: 'cornerstoneOverlayTopRight',
|
||||||
|
customizationType: 'ohif.cornerstoneOverlay',
|
||||||
|
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'InstanceNmber',
|
||||||
|
customizationType: 'ohif.overlayItem.instanceNumber',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'StudyDescription',
|
||||||
|
customizationType: 'ohif.overlayItem',
|
||||||
|
label: '',
|
||||||
|
title: ({ instance }) =>
|
||||||
|
instance &&
|
||||||
|
instance.StudyDescription &&
|
||||||
|
`Study Description: ${instance.StudyDescription}`,
|
||||||
|
condition: ({ instance }) => instance && instance.StudyDescription,
|
||||||
|
contentF: ({ instance }) => instance.StudyDescription,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'StudyDate',
|
||||||
|
customizationType: 'ohif.overlayItem',
|
||||||
|
label: '',
|
||||||
|
title: 'Study date',
|
||||||
|
condition: ({ instance }) => instance && instance.StudyDate,
|
||||||
|
contentF: ({ instance, formatters: { formatDate } }) =>
|
||||||
|
formatDate(instance.StudyDate),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'StudyTime',
|
||||||
|
customizationType: 'ohif.overlayItem',
|
||||||
|
label: '',
|
||||||
|
title: 'Study time',
|
||||||
|
condition: ({ instance }) => instance && instance.StudyTime,
|
||||||
|
contentF: ({ instance, formatters: { formatTime } }) =>
|
||||||
|
formatTime(instance.StudyTime),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
cornerstoneOverlayBottomLeft: {
|
||||||
|
id: 'cornerstoneOverlayBottomLeft',
|
||||||
|
customizationType: 'ohif.cornerstoneOverlay',
|
||||||
|
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'SeriesNumber',
|
||||||
|
customizationType: 'ohif.overlayItem',
|
||||||
|
label: 'Ser:',
|
||||||
|
title: 'Series Number',
|
||||||
|
condition: ({ instance }) => instance && instance.SeriesNumber,
|
||||||
|
contentF: ({ instance }) => instance.SeriesNumber,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'SliceLocation',
|
||||||
|
customizationType: 'ohif.overlayItem',
|
||||||
|
label: 'Loc:',
|
||||||
|
title: 'Slice Location',
|
||||||
|
condition: ({ instance }) => instance && instance.SliceLocation,
|
||||||
|
contentF: ({ instance, formatters: { formatNumberPrecision } }) =>
|
||||||
|
formatNumberPrecision(instance.SliceLocation, 2) + ' mm',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'SliceThickness',
|
||||||
|
customizationType: 'ohif.overlayItem',
|
||||||
|
label: 'Thick:',
|
||||||
|
title: 'Slice Thickness',
|
||||||
|
condition: ({ instance }) => instance && instance.SliceThickness,
|
||||||
|
contentF: ({ instance, formatters: { formatNumberPrecision } }) =>
|
||||||
|
formatNumberPrecision(instance.SliceThickness, 2) + ' mm',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<img src="../../../assets/img/customizable-overlay.png" />
|
||||||
|
|
||||||
> 3rd Party implementers may be added to this table via pull requests.
|
> 3rd Party implementers may be added to this table via pull requests.
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const ViewportOverlay = ({
|
|||||||
bottomLeft,
|
bottomLeft,
|
||||||
color,
|
color,
|
||||||
}) => {
|
}) => {
|
||||||
const overlay = 'absolute pointer-events-none';
|
const overlay = 'absolute pointer-events-none viewport-overlay';
|
||||||
return (
|
return (
|
||||||
<div className={classnames(color ? color : 'text-primary-light')}>
|
<div className={classnames(color ? color : 'text-primary-light')}>
|
||||||
<div
|
<div
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user