fix: OHIF 3.9 Recovery 3 (#4697)
Co-authored-by: Ibrahim <93064150+IbrahimCSAE@users.noreply.github.com> Co-authored-by: Bill Wallace <wayfarer3130@gmail.com> Co-authored-by: Chao Sui <chao.sui@curvebeamai.com> Co-authored-by: Jaseel-trenser <108331536+Jaseel-trenser@users.noreply.github.com>
This commit is contained in:
parent
b19d7af9ff
commit
5d4f2e2a8e
@ -68,7 +68,7 @@
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"react-refresh": "^0.14.2",
|
||||
"semver": "^7.5.1",
|
||||
"serve": "^14.2.0",
|
||||
"serve": "^14.2.4",
|
||||
"shader-loader": "^1.3.1",
|
||||
"shx": "^0.3.3",
|
||||
"source-map-loader": "^4.0.1",
|
||||
|
||||
@ -97,6 +97,7 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||
viewportType: 'volume',
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
presentationIds: viewportOptions.presentationIds,
|
||||
}}
|
||||
displaySetOptions={[{}, pmapDisplaySetOptions]}
|
||||
></Component>
|
||||
|
||||
@ -126,6 +126,7 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
ROIContours: [],
|
||||
visible: true,
|
||||
ReferencedSOPInstanceUIDsSet: new Set(),
|
||||
};
|
||||
|
||||
for (let i = 0; i < ROIContourSequence.length; i++) {
|
||||
@ -142,7 +143,8 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet,
|
||||
|
||||
const contourPoints = [];
|
||||
for (let c = 0; c < ContourSequenceArray.length; c++) {
|
||||
const { ContourData, NumberOfContourPoints, ContourGeometricType } = ContourSequenceArray[c];
|
||||
const { ContourData, NumberOfContourPoints, ContourGeometricType, ContourImageSequence } =
|
||||
ContourSequenceArray[c];
|
||||
|
||||
let isSupported = false;
|
||||
|
||||
@ -172,6 +174,12 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet,
|
||||
type: ContourGeometricType,
|
||||
isSupported,
|
||||
});
|
||||
|
||||
if (ContourImageSequence?.ReferencedSOPInstanceUID) {
|
||||
structureSet.ReferencedSOPInstanceUIDsSet.add(
|
||||
ContourImageSequence?.ReferencedSOPInstanceUID
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_setROIContourMetadata(
|
||||
|
||||
@ -6,7 +6,7 @@ import promptHydrateRT from '../utils/promptHydrateRT';
|
||||
import _getStatusComponent from './_getStatusComponent';
|
||||
|
||||
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
|
||||
|
||||
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
|
||||
|
||||
@ -43,8 +43,8 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
|
||||
// States
|
||||
const [isToolGroupCreated, setToolGroupCreated] = useState(false);
|
||||
const [selectedSegment, setSelectedSegment] = useState(1);
|
||||
const { setPositionPresentation } = usePositionPresentationStore();
|
||||
|
||||
// Hydration means that the RT is opened and segments are loaded into the
|
||||
// segmentation panel, and RT is also rendered on any viewport that is in the
|
||||
@ -123,6 +123,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
toolGroupId: toolGroupId,
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
presentationIds: viewportOptions.presentationIds,
|
||||
}}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
@ -185,6 +186,19 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
setRtIsLoading(false);
|
||||
}
|
||||
|
||||
if (rtDisplaySet?.firstSegmentedSliceImageId && viewportOptions?.presentationIds) {
|
||||
const { firstSegmentedSliceImageId } = rtDisplaySet;
|
||||
const { presentationIds } = viewportOptions;
|
||||
|
||||
setPositionPresentation(presentationIds.positionPresentationId, {
|
||||
viewportType: 'stack',
|
||||
viewReference: {
|
||||
referencedImageId: firstSegmentedSliceImageId,
|
||||
},
|
||||
viewPresentation: {},
|
||||
});
|
||||
}
|
||||
|
||||
if (evt.overlappingSegments) {
|
||||
uiNotificationService.show({
|
||||
title: 'Overlapping Segments',
|
||||
@ -247,8 +261,6 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
|
||||
toolGroup = createRTToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
|
||||
|
||||
setToolGroupCreated(true);
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
segmentationService.removeSegmentationRepresentations(viewportId);
|
||||
|
||||
@ -4,6 +4,7 @@ import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } f
|
||||
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
||||
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
||||
import _getStatusComponent from './_getStatusComponent';
|
||||
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
|
||||
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
|
||||
@ -41,6 +42,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
|
||||
// States
|
||||
const [selectedSegment, setSelectedSegment] = useState(1);
|
||||
const { setPositionPresentation } = usePositionPresentationStore();
|
||||
|
||||
// Hydration means that the SEG is opened and segments are loaded into the
|
||||
// segmentation panel, and SEG is also rendered on any viewport that is in the
|
||||
@ -198,6 +200,17 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
if (evt.segDisplaySet.displaySetInstanceUID === segDisplaySet.displaySetInstanceUID) {
|
||||
setSegIsLoading(false);
|
||||
}
|
||||
|
||||
if (segDisplaySet?.firstSegmentedSliceImageId && viewportOptions?.presentationIds) {
|
||||
const { firstSegmentedSliceImageId } = segDisplaySet;
|
||||
const { presentationIds } = viewportOptions;
|
||||
|
||||
setPositionPresentation(presentationIds.positionPresentationId, {
|
||||
viewReference: {
|
||||
referencedImageId: firstSegmentedSliceImageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@ -9,24 +9,20 @@ import { Icon, Tooltip, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
|
||||
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
|
||||
import { useAppConfig } from '@state';
|
||||
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
|
||||
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
|
||||
|
||||
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
|
||||
|
||||
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
|
||||
|
||||
function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
const {
|
||||
commandsManager,
|
||||
children,
|
||||
dataSource,
|
||||
displaySets,
|
||||
viewportOptions,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
} = props;
|
||||
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
|
||||
props;
|
||||
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const { setPositionPresentation } = usePositionPresentationStore();
|
||||
|
||||
const {
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
@ -157,30 +153,13 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
setActiveImageDisplaySetData(referencedDisplaySet);
|
||||
setReferencedDisplaySetMetadata(referencedDisplaySetMetadata);
|
||||
|
||||
if (
|
||||
referencedDisplaySet.displaySetInstanceUID ===
|
||||
activeImageDisplaySetData?.displaySetInstanceUID
|
||||
) {
|
||||
const { measurements } = srDisplaySet;
|
||||
|
||||
// it means that we have a new referenced display set, and the
|
||||
// imageIdIndex will handle it by updating the viewport, but if they
|
||||
// are the same we just need to use measurementService to jump to the
|
||||
// new measurement
|
||||
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (!csViewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIds = csViewport.getImageIds();
|
||||
|
||||
const imageIdIndex = imageIds.indexOf(measurements[newMeasurementSelected].imageId);
|
||||
|
||||
if (imageIdIndex !== -1) {
|
||||
csViewport.setImageIdIndex(imageIdIndex);
|
||||
}
|
||||
}
|
||||
const { presentationIds } = viewportOptions;
|
||||
const measurement = srDisplaySet.measurements[newMeasurementSelected];
|
||||
setPositionPresentation(presentationIds.positionPresentationId, {
|
||||
viewReference: {
|
||||
referencedImageId: measurement.imageId,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
[dataSource, srDisplaySet, activeImageDisplaySetData, viewportId]
|
||||
@ -202,10 +181,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const initialImageIndex = activeImageDisplaySetData.images.findIndex(
|
||||
image => image.imageId === measurement.imageId
|
||||
);
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
@ -230,7 +205,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
initialImageIndex={initialImageIndex}
|
||||
isJumpToMeasurementDisabled={true}
|
||||
></Component>
|
||||
);
|
||||
|
||||
@ -25,7 +25,7 @@ const contentItemFormatters = {
|
||||
return `${NumericValue} ${CodeValue}`;
|
||||
},
|
||||
PNAME: contentItem => {
|
||||
const personName = contentItem.PersonName?.[0]?.Alphabetic;
|
||||
const personName = contentItem.PersonName?.[0];
|
||||
return personName ? utils.formatPN(personName) : undefined;
|
||||
},
|
||||
DATE: contentItem => {
|
||||
|
||||
@ -262,7 +262,6 @@ function _getInstanceFromSegmentations(segmentations, { servicesManager }) {
|
||||
}
|
||||
|
||||
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
|
||||
debugger;
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
const { seriesMetadata, instance } =
|
||||
|
||||
@ -5,12 +5,14 @@ import { metaData, Enums, utilities } from '@cornerstonejs/core';
|
||||
import type { ImageSliceData } from '@cornerstonejs/core/types';
|
||||
import { ViewportOverlay } from '@ohif/ui';
|
||||
import type { InstanceMetadata } from '@ohif/core/src/types';
|
||||
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
|
||||
import { formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
|
||||
import { utils } from '@ohif/core';
|
||||
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
||||
|
||||
import './CustomizableViewportOverlay.css';
|
||||
|
||||
const EPSILON = 1e-4;
|
||||
const { formatPN } = utils;
|
||||
|
||||
type ViewportData = StackViewportData | VolumeViewportData;
|
||||
|
||||
|
||||
@ -52,31 +52,6 @@ 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 '';
|
||||
}
|
||||
if (typeof name === 'object') {
|
||||
name = name.Alphabetic;
|
||||
if (!name) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const cleaned = name
|
||||
.split('^')
|
||||
.filter(s => !!s)
|
||||
.join(', ')
|
||||
.trim();
|
||||
return cleaned === ',' || cleaned === '' ? '' : cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets compression type
|
||||
*
|
||||
|
||||
@ -20,7 +20,7 @@ const frameView: Types.HangingProtocol.Protocol = {
|
||||
},
|
||||
{
|
||||
attribute: 'isDisplaySetFromUrl',
|
||||
weight: 10,
|
||||
weight: 20,
|
||||
constraint: {
|
||||
equals: true,
|
||||
},
|
||||
|
||||
@ -162,6 +162,14 @@ export default function PanelSegmentation({
|
||||
const firstImageId = referencedImageIds[0];
|
||||
|
||||
const instance = metaData.get('instance', firstImageId);
|
||||
|
||||
if (!instance) {
|
||||
return {
|
||||
segmentationId,
|
||||
isExportable: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { SOPInstanceUID, SeriesInstanceUID } = instance;
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
|
||||
|
||||
@ -478,14 +478,24 @@ class SegmentationService extends PubSubService {
|
||||
// We should parse the segmentation as separate slices to support overlapping segments.
|
||||
// This parsing should occur in the CornerstoneJS library adapters.
|
||||
// For now, we use the volume returned from the library and chop it here.
|
||||
let firstSegmentedSliceImageId = null;
|
||||
for (let i = 0; i < derivedSegmentationImages.length; i++) {
|
||||
const voxelManager = derivedSegmentationImages[i]
|
||||
.voxelManager as csTypes.IVoxelManager<number>;
|
||||
const scalarData = voxelManager.getScalarData();
|
||||
scalarData.set(volumeScalarData.slice(i * scalarData.length, (i + 1) * scalarData.length));
|
||||
const sliceData = volumeScalarData.slice(i * scalarData.length, (i + 1) * scalarData.length);
|
||||
scalarData.set(sliceData);
|
||||
voxelManager.setScalarData(scalarData);
|
||||
|
||||
// Check if this slice has any non-zero voxels and we haven't found one yet
|
||||
if (!firstSegmentedSliceImageId && sliceData.some(value => value !== 0)) {
|
||||
firstSegmentedSliceImageId = derivedSegmentationImages[i].referencedImageId;
|
||||
}
|
||||
}
|
||||
|
||||
// assign the first non zero voxel image id to the segDisplaySet
|
||||
segDisplaySet.firstSegmentedSliceImageId = firstSegmentedSliceImageId;
|
||||
|
||||
this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, {
|
||||
segmentationId,
|
||||
segDisplaySet,
|
||||
@ -542,7 +552,19 @@ class SegmentationService extends PubSubService {
|
||||
}
|
||||
|
||||
const rtDisplaySetUID = rtDisplaySet.displaySetInstanceUID;
|
||||
const referencedDisplaySet = this.servicesManager.services.displaySetService.getDisplaySetByUID(
|
||||
rtDisplaySet.referencedDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
const referencedImageIdsWithGeometry = Array.from(structureSet.ReferencedSOPInstanceUIDsSet);
|
||||
|
||||
const referencedImageIds = referencedDisplaySet.instances.map(image => image.imageId);
|
||||
// find the first image id that contains a referenced SOP instance UID
|
||||
const firstSegmentedSliceImageId = referencedImageIds.find(imageId =>
|
||||
referencedImageIdsWithGeometry.some(referencedId => imageId.includes(referencedId))
|
||||
);
|
||||
|
||||
rtDisplaySet.firstSegmentedSliceImageId = firstSegmentedSliceImageId;
|
||||
// Map ROI contours to RT Struct Data
|
||||
const allRTStructData = mapROIContoursToRTStructData(structureSet, rtDisplaySetUID);
|
||||
|
||||
|
||||
@ -605,10 +605,6 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
let initialImageIndexToUse =
|
||||
presentations?.positionPresentation?.initialImageIndex ?? initialImageIndex;
|
||||
|
||||
if (initialImageIndexToUse === undefined || initialImageIndexToUse === null) {
|
||||
initialImageIndexToUse = this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0;
|
||||
}
|
||||
|
||||
const { rotation, flipHorizontal, displayArea } = viewportInfo.getViewportOptions();
|
||||
|
||||
const properties = { ...presentations.lutPresentation?.properties };
|
||||
@ -637,12 +633,26 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
});
|
||||
|
||||
let imageIdsToSet = imageIds;
|
||||
const res = this._processExtraDisplaySetsForViewport(viewport);
|
||||
imageIdsToSet = res?.imageIds ?? imageIdsToSet;
|
||||
const overlayProcessingResult = this._processExtraDisplaySetsForViewport(viewport);
|
||||
imageIdsToSet = overlayProcessingResult?.imageIds ?? imageIdsToSet;
|
||||
|
||||
const referencedImageId = presentations?.positionPresentation?.viewReference?.referencedImageId;
|
||||
if (referencedImageId) {
|
||||
initialImageIndexToUse = imageIdsToSet.indexOf(referencedImageId);
|
||||
}
|
||||
|
||||
if (initialImageIndexToUse === undefined || initialImageIndexToUse === null) {
|
||||
initialImageIndexToUse = this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0;
|
||||
}
|
||||
|
||||
return viewport.setStack(imageIdsToSet, initialImageIndexToUse).then(() => {
|
||||
viewport.setProperties({ ...properties });
|
||||
this.setPresentations(viewport.id, presentations, viewportInfo);
|
||||
|
||||
if (overlayProcessingResult?.addOverlayFn) {
|
||||
overlayProcessingResult.addOverlayFn();
|
||||
}
|
||||
|
||||
if (displayArea) {
|
||||
viewport.setDisplayArea(displayArea);
|
||||
}
|
||||
@ -827,10 +837,14 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
});
|
||||
|
||||
// For SEG and RT viewports
|
||||
this._processExtraDisplaySetsForViewport(viewport);
|
||||
const { addOverlayFn } = this._processExtraDisplaySetsForViewport(viewport) || {};
|
||||
|
||||
await viewport.setVolumes(volumeInputArray);
|
||||
|
||||
if (addOverlayFn) {
|
||||
addOverlayFn();
|
||||
}
|
||||
|
||||
volumesProperties.forEach(({ properties, volumeId }) => {
|
||||
viewport.setProperties(properties, volumeId);
|
||||
});
|
||||
@ -880,8 +894,12 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
segOrRTSOverlayDisplaySet.referencedDisplaySetInstanceUID
|
||||
);
|
||||
const imageIds = referenceDisplaySet.images.map(image => image.imageId);
|
||||
this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport);
|
||||
return { imageIds };
|
||||
|
||||
return {
|
||||
imageIds,
|
||||
addOverlayFn: () =>
|
||||
this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport),
|
||||
};
|
||||
}
|
||||
|
||||
private addOverlayRepresentationForDisplaySet(
|
||||
|
||||
@ -4,7 +4,7 @@ import { PositionPresentation } from '../types/Presentation';
|
||||
import { addUniqueIndex, JOIN_STR } from './presentationUtils';
|
||||
|
||||
const PRESENTATION_TYPE_ID = 'positionPresentationId';
|
||||
const DEBUG_STORE = true;
|
||||
const DEBUG_STORE = false;
|
||||
|
||||
/**
|
||||
* Represents the state and actions for managing position presentations.
|
||||
|
||||
@ -142,7 +142,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,
|
||||
@ -151,22 +153,14 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
} = instance;
|
||||
|
||||
instance.imageId = imageId;
|
||||
const numberOfFrames = instance.NumberOfFrames || 1;
|
||||
// Process all frames consistently, whether single or multiframe
|
||||
for (let i = 0; i < numberOfFrames; i++) {
|
||||
const frameNumber = i + 1;
|
||||
const frameImageId = implementation.getImageIdsForInstance({
|
||||
instance,
|
||||
frame: frameNumber,
|
||||
});
|
||||
// Add imageId specific mapping to this data as the URL isn't necessarily WADO-URI.
|
||||
metadataProvider.addImageIdToUIDs(frameImageId, {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameNumber: numberOfFrames > 1 ? frameNumber : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// Add imageId specific mapping to this data as the URL isn't necessarily WADO-URI.
|
||||
metadataProvider.addImageIdToUIDs(imageId, {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex: isMultiframe ? index : 1,
|
||||
});
|
||||
});
|
||||
|
||||
DicomMetadataStore._broadcastEvent(EVENTS.INSTANCES_ADDED, {
|
||||
|
||||
@ -30,6 +30,7 @@ function WrappedPanelStudyBrowser({ commandsManager, extensionManager, servicesM
|
||||
return (
|
||||
<PanelStudyBrowser
|
||||
servicesManager={servicesManager}
|
||||
commandsManager={commandsManager}
|
||||
dataSource={dataSource}
|
||||
getImageSrc={_getImageSrcFromImageId}
|
||||
getStudiesForPatientByMRN={_getStudiesForPatientByMRN}
|
||||
|
||||
@ -63,7 +63,7 @@ const defaultProtocol = {
|
||||
// It has no affect if nothing is specified in the URL.
|
||||
{
|
||||
attribute: 'isDisplaySetFromUrl',
|
||||
weight: 10,
|
||||
weight: 20,
|
||||
constraint: {
|
||||
equals: true,
|
||||
},
|
||||
|
||||
@ -24,7 +24,7 @@ const defaultDisplaySetSelector = {
|
||||
// It has no affect if nothing is specified in the URL.
|
||||
{
|
||||
attribute: 'isDisplaySetFromUrl',
|
||||
weight: 10,
|
||||
weight: 20,
|
||||
constraint: {
|
||||
equals: true,
|
||||
},
|
||||
@ -56,7 +56,7 @@ const priorDisplaySetSelector = {
|
||||
// It has no affect if nothing is specified in the URL.
|
||||
{
|
||||
attribute: 'isDisplaySetFromUrl',
|
||||
weight: 10,
|
||||
weight: 20,
|
||||
constraint: {
|
||||
equals: true,
|
||||
},
|
||||
|
||||
@ -50,7 +50,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
||||
// It has no affect if nothing is specified in the URL.
|
||||
{
|
||||
attribute: 'isDisplaySetFromUrl',
|
||||
weight: 10,
|
||||
weight: 20,
|
||||
constraint: {
|
||||
equals: true,
|
||||
},
|
||||
|
||||
@ -43,7 +43,7 @@ const hpScale: Types.HangingProtocol.Protocol = {
|
||||
// It has no affect if nothing is specified in the URL.
|
||||
{
|
||||
attribute: 'isDisplaySetFromUrl',
|
||||
weight: 10,
|
||||
weight: 20,
|
||||
constraint: {
|
||||
equals: true,
|
||||
},
|
||||
|
||||
@ -38,7 +38,7 @@ function usePatientInfo(servicesManager: AppTypes.ServicesManager) {
|
||||
}
|
||||
setPatientInfo({
|
||||
PatientID: instance.PatientID || null,
|
||||
PatientName: instance.PatientName ? formatPN(instance.PatientName.Alphabetic) : null,
|
||||
PatientName: instance.PatientName ? formatPN(instance.PatientName) : null,
|
||||
PatientSex: instance.PatientSex || null,
|
||||
PatientDOB: formatDate(instance.PatientBirthDate) || null,
|
||||
});
|
||||
|
||||
@ -2,7 +2,7 @@ import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
|
||||
const PRESENTATION_TYPE_ID = 'viewportsByPositionId';
|
||||
const DEBUG_STORE = true;
|
||||
const DEBUG_STORE = false;
|
||||
|
||||
/**
|
||||
* Represents the state and actions for managing viewports by position.
|
||||
|
||||
@ -3,7 +3,10 @@ import classnames from 'classnames';
|
||||
|
||||
import listComponentGenerator from './listComponentGenerator';
|
||||
import './ViewportOverlay.css';
|
||||
import { formatDICOMDate, formatDICOMTime, formatNumberPrecision, formatPN } from './utils';
|
||||
import { formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const { formatPN } = utils;
|
||||
|
||||
interface OverlayItem {
|
||||
id: string;
|
||||
|
||||
@ -52,28 +52,6 @@ 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;
|
||||
}
|
||||
|
||||
// Convert the first ^ to a ', '. String.replace() only affects
|
||||
// the first appearance of the character.
|
||||
const commaBetweenFirstAndLast = name.replace('^', ', ');
|
||||
|
||||
// Replace any remaining '^' characters with spaces
|
||||
const cleaned = commaBetweenFirstAndLast.replace(/\^/g, ' ');
|
||||
|
||||
// Trim any extraneous whitespace
|
||||
return cleaned.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets compression type
|
||||
*
|
||||
|
||||
@ -188,7 +188,7 @@ const hpTestSwitch: Types.HangingProtocol.Protocol = {
|
||||
// It has no affect if nothing is specified in the URL.
|
||||
{
|
||||
attribute: 'isDisplaySetFromUrl',
|
||||
weight: 10,
|
||||
weight: 20,
|
||||
constraint: {
|
||||
equals: true,
|
||||
},
|
||||
|
||||
@ -9,8 +9,10 @@ import dicomRTAnnotationExport from './utils/dicomRTAnnotationExport/RTStructure
|
||||
|
||||
import { getWebWorkerManager } from '@cornerstonejs/core';
|
||||
import { Enums } from '@cornerstonejs/tools';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const { SegmentationRepresentations } = Enums;
|
||||
const { formatPN } = utils;
|
||||
|
||||
const metadataProvider = classes.MetadataProvider;
|
||||
const ROI_THRESHOLD_MANUAL_TOOL_IDS = [
|
||||
@ -595,7 +597,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }:
|
||||
report[id] = {
|
||||
...segReport,
|
||||
PatientID: instance.PatientID ?? '000000',
|
||||
PatientName: instance.PatientName.Alphabetic,
|
||||
PatientName: formatPN(instance.PatientName),
|
||||
StudyInstanceUID: instance.StudyInstanceUID,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
StudyDate: instance.StudyDate,
|
||||
|
||||
@ -23,6 +23,7 @@ window.config = {
|
||||
wadoRoot: 'http://localhost/dicom-web',
|
||||
qidoSupportsIncludeField: true,
|
||||
supportsReject: true,
|
||||
dicomUploadEnabled: true,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
|
||||
@ -6,10 +6,10 @@ import DicomMetadataStore from '../services/DicomMetadataStore';
|
||||
import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData';
|
||||
import toNumber from '../utils/toNumber';
|
||||
import combineFrameInstance from '../utils/combineFrameInstance';
|
||||
import formatPN from '../utils/formatPN';
|
||||
|
||||
class MetadataProvider {
|
||||
private readonly imageURIToUIDs: Map<string, any> = new Map();
|
||||
private readonly imageUIDsByImageId: Map<string, any> = new Map();
|
||||
// Can be used to store custom metadata for a specific type.
|
||||
// For instance, the scaling metadata for PET can be stored here
|
||||
// as type "scalingModule"
|
||||
@ -25,7 +25,6 @@ class MetadataProvider {
|
||||
// An example would be dicom hosted at some random site.
|
||||
const imageURI = imageIdToURI(imageId);
|
||||
this.imageURIToUIDs.set(imageURI, uids);
|
||||
this.imageUIDsByImageId.set(imageId, uids);
|
||||
}
|
||||
|
||||
addCustomMetadata(imageId, type, metadata) {
|
||||
@ -347,7 +346,7 @@ class MetadataProvider {
|
||||
|
||||
let patientName;
|
||||
if (PatientName) {
|
||||
patientName = PatientName.Alphabetic;
|
||||
patientName = formatPN(PatientName);
|
||||
}
|
||||
|
||||
metadata = {
|
||||
@ -462,11 +461,6 @@ class MetadataProvider {
|
||||
}
|
||||
|
||||
getUIDsFromImageID(imageId) {
|
||||
const cachedUIDs = this.imageUIDsByImageId.get(imageId);
|
||||
if (cachedUIDs) {
|
||||
return cachedUIDs;
|
||||
}
|
||||
|
||||
if (imageId.startsWith('wadors:')) {
|
||||
const strippedImageId = imageId.split('/studies/')[1];
|
||||
const splitImageId = strippedImageId.split('/');
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { vec3 } from 'gl-matrix';
|
||||
import { dicomSplit } from './dicomSplit';
|
||||
|
||||
/**
|
||||
* Combine the Per instance frame data, the shared frame data
|
||||
@ -15,24 +16,13 @@ const combineFrameInstance = (frame, instance) => {
|
||||
PerFrameFunctionalGroupsSequence,
|
||||
SharedFunctionalGroupsSequence,
|
||||
NumberOfFrames,
|
||||
SpacingBetweenSlices,
|
||||
ImageType,
|
||||
} = instance;
|
||||
|
||||
instance.ImageType = dicomSplit(ImageType);
|
||||
|
||||
if (PerFrameFunctionalGroupsSequence || NumberOfFrames > 1) {
|
||||
const frameNumber = Number.parseInt(frame || 1);
|
||||
const shared = SharedFunctionalGroupsSequence
|
||||
? Object.values(SharedFunctionalGroupsSequence[0])
|
||||
.filter(Boolean)
|
||||
.map(it => it[0])
|
||||
.filter(it => typeof it === 'object')
|
||||
: [];
|
||||
|
||||
const perFrame = PerFrameFunctionalGroupsSequence
|
||||
? Object.values(PerFrameFunctionalGroupsSequence[frameNumber - 1])
|
||||
.filter(Boolean)
|
||||
.map(it => it[0])
|
||||
.filter(it => typeof it === 'object')
|
||||
: [];
|
||||
|
||||
// this is to fix NM multiframe datasets with position and orientation
|
||||
// information inside DetectorInformationSequence
|
||||
@ -44,8 +34,12 @@ const combineFrameInstance = (frame, instance) => {
|
||||
let ImagePositionPatientToUse = instance.ImagePositionPatient;
|
||||
|
||||
if (!instance.ImagePositionPatient && instance.DetectorInformationSequence) {
|
||||
const imagePositionPatient = instance.DetectorInformationSequence[0].ImagePositionPatient;
|
||||
const imageOrientationPatient = instance.ImageOrientationPatient;
|
||||
let imagePositionPatient = instance.DetectorInformationSequence[0].ImagePositionPatient;
|
||||
let imageOrientationPatient = instance.ImageOrientationPatient;
|
||||
|
||||
imagePositionPatient = imagePositionPatient.map(it => Number(it));
|
||||
imageOrientationPatient = imageOrientationPatient.map(it => Number(it));
|
||||
const SpacingBetweenSlices = Number(instance.SpacingBetweenSlices);
|
||||
|
||||
// Calculate the position for the current frame
|
||||
if (imageOrientationPatient && SpacingBetweenSlices) {
|
||||
@ -73,27 +67,77 @@ const combineFrameInstance = (frame, instance) => {
|
||||
ImagePositionPatientToUse = [position[0], position[1], position[2]];
|
||||
}
|
||||
}
|
||||
console.debug('🚀 ~ ImagePositionPatientToUse:', ImagePositionPatientToUse);
|
||||
|
||||
const newInstance = Object.assign(instance, { frameNumber: frameNumber });
|
||||
|
||||
// merge the shared first then the per frame to override
|
||||
[...shared, ...perFrame].forEach(item => {
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
newInstance[key] = value;
|
||||
// Cache the _parentInstance at the top level as a full copy to prevent
|
||||
// setting values hard.
|
||||
if (!instance._parentInstance) {
|
||||
Object.defineProperty(instance, '_parentInstance', {
|
||||
value: { ...instance },
|
||||
});
|
||||
});
|
||||
}
|
||||
const sharedInstance = createCombinedValue(
|
||||
instance._parentInstance,
|
||||
SharedFunctionalGroupsSequence?.[0],
|
||||
'_shared'
|
||||
);
|
||||
const newInstance = createCombinedValue(
|
||||
sharedInstance,
|
||||
PerFrameFunctionalGroupsSequence?.[frameNumber - 1],
|
||||
frameNumber
|
||||
);
|
||||
|
||||
// Todo: we should cache this combined instance somewhere, maybe add it
|
||||
// back to the dicomMetaStore so we don't have to do this again.
|
||||
return {
|
||||
...newInstance,
|
||||
ImagePositionPatient: ImagePositionPatientToUse ??
|
||||
newInstance.ImagePositionPatient ?? [0, 0, frameNumber],
|
||||
};
|
||||
newInstance.ImagePositionPatient = ImagePositionPatientToUse ??
|
||||
newInstance.ImagePositionPatient ?? [0, 0, frameNumber];
|
||||
|
||||
Object.defineProperty(newInstance, 'frameNumber', {
|
||||
value: frameNumber,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
return newInstance;
|
||||
} else {
|
||||
return instance;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a combined instance stored in the parent object which
|
||||
* inherits from the parent instance the attributes in the functional groups.
|
||||
* The storage key in the parent is in key
|
||||
*/
|
||||
function createCombinedValue(parent, functionalGroups, key) {
|
||||
if (parent[key]) {
|
||||
return parent[key];
|
||||
}
|
||||
// Exclude any proxying values
|
||||
const newInstance = Object.create(parent);
|
||||
Object.defineProperty(parent, key, {
|
||||
value: newInstance,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
});
|
||||
if (!functionalGroups) {
|
||||
return newInstance;
|
||||
}
|
||||
const shared = functionalGroups
|
||||
? Object.values(functionalGroups)
|
||||
.filter(Boolean)
|
||||
.map(it => it[0])
|
||||
.filter(it => typeof it === 'object')
|
||||
: [];
|
||||
|
||||
// merge the shared first then the per frame to override
|
||||
[...shared].forEach(item => {
|
||||
if (item.SOPInstanceUID) {
|
||||
// This sub-item is a previous value information item, so don't merge it
|
||||
return;
|
||||
}
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
newInstance[key] = value;
|
||||
});
|
||||
});
|
||||
return newInstance;
|
||||
}
|
||||
|
||||
export default combineFrameInstance;
|
||||
|
||||
5
platform/core/src/utils/dicomSplit.ts
Normal file
5
platform/core/src/utils/dicomSplit.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export function dicomSplit(value) {
|
||||
return (
|
||||
(Array.isArray(value) && value) || (typeof value === 'string' && value.split('\\')) || value
|
||||
);
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import { DicomMetadataStore } from '../services/DicomMetadataStore/DicomMetadataStore';
|
||||
import formatPN from './formatPN';
|
||||
|
||||
export default function downloadCSVReport(measurementData) {
|
||||
if (measurementData.length === 0) {
|
||||
@ -86,7 +87,7 @@ function _getCommonRowItems(measurement, seriesMetadata) {
|
||||
|
||||
return {
|
||||
'Patient ID': firstInstance.PatientID, // Patient ID
|
||||
'Patient Name': firstInstance.PatientName?.Alphabetic || '', // Patient Name
|
||||
'Patient Name': formatPN(firstInstance.PatientName) || '', // Patient Name
|
||||
StudyInstanceUID: measurement.referenceStudyUID, // StudyInstanceUID
|
||||
SeriesInstanceUID: measurement.referenceSeriesUID, // SeriesInstanceUID
|
||||
SOPInstanceUID: measurement.SOPInstanceUID, // SOPInstanceUID
|
||||
|
||||
@ -48,7 +48,7 @@ function usePatientInfo(servicesManager: AppTypes.ServicesManager) {
|
||||
}
|
||||
setPatientInfo({
|
||||
PatientID: instance.PatientID || null,
|
||||
PatientName: instance.PatientName ? formatPN(instance.PatientName.Alphabetic) : null,
|
||||
PatientName: instance.PatientName ? formatPN(instance.PatientName) : null,
|
||||
PatientSex: instance.PatientSex || null,
|
||||
PatientDOB: formatDate(instance.PatientBirthDate) || null,
|
||||
});
|
||||
|
||||
@ -1 +1 @@
|
||||
3.9.2
|
||||
3.9.3
|
||||
37
yarn.lock
37
yarn.lock
@ -12594,13 +12594,6 @@ fast-uri@^3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.2.tgz#d78b298cf70fd3b752fd951175a3da6a7b48f024"
|
||||
integrity sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==
|
||||
|
||||
fast-url-parser@1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d"
|
||||
integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==
|
||||
dependencies:
|
||||
punycode "^1.3.2"
|
||||
|
||||
fastest-levenshtein@^1.0.12:
|
||||
version "1.0.16"
|
||||
resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5"
|
||||
@ -19130,7 +19123,7 @@ path-to-regexp@1.9.0, path-to-regexp@^1.7.0:
|
||||
dependencies:
|
||||
isarray "0.0.1"
|
||||
|
||||
path-to-regexp@2.2.1, path-to-regexp@3.3.0:
|
||||
path-to-regexp@3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b"
|
||||
integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==
|
||||
@ -20438,7 +20431,7 @@ pumpify@^1.3.3:
|
||||
inherits "^2.0.3"
|
||||
pump "^2.0.0"
|
||||
|
||||
punycode@^1.3.2, punycode@^1.4.1:
|
||||
punycode@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
|
||||
integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==
|
||||
@ -22034,21 +22027,7 @@ serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
|
||||
dependencies:
|
||||
randombytes "^2.1.0"
|
||||
|
||||
serve-handler@6.1.5:
|
||||
version "6.1.5"
|
||||
resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.5.tgz#a4a0964f5c55c7e37a02a633232b6f0d6f068375"
|
||||
integrity sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg==
|
||||
dependencies:
|
||||
bytes "3.0.0"
|
||||
content-disposition "0.5.2"
|
||||
fast-url-parser "1.1.3"
|
||||
mime-types "2.1.18"
|
||||
minimatch "3.1.2"
|
||||
path-is-inside "1.0.2"
|
||||
path-to-regexp "2.2.1"
|
||||
range-parser "1.2.0"
|
||||
|
||||
serve-handler@^6.1.6:
|
||||
serve-handler@6.1.6, serve-handler@^6.1.6:
|
||||
version "6.1.6"
|
||||
resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.6.tgz#50803c1d3e947cd4a341d617f8209b22bd76cfa1"
|
||||
integrity sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==
|
||||
@ -22084,10 +22063,10 @@ serve-static@1.16.2:
|
||||
parseurl "~1.3.3"
|
||||
send "0.19.0"
|
||||
|
||||
serve@^14.2.0:
|
||||
version "14.2.3"
|
||||
resolved "https://registry.yarnpkg.com/serve/-/serve-14.2.3.tgz#047ba2b349354255bc09e0332cd41a92787836c9"
|
||||
integrity sha512-VqUFMC7K3LDGeGnJM9h56D3XGKb6KGgOw0cVNtA26yYXHCcpxf3xwCTUaQoWlVS7i8Jdh3GjQkOB23qsXyjoyQ==
|
||||
serve@^14.2.4:
|
||||
version "14.2.4"
|
||||
resolved "https://registry.yarnpkg.com/serve/-/serve-14.2.4.tgz#ba4c425c3c965f496703762e808f34b913f42fb0"
|
||||
integrity sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==
|
||||
dependencies:
|
||||
"@zeit/schemas" "2.36.0"
|
||||
ajv "8.12.0"
|
||||
@ -22098,7 +22077,7 @@ serve@^14.2.0:
|
||||
clipboardy "3.0.0"
|
||||
compression "1.7.4"
|
||||
is-port-reachable "4.0.0"
|
||||
serve-handler "6.1.5"
|
||||
serve-handler "6.1.6"
|
||||
update-check "1.5.4"
|
||||
|
||||
set-blocking@^2.0.0:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user