feat: Add CS3D WSI and Video Viewports and add annotation navigation for MPR (#4182)
This commit is contained in:
parent
b12e0211d4
commit
7599ec9421
@ -385,7 +385,7 @@ jobs:
|
||||
- yarn-berry
|
||||
type: enum
|
||||
parallelism:
|
||||
default: 1
|
||||
default: 4
|
||||
description: |
|
||||
Number of Circle machines to use for load balancing, min 1
|
||||
(requires `parallel` and `record` flags in your `cypress-command`)
|
||||
|
||||
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@ -1,6 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"mikestead.dotenv",
|
||||
"bungcip.better-toml",
|
||||
|
||||
102
.vscode/settings.json
vendored
102
.vscode/settings.json
vendored
@ -1,4 +1,5 @@
|
||||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.rulers": [80, 120],
|
||||
// ===
|
||||
// Spacing
|
||||
@ -14,23 +15,100 @@
|
||||
// Event Triggers
|
||||
// ===
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.autoFixOnSave": true,
|
||||
"eslint.run": "onSave",
|
||||
"eslint.validate": [
|
||||
{
|
||||
"language": "javascript",
|
||||
"autoFix": true
|
||||
},
|
||||
{
|
||||
"language": "javascriptreact",
|
||||
"autoFix": true
|
||||
}
|
||||
],
|
||||
"jest.autoRun": "off",
|
||||
"prettier.disableLanguages": ["html"],
|
||||
"prettier.endOfLine": "lf",
|
||||
"workbench.colorCustomizations": {},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
},
|
||||
"cSpell.userWords": [
|
||||
"aabb",
|
||||
"architectured",
|
||||
"attrname",
|
||||
"Barksy",
|
||||
"browserslist",
|
||||
"bulkdata",
|
||||
"Cacheable",
|
||||
"cfun",
|
||||
"clonedeep",
|
||||
"Colormap",
|
||||
"Colormaps",
|
||||
"Comlink",
|
||||
"cornerstonejs",
|
||||
"Crosshairs",
|
||||
"datasource",
|
||||
"dcmjs",
|
||||
"decache",
|
||||
"decached",
|
||||
"decaching",
|
||||
"deepmerge",
|
||||
"Dicom",
|
||||
"dicomweb",
|
||||
"DISPLAYSETS",
|
||||
"glwindow",
|
||||
"grababble",
|
||||
"grabbable",
|
||||
"Hounsfield",
|
||||
"Interactable",
|
||||
"Interactor",
|
||||
"istyle",
|
||||
"kitware",
|
||||
"labelmap",
|
||||
"labelmaps",
|
||||
"livewire",
|
||||
"Mergeable",
|
||||
"multiframe",
|
||||
"nifti",
|
||||
"ofun",
|
||||
"OHIF",
|
||||
"polylines",
|
||||
"POLYSEG",
|
||||
"prapogation",
|
||||
"precisionmetrics",
|
||||
"prefetch",
|
||||
"Prescaled",
|
||||
"pydicom",
|
||||
"Radiopharmaceutical",
|
||||
"rasterizing",
|
||||
"reconstructable",
|
||||
"Rehydratable",
|
||||
"renderable",
|
||||
"resampler",
|
||||
"resemblejs",
|
||||
"reslice",
|
||||
"resliced",
|
||||
"Reslices",
|
||||
"roadmap",
|
||||
"ROADMAPS",
|
||||
"Segmentations",
|
||||
"semibold",
|
||||
"sitk",
|
||||
"SUBRESOLUTION",
|
||||
"suvbsa",
|
||||
"suvbw",
|
||||
"suvlbm",
|
||||
"textbox",
|
||||
"thresholded",
|
||||
"thresholding",
|
||||
"timepoint",
|
||||
"timepoints",
|
||||
"TMTV",
|
||||
"TOOLGROUP",
|
||||
"tqdm",
|
||||
"transferables",
|
||||
"typedoc",
|
||||
"unsubscriptions",
|
||||
"uuidv",
|
||||
"viewplane",
|
||||
"viewports",
|
||||
"Voxel",
|
||||
"Voxels",
|
||||
"Vtkjs",
|
||||
"wado",
|
||||
"wadors",
|
||||
"wadouri",
|
||||
"workerpool"
|
||||
]
|
||||
}
|
||||
|
||||
@ -14,11 +14,12 @@ const TerserJSPlugin = require('terser-webpack-plugin');
|
||||
// const vtkRules = require('vtk.js/Utilities/config/dependency.js').webpack.core
|
||||
// .rules;
|
||||
// ~~ RULES
|
||||
const loadShadersRule = require('./rules/loadShaders.js');
|
||||
// const loadShadersRule = require('./rules/loadShaders.js');
|
||||
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
|
||||
const transpileJavaScriptRule = require('./rules/transpileJavaScript.js');
|
||||
const cssToJavaScript = require('./rules/cssToJavaScript.js');
|
||||
const stylusToJavaScript = require('./rules/stylusToJavaScript.js');
|
||||
// Only uncomment for old v2 stylus
|
||||
// const stylusToJavaScript = require('./rules/stylusToJavaScript.js');
|
||||
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
||||
|
||||
|
||||
|
||||
@ -46,9 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.82.0",
|
||||
"@cornerstonejs/core": "^1.82.0",
|
||||
"@cornerstonejs/tools": "^1.82.0",
|
||||
"@cornerstonejs/adapters": "^1.82.4",
|
||||
"@cornerstonejs/core": "^1.82.4",
|
||||
"@cornerstonejs/tools": "^1.82.4",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,9 +42,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/core": "^1.82.0",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.82.0",
|
||||
"@cornerstonejs/tools": "^1.82.0",
|
||||
"@cornerstonejs/core": "^1.82.4",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.82.4",
|
||||
"@cornerstonejs/tools": "^1.82.4",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.2",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.82.0",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.82.4",
|
||||
"@icr/polyseg-wasm": "^0.4.0",
|
||||
"@ohif/core": "3.9.0-beta.77",
|
||||
"@ohif/ui": "3.9.0-beta.77",
|
||||
@ -55,10 +55,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.82.0",
|
||||
"@cornerstonejs/core": "^1.82.0",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.82.0",
|
||||
"@cornerstonejs/tools": "^1.82.0",
|
||||
"@cornerstonejs/adapters": "^1.82.4",
|
||||
"@cornerstonejs/core": "^1.82.4",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.82.4",
|
||||
"@cornerstonejs/tools": "^1.82.4",
|
||||
"@icr/polyseg-wasm": "^0.4.0",
|
||||
"@kitware/vtk.js": "30.4.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
|
||||
@ -2,24 +2,17 @@ import React, { useEffect, useRef, useCallback, useState } from 'react';
|
||||
import { useResizeDetector } from 'react-resize-detector';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as cs3DTools from '@cornerstonejs/tools';
|
||||
import {
|
||||
Enums,
|
||||
eventTarget,
|
||||
getEnabledElement,
|
||||
StackViewport,
|
||||
utilities as csUtils,
|
||||
} from '@cornerstonejs/core';
|
||||
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
|
||||
import { MeasurementService } from '@ohif/core';
|
||||
import { Notification, useViewportDialog, AllInOneMenu } from '@ohif/ui';
|
||||
import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/dist/esm/types';
|
||||
import type { Types as csTypes } from '@cornerstonejs/core';
|
||||
|
||||
import { setEnabledElement } from '../state';
|
||||
|
||||
import './OHIFCornerstoneViewport.css';
|
||||
import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
|
||||
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||
import CinePlayer from '../components/CinePlayer';
|
||||
import { Types } from '@ohif/core';
|
||||
import type { Types } from '@ohif/core';
|
||||
|
||||
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
|
||||
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
|
||||
@ -106,6 +99,7 @@ const OHIFCornerstoneViewport = React.memo((props: withAppTypes) => {
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
onElementEnabled,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
onElementDisabled,
|
||||
@ -149,14 +143,12 @@ const OHIFCornerstoneViewport = React.memo((props: withAppTypes) => {
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const {
|
||||
measurementService,
|
||||
displaySetService,
|
||||
toolbarService,
|
||||
toolGroupService,
|
||||
syncGroupService,
|
||||
cornerstoneViewportService,
|
||||
cornerstoneCacheService,
|
||||
viewportGridService,
|
||||
stateSyncService,
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
@ -370,24 +362,12 @@ const OHIFCornerstoneViewport = React.memo((props: withAppTypes) => {
|
||||
}
|
||||
|
||||
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
|
||||
measurementService,
|
||||
displaySetService,
|
||||
elementRef,
|
||||
viewportId,
|
||||
displaySets,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
servicesManager
|
||||
);
|
||||
|
||||
_checkForCachedJumpToMeasurementEvents(
|
||||
measurementService,
|
||||
displaySetService,
|
||||
elementRef,
|
||||
viewportId,
|
||||
displaySets,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
);
|
||||
_checkForCachedJumpToMeasurementEvents(elementRef, viewportId, displaySets, servicesManager);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromJumpToMeasurementEvents();
|
||||
@ -471,15 +451,9 @@ const OHIFCornerstoneViewport = React.memo((props: withAppTypes) => {
|
||||
);
|
||||
}, areEqual);
|
||||
|
||||
function _subscribeToJumpToMeasurementEvents(
|
||||
measurementService,
|
||||
displaySetService,
|
||||
elementRef,
|
||||
viewportId,
|
||||
displaySets,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
) {
|
||||
function _subscribeToJumpToMeasurementEvents(elementRef, viewportId, servicesManager) {
|
||||
const { measurementService, cornerstoneViewportService } = servicesManager.services;
|
||||
|
||||
const { unsubscribe } = measurementService.subscribe(
|
||||
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT,
|
||||
props => {
|
||||
@ -491,27 +465,17 @@ function _subscribeToJumpToMeasurementEvents(
|
||||
if (cacheJumpToMeasurementEvent.cornerstoneViewport === undefined) {
|
||||
// Decide on which viewport should handle this
|
||||
cacheJumpToMeasurementEvent.cornerstoneViewport =
|
||||
cornerstoneViewportService.getViewportIdToJump(
|
||||
jumpId,
|
||||
measurement.displaySetInstanceUID,
|
||||
{
|
||||
referencedImageId:
|
||||
measurement.referencedImageId || measurement.metadata?.referencedImageId,
|
||||
}
|
||||
);
|
||||
cornerstoneViewportService.getViewportIdToJump(jumpId, {
|
||||
displaySetInstanceUID: measurement.displaySetInstanceUID,
|
||||
...measurement.metadata,
|
||||
referencedImageId:
|
||||
measurement.referencedImageId || measurement.metadata?.referencedImageId,
|
||||
});
|
||||
}
|
||||
if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) {
|
||||
return;
|
||||
}
|
||||
_jumpToMeasurement(
|
||||
measurement,
|
||||
elementRef,
|
||||
viewportId,
|
||||
measurementService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
);
|
||||
_jumpToMeasurement(measurement, elementRef, viewportId, servicesManager);
|
||||
}
|
||||
);
|
||||
|
||||
@ -520,13 +484,10 @@ function _subscribeToJumpToMeasurementEvents(
|
||||
|
||||
// Check if there is a queued jumpToMeasurement event
|
||||
function _checkForCachedJumpToMeasurementEvents(
|
||||
measurementService,
|
||||
displaySetService,
|
||||
elementRef,
|
||||
viewportId,
|
||||
displaySets,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
servicesManager
|
||||
) {
|
||||
if (!cacheJumpToMeasurementEvent) {
|
||||
return;
|
||||
@ -544,37 +505,15 @@ function _checkForCachedJumpToMeasurementEvents(
|
||||
const { measurement } = cacheJumpToMeasurementEvent;
|
||||
if (measurement && elementRef) {
|
||||
if (displaysUIDs.includes(measurement?.displaySetInstanceUID)) {
|
||||
_jumpToMeasurement(
|
||||
measurement,
|
||||
elementRef,
|
||||
viewportId,
|
||||
measurementService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
);
|
||||
_jumpToMeasurement(measurement, elementRef, viewportId, servicesManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _jumpToMeasurement(
|
||||
measurement,
|
||||
targetElementRef,
|
||||
viewportId,
|
||||
measurementService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
) {
|
||||
function _jumpToMeasurement(measurement, targetElementRef, viewportId, servicesManager) {
|
||||
const { viewportGridService } = servicesManager.services;
|
||||
|
||||
const targetElement = targetElementRef.current;
|
||||
const { displaySetInstanceUID, SOPInstanceUID, frameNumber } = measurement;
|
||||
|
||||
if (!SOPInstanceUID) {
|
||||
console.warn('cannot jump in a non-acquisition plane measurements yet');
|
||||
return;
|
||||
}
|
||||
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
// Todo: setCornerstoneMeasurementActive should be handled by the toolGroupManager
|
||||
// to set it properly
|
||||
@ -586,47 +525,15 @@ function _jumpToMeasurement(
|
||||
|
||||
if (enabledElement) {
|
||||
// See how the jumpToSlice() of Cornerstone3D deals with imageIdx param.
|
||||
const viewport = enabledElement.viewport as IStackViewport | IVolumeViewport;
|
||||
const viewport = enabledElement.viewport as csTypes.IStackViewport | csTypes.IVolumeViewport;
|
||||
|
||||
let imageIdIndex = 0;
|
||||
let viewportCameraDirectionMatch = true;
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
const imageIds = viewport.getImageIds();
|
||||
imageIdIndex = imageIds.findIndex(imageId => {
|
||||
const { SOPInstanceUID: aSOPInstanceUID, frameNumber: aFrameNumber } =
|
||||
getSOPInstanceAttributes(imageId);
|
||||
return aSOPInstanceUID === SOPInstanceUID && (!frameNumber || frameNumber === aFrameNumber);
|
||||
});
|
||||
} else {
|
||||
// for volume viewport we can't rely on the imageIdIndex since it can be
|
||||
// a reconstructed view that doesn't match the original slice numbers etc.
|
||||
const { viewPlaneNormal: measurementViewPlane } = measurement.metadata;
|
||||
imageIdIndex = referencedDisplaySet.images.findIndex(
|
||||
i => i.SOPInstanceUID === SOPInstanceUID
|
||||
);
|
||||
|
||||
// the index is reversed in the volume viewport
|
||||
// imageIdIndex = referencedDisplaySet.images.length - 1 - imageIdIndex;
|
||||
|
||||
const { viewPlaneNormal: viewportViewPlane } = viewport.getCamera();
|
||||
|
||||
// should compare abs for both planes since the direction can be flipped
|
||||
if (
|
||||
measurementViewPlane &&
|
||||
!csUtils.isEqual(measurementViewPlane.map(Math.abs), viewportViewPlane.map(Math.abs))
|
||||
) {
|
||||
viewportCameraDirectionMatch = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!viewportCameraDirectionMatch || imageIdIndex === -1) {
|
||||
const { metadata } = measurement;
|
||||
if (!viewport.isReferenceViewable(metadata, { withNavigation: true, withOrientation: true })) {
|
||||
console.log("Reference isn't viewable, postponing until updated");
|
||||
return;
|
||||
}
|
||||
|
||||
cs3DTools.utilities.jumpToSlice(targetElement, {
|
||||
imageIndex: imageIdIndex,
|
||||
});
|
||||
viewport.setViewReference(metadata);
|
||||
|
||||
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
|
||||
// Jump to measurement consumed, remove.
|
||||
|
||||
@ -43,67 +43,40 @@ function CornerstoneImageScrollbar({
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||
const imageIndex = viewport.getCurrentImageIdIndex();
|
||||
const imageIndex = viewport.getCurrentImageIdIndex();
|
||||
const numberOfSlices = viewport.getNumberOfSlices();
|
||||
|
||||
setImageSliceData({
|
||||
imageIndex: imageIndex,
|
||||
numberOfSlices: viewportData.data[0].imageIds.length,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
const sliceData = utilities.getImageSliceDataForVolumeViewport(
|
||||
viewport as Types.IVolumeViewport
|
||||
);
|
||||
|
||||
if (!sliceData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { imageIndex, numberOfSlices } = sliceData;
|
||||
setImageSliceData({ imageIndex, numberOfSlices });
|
||||
}
|
||||
setImageSliceData({
|
||||
imageIndex: imageIndex,
|
||||
numberOfSlices,
|
||||
});
|
||||
}, [viewportId, viewportData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewportData?.viewportType !== Enums.ViewportType.STACK) {
|
||||
if (!viewportData) {
|
||||
return;
|
||||
}
|
||||
const { viewportType } = viewportData;
|
||||
const eventId =
|
||||
(viewportType === Enums.ViewportType.STACK && Enums.Events.STACK_VIEWPORT_SCROLL) ||
|
||||
(viewportType === Enums.ViewportType.ORTHOGRAPHIC && Enums.Events.VOLUME_NEW_IMAGE) ||
|
||||
Enums.Events.IMAGE_RENDERED;
|
||||
|
||||
const updateStackIndex = event => {
|
||||
const { newImageIdIndex } = event.detail;
|
||||
const updateIndex = event => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
const { imageIndex, newImageIdIndex = imageIndex } = event.detail;
|
||||
const numberOfSlices = viewport.getNumberOfSlices();
|
||||
// find the index of imageId in the imageIds
|
||||
setImageSliceData({
|
||||
imageIndex: newImageIdIndex,
|
||||
numberOfSlices: viewportData.data[0].imageIds.length,
|
||||
numberOfSlices,
|
||||
});
|
||||
};
|
||||
|
||||
element.addEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, updateStackIndex);
|
||||
element.addEventListener(eventId, updateIndex);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, updateStackIndex);
|
||||
};
|
||||
}, [viewportData, element]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewportData?.viewportType !== Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateVolumeIndex = event => {
|
||||
const { imageIndex, numberOfSlices } = event.detail;
|
||||
// find the index of imageId in the imageIds
|
||||
setImageSliceData({ imageIndex, numberOfSlices });
|
||||
};
|
||||
|
||||
element.addEventListener(Enums.Events.VOLUME_NEW_IMAGE, updateVolumeIndex);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener(Enums.Events.VOLUME_NEW_IMAGE, updateVolumeIndex);
|
||||
element.removeEventListener(eventId, updateIndex);
|
||||
};
|
||||
}, [viewportData, element]);
|
||||
|
||||
@ -112,7 +85,7 @@ function CornerstoneImageScrollbar({
|
||||
onChange={evt => onImageScrollbarChange(evt, viewportId)}
|
||||
max={imageSliceData.numberOfSlices ? imageSliceData.numberOfSlices - 1 : 0}
|
||||
height={scrollbarHeight}
|
||||
value={imageSliceData.imageIndex}
|
||||
value={imageSliceData.imageIndex || 0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import { utilities as csUtils, Enums as csEnums } from '@cornerstonejs/core';
|
||||
import dcmjs from 'dcmjs';
|
||||
import { dicomWebUtils } from '@ohif/extension-default';
|
||||
|
||||
const { MetadataModules } = csEnums;
|
||||
const { utils } = OHIF;
|
||||
const { denaturalizeDataset } = dcmjs.data.DicomMetaDictionary;
|
||||
const { transferDenaturalizedDataset, fixMultiValueKeys } = dicomWebUtils;
|
||||
|
||||
const SOP_CLASS_UIDS = {
|
||||
VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.6',
|
||||
};
|
||||
|
||||
const SOPClassHandlerId =
|
||||
'@ohif/extension-dicom-microscopy.sopClassHandlerModule.DicomMicroscopySopClassHandler';
|
||||
'@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler';
|
||||
|
||||
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
|
||||
// If the series has no instances, stop here
|
||||
@ -27,6 +33,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
}
|
||||
}
|
||||
let imageIdForThumbnail = null;
|
||||
const dataSource = extensionManager.getActiveDataSource()[0];
|
||||
if (singleFrameInstance) {
|
||||
if (currentFrames == 1) {
|
||||
// Not all DICOM server implementations support thumbnail service,
|
||||
@ -35,7 +42,6 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
}
|
||||
if (!imageIdForThumbnail) {
|
||||
// use the thumbnail service provided by DICOM server
|
||||
const dataSource = extensionManager.getActiveDataSource()[0];
|
||||
imageIdForThumbnail = dataSource.getImageIdsForInstance({
|
||||
instance: singleFrameInstance,
|
||||
thumbnail: true,
|
||||
@ -83,6 +89,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
const displaySet = {
|
||||
plugin: 'microscopy',
|
||||
Modality: 'SM',
|
||||
viewportType: csEnums.ViewportType.WholeSlide,
|
||||
altImageText: 'Microscopy',
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
SOPInstanceUID,
|
||||
@ -102,13 +109,51 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
numInstances: 1,
|
||||
imageIdForThumbnail, // thumbnail image
|
||||
others: instances, // all other level instances in the image Pyramid
|
||||
instances,
|
||||
othersFrameOfReferenceUID,
|
||||
imageIds: instances.map(instance => instance.imageId),
|
||||
};
|
||||
// The microscopy viewer directly accesses the metadata already loaded, and
|
||||
// uses the DICOMweb client library directly for loading, so it has to be
|
||||
// provided here.
|
||||
const dicomWebClient = dataSource.retrieve.getWadoDicomWebClient?.();
|
||||
const instanceMap = new Map();
|
||||
instances.forEach(instance => instanceMap.set(instance.imageId, instance));
|
||||
if (dicomWebClient) {
|
||||
const webClient = Object.create(dicomWebClient);
|
||||
// This replaces just the dicom web metadata call with one which retrieves
|
||||
// internally.
|
||||
webClient.getDICOMwebMetadata = getDICOMwebMetadata.bind(webClient, instanceMap);
|
||||
|
||||
csUtils.genericMetadataProvider.addRaw(displaySet.imageIds[0], {
|
||||
type: MetadataModules.WADO_WEB_CLIENT,
|
||||
metadata: webClient,
|
||||
});
|
||||
} else {
|
||||
// Might have some other way of getting the data in the future or internally?
|
||||
// throw new Error('Unable to provide a DICOMWeb client library, microscopy will fail to view');
|
||||
}
|
||||
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
export default function getDicomMicroscopySopClassHandler({ servicesManager, extensionManager }) {
|
||||
/**
|
||||
* This method provides access to the internal DICOMweb metadata, used to avoid
|
||||
* refetching the DICOMweb data. It gets assigned as a member function to the
|
||||
* dicom web client.
|
||||
*/
|
||||
function getDICOMwebMetadata(instanceMap, imageId) {
|
||||
const instance = instanceMap.get(imageId);
|
||||
if (!instance) {
|
||||
console.warn('Metadata not already found for', imageId, 'in', instanceMap);
|
||||
return this.super.getDICOMwebMetadata(imageId);
|
||||
}
|
||||
return transferDenaturalizedDataset(
|
||||
denaturalizeDataset(fixMultiValueKeys(instanceMap.get(imageId)))
|
||||
);
|
||||
}
|
||||
|
||||
export function getDicomMicroscopySopClassHandler({ servicesManager, extensionManager }) {
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
};
|
||||
@ -119,3 +164,7 @@ export default function getDicomMicroscopySopClassHandler({ servicesManager, ext
|
||||
getDisplaySetsFromSeries,
|
||||
};
|
||||
}
|
||||
|
||||
export function getSopClassHandlerModule(params) {
|
||||
return [getDicomMicroscopySopClassHandler(params)];
|
||||
}
|
||||
@ -39,6 +39,7 @@ import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
|
||||
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||
import { findNearbyToolData } from './utils/findNearbyToolData';
|
||||
import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer';
|
||||
import { getSopClassHandlerModule } from './getSopClassHandlerModule';
|
||||
|
||||
const { helpers: volumeLoaderHelpers } = csStreamingImageVolumeLoader;
|
||||
const { getDynamicVolumeInfo } = volumeLoaderHelpers ?? {};
|
||||
@ -218,6 +219,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
},
|
||||
];
|
||||
},
|
||||
getSopClassHandlerModule,
|
||||
};
|
||||
|
||||
export type { PublicViewportOptions };
|
||||
|
||||
@ -32,7 +32,7 @@ class CornerstoneCacheService {
|
||||
}
|
||||
|
||||
public async createViewportData(
|
||||
displaySets: unknown[],
|
||||
displaySets: Types.DisplaySet[],
|
||||
viewportOptions: Record<string, unknown>,
|
||||
dataSource: unknown,
|
||||
initialImageIndex?: number
|
||||
@ -55,23 +55,29 @@ class CornerstoneCacheService {
|
||||
viewportOptions.viewportType = viewportType;
|
||||
}
|
||||
|
||||
const cs3DViewportType = getCornerstoneViewportType(viewportType);
|
||||
const cs3DViewportType = getCornerstoneViewportType(viewportType, displaySets);
|
||||
let viewportData: StackViewportData | VolumeViewportData;
|
||||
|
||||
if (cs3DViewportType === Enums.ViewportType.STACK) {
|
||||
viewportData = await this._getStackViewportData(
|
||||
dataSource,
|
||||
displaySets,
|
||||
initialImageIndex,
|
||||
cs3DViewportType
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC ||
|
||||
cs3DViewportType === Enums.ViewportType.VOLUME_3D
|
||||
) {
|
||||
viewportData = await this._getVolumeViewportData(dataSource, displaySets, cs3DViewportType);
|
||||
} else if (cs3DViewportType === Enums.ViewportType.STACK) {
|
||||
// Everything else looks like a stack
|
||||
viewportData = await this._getStackViewportData(
|
||||
dataSource,
|
||||
displaySets,
|
||||
initialImageIndex,
|
||||
cs3DViewportType
|
||||
);
|
||||
} else {
|
||||
viewportData = await this._getOtherViewportData(
|
||||
dataSource,
|
||||
displaySets,
|
||||
initialImageIndex,
|
||||
cs3DViewportType
|
||||
);
|
||||
}
|
||||
|
||||
viewportData.viewportType = cs3DViewportType;
|
||||
@ -143,6 +149,25 @@ class CornerstoneCacheService {
|
||||
return newViewportData;
|
||||
}
|
||||
|
||||
private async _getOtherViewportData(
|
||||
dataSource,
|
||||
displaySets,
|
||||
_initialImageIndex,
|
||||
viewportType: Enums.ViewportType
|
||||
): Promise<StackViewportData> {
|
||||
// TODO - handle overlays and secondary display sets, but for now assume
|
||||
// the 1st display set is the one of interest
|
||||
const [displaySet] = displaySets;
|
||||
if (!displaySet.imageIds) {
|
||||
displaySet.imagesIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
|
||||
}
|
||||
const { imageIds: data, viewportType: dsViewportType } = displaySet;
|
||||
return {
|
||||
viewportType: dsViewportType || viewportType,
|
||||
data: displaySets,
|
||||
};
|
||||
}
|
||||
|
||||
private async _getStackViewportData(
|
||||
dataSource,
|
||||
displaySets,
|
||||
|
||||
@ -65,6 +65,14 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
this.viewportGridResizeObserver = null;
|
||||
this.servicesManager = servicesManager;
|
||||
}
|
||||
hangingProtocolService: unknown;
|
||||
viewportsInfo: unknown;
|
||||
sceneVolumeInputs: unknown;
|
||||
viewportDivElements: unknown;
|
||||
ViewportPropertiesMap: unknown;
|
||||
volumeUIDs: unknown;
|
||||
displaySetsNeedRerendering: unknown;
|
||||
viewportDisplaySets: unknown;
|
||||
|
||||
/**
|
||||
* Adds the HTML element to the viewportService
|
||||
@ -166,17 +174,18 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
*
|
||||
* @param viewportId - The ID of the viewport.
|
||||
* @param presentations - The presentations to apply to the viewport.
|
||||
* @param viewportInfo - Contains a view reference for immediate application
|
||||
*/
|
||||
public setPresentations(viewportId: string, presentations?: Presentations): void {
|
||||
public setPresentations(
|
||||
viewportId: string,
|
||||
presentations?: Presentations,
|
||||
viewportInfo?: ViewportInfo
|
||||
): void {
|
||||
const viewport = this.getCornerstoneViewport(viewportId) as
|
||||
| Types.IStackViewport
|
||||
| Types.IVolumeViewport;
|
||||
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!presentations) {
|
||||
if (!viewport || !presentations) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -196,17 +205,12 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
}
|
||||
}
|
||||
|
||||
if (positionPresentation) {
|
||||
const { viewPlaneNormal, viewUp, zoom, pan } = positionPresentation.presentation;
|
||||
viewport.setCamera({ viewPlaneNormal, viewUp });
|
||||
|
||||
if (zoom !== undefined) {
|
||||
viewport.setZoom(zoom);
|
||||
}
|
||||
|
||||
if (pan !== undefined) {
|
||||
viewport.setPan(pan);
|
||||
}
|
||||
const viewRef = viewportInfo?.getViewReference() || positionPresentation?.viewReference;
|
||||
if (viewRef) {
|
||||
viewport.setViewReference(viewRef);
|
||||
}
|
||||
if (positionPresentation?.position) {
|
||||
viewport.setViewPresentation(positionPresentation.position);
|
||||
}
|
||||
}
|
||||
|
||||
@ -235,21 +239,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
return;
|
||||
}
|
||||
|
||||
const { viewPlaneNormal, viewUp } = csViewport.getCamera();
|
||||
const initialImageIndex = csViewport.getCurrentImageIdIndex() || 0;
|
||||
const zoom = csViewport.getZoom();
|
||||
const pan = csViewport.getPan();
|
||||
|
||||
return {
|
||||
id: positionPresentationId,
|
||||
viewportType: viewportInfo.getViewportType(),
|
||||
presentation: {
|
||||
initialImageIndex,
|
||||
viewUp,
|
||||
viewPlaneNormal,
|
||||
zoom,
|
||||
pan,
|
||||
},
|
||||
viewReference: csViewport.getViewReference(),
|
||||
position: csViewport.getViewPresentation({ pan: true, zoom: true }),
|
||||
};
|
||||
}
|
||||
|
||||
@ -428,7 +422,14 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
// override the viewportOptions and displaySetOptions with the public ones
|
||||
// since those are the newly set ones, we set them here so that it handles defaults
|
||||
const displaySetOptions = viewportInfo.setPublicDisplaySetOptions(publicDisplaySetOptions);
|
||||
const viewportOptions = viewportInfo.setPublicViewportOptions(publicViewportOptions);
|
||||
// Specify an over-ride for the viewport type, even though it is in the public
|
||||
// viewport options, because the one in the public viewport options is a suggestion
|
||||
// for initial view, whereas the one in viewportData is a requirement based on the
|
||||
// type of data being displayed.
|
||||
const viewportOptions = viewportInfo.setPublicViewportOptions(
|
||||
publicViewportOptions,
|
||||
viewportData.viewportType
|
||||
);
|
||||
|
||||
const element = viewportInfo.getElement();
|
||||
const type = viewportInfo.getViewportType();
|
||||
@ -518,35 +519,80 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
|
||||
/**
|
||||
* Looks through the viewports to see if the specified measurement can be
|
||||
* displayed in one of the viewports.
|
||||
* displayed in one of the viewports. This function tries to get a "best fit"
|
||||
* viewport to display the image in where it matches, in order:
|
||||
* * Active viewport that can be navigated to the given image without orientation change
|
||||
* * Other viewport that can be navigated to the given image without orientation change
|
||||
* * Active viewport that can change orientation to display the image
|
||||
* * Other viewport that can change orientation to display the image
|
||||
*
|
||||
* @param measurement
|
||||
* The measurement that is desired to view.
|
||||
* It returns `null` otherwise, indicating that a viewport needs display set/type
|
||||
* changes in order to display the image.
|
||||
*
|
||||
* Notes:
|
||||
* * If the display set is displayed in multiple viewports all needing orientation change,
|
||||
* then the active one or first one listed will be modified. This can create unexpected
|
||||
* behaviour for MPR views.
|
||||
* * If the image is contained in multiple display sets, then the first one
|
||||
* found will be navigated (active first, followed by first found)
|
||||
*
|
||||
* @param measurement - The measurement that is desired to view.
|
||||
* @param activeViewportId - the index that was active at the time the jump
|
||||
* was initiated.
|
||||
* @return the viewportId that the measurement should be displayed in.
|
||||
*/
|
||||
public getViewportIdToJump(
|
||||
activeViewportId: string,
|
||||
displaySetInstanceUID: string,
|
||||
cameraProps: unknown
|
||||
): string {
|
||||
const viewportInfo = this.getViewportInfo(activeViewportId);
|
||||
|
||||
if (viewportInfo.getViewportType() === csEnums.ViewportType.VOLUME_3D) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { referencedImageId } = cameraProps;
|
||||
if (viewportInfo?.contains(displaySetInstanceUID, referencedImageId)) {
|
||||
public getViewportIdToJump(activeViewportId: string, metadata): string {
|
||||
// First check if the active viewport can just be navigated to show the given item
|
||||
const activeViewport = this.getCornerstoneViewport(activeViewportId);
|
||||
if (activeViewport.isReferenceViewable(metadata, { withNavigation: true })) {
|
||||
return activeViewportId;
|
||||
}
|
||||
|
||||
return (
|
||||
[...this.viewportsById.values()].find(viewportInfo =>
|
||||
viewportInfo.contains(displaySetInstanceUID, referencedImageId)
|
||||
)?.viewportId ?? null
|
||||
);
|
||||
// Next, see if any viewport could be navigated to show the given item,
|
||||
// without considering orientation changes.
|
||||
for (const id of this.viewportsById.keys()) {
|
||||
const viewport = this.getCornerstoneViewport(id);
|
||||
if (viewport?.isReferenceViewable(metadata, { withNavigation: true })) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
// No viewport is in the right display set/orientation to show this, so see if
|
||||
// the active viewport could change orientations to show this
|
||||
if (
|
||||
activeViewport.isReferenceViewable(metadata, { withNavigation: true, withOrientation: true })
|
||||
) {
|
||||
return activeViewportId;
|
||||
}
|
||||
|
||||
// See if any viewport could show this with an orientation change
|
||||
for (const id of this.viewportsById.keys()) {
|
||||
const viewport = this.getCornerstoneViewport(id);
|
||||
if (
|
||||
viewport?.isReferenceViewable(metadata, { withNavigation: true, withOrientation: true })
|
||||
) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
// No luck, need to update the viewport itself
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the image data for the given viewport.
|
||||
*/
|
||||
private async _setOtherViewport(
|
||||
viewport: Types.IStackViewport,
|
||||
viewportData: StackViewportData,
|
||||
viewportInfo: ViewportInfo,
|
||||
_presentations: Presentations = {}
|
||||
): Promise<void> {
|
||||
const [displaySet] = viewportData.data;
|
||||
return viewport.setDataIds(displaySet.imageIds, {
|
||||
groupId: displaySet.displaySetInstanceUID,
|
||||
viewReference: viewportInfo.getViewReference(),
|
||||
});
|
||||
}
|
||||
|
||||
private async _setStackViewport(
|
||||
@ -566,6 +612,9 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
|
||||
const { initialImageIndex, imageIds } = viewportData.data[0];
|
||||
|
||||
// Use the slice index from any provided view reference, as the view reference
|
||||
// is being used to navigate to the initial view position for measurement
|
||||
// navigation and other navigation forcing specific views.
|
||||
let initialImageIndexToUse =
|
||||
presentations?.positionPresentation?.initialImageIndex ?? initialImageIndex;
|
||||
|
||||
@ -586,20 +635,15 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
properties.voiRange = { lower, upper };
|
||||
}
|
||||
|
||||
if (voiInverted !== undefined) {
|
||||
properties.invert = voiInverted;
|
||||
}
|
||||
|
||||
if (colormap !== undefined) {
|
||||
properties.colormap = colormap;
|
||||
}
|
||||
properties.invert = voiInverted ?? properties.invert;
|
||||
properties.colormap = colormap ?? properties.colormap;
|
||||
}
|
||||
|
||||
this._handleOverlays(viewport);
|
||||
|
||||
return viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
|
||||
viewport.setProperties({ ...properties });
|
||||
this.setPresentations(viewport.id, presentations);
|
||||
this.setPresentations(viewport.id, presentations, viewportInfo);
|
||||
if (displayArea) {
|
||||
viewport.setDisplayArea(displayArea);
|
||||
}
|
||||
@ -750,7 +794,6 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewport.id);
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]);
|
||||
const displaySetModality = displaySet?.Modality;
|
||||
|
||||
// Todo: use presentations states
|
||||
const volumesProperties = volumeInputArray.map((volumeInput, index) => {
|
||||
const { volumeId } = volumeInput;
|
||||
@ -786,7 +829,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
viewport.setProperties(properties, volumeId);
|
||||
});
|
||||
|
||||
this.setPresentations(viewport.id, presentations);
|
||||
this.setPresentations(viewport.id, presentations, viewportInfo);
|
||||
|
||||
this._handleOverlays(viewport);
|
||||
|
||||
@ -976,7 +1019,12 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error('Unknown viewport type');
|
||||
return this._setOtherViewport(
|
||||
viewport as Types.IViewport,
|
||||
viewportData as StackViewportData,
|
||||
viewportInfo,
|
||||
presentations
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -31,6 +31,9 @@ export type ViewportOptions = {
|
||||
displayArea?: Types.DisplayArea;
|
||||
syncGroups?: SyncGroup[];
|
||||
initialImageOptions?: InitialImageOptions;
|
||||
rotation?: number;
|
||||
flipHorizontal?: boolean;
|
||||
viewReference?: Types.ViewReference;
|
||||
customViewportProps?: Record<string, unknown>;
|
||||
/*
|
||||
* Allows drag and drop of display sets not matching viewport options, but
|
||||
@ -49,6 +52,8 @@ export type PublicViewportOptions = {
|
||||
background?: Types.Point3;
|
||||
displayArea?: Types.DisplayArea;
|
||||
syncGroups?: SyncGroup[];
|
||||
rotation?: number;
|
||||
flipHorizontal?: boolean;
|
||||
initialImageOptions?: InitialImageOptions;
|
||||
customViewportProps?: Record<string, unknown>;
|
||||
allowUnmatchedView?: boolean;
|
||||
@ -130,6 +135,7 @@ class ViewportInfo {
|
||||
private displaySetOptions: Array<DisplaySetOptions>;
|
||||
private viewportData: StackViewportData | VolumeViewportData;
|
||||
private renderingEngineId: string;
|
||||
private viewReference: Types.ViewReference;
|
||||
|
||||
constructor(viewportId: string) {
|
||||
this.viewportId = viewportId;
|
||||
@ -201,6 +207,10 @@ class ViewportInfo {
|
||||
return this.viewportId;
|
||||
}
|
||||
|
||||
public getViewReference(): Types.ViewReference {
|
||||
return this.viewportOptions?.viewReference;
|
||||
}
|
||||
|
||||
public setPublicDisplaySetOptions(
|
||||
publicDisplaySetOptions: PublicDisplaySetOptions[] | DisplaySetSelector[]
|
||||
): Array<DisplaySetOptions> {
|
||||
@ -232,21 +242,22 @@ class ViewportInfo {
|
||||
return viewportData.data.displaySetInstanceUID === displaySetInstanceUID;
|
||||
}
|
||||
|
||||
public setPublicViewportOptions(viewportOptionsEntry: PublicViewportOptions): ViewportOptions {
|
||||
let viewportType = viewportOptionsEntry.viewportType;
|
||||
const { toolGroupId = DEFAULT_TOOLGROUP_ID, presentationIds } = viewportOptionsEntry;
|
||||
let orientation;
|
||||
/**
|
||||
*
|
||||
* @param viewportOptionsEntry - the base values for the options
|
||||
* @param viewportTypeDisplaySet - allows overriding the viewport type
|
||||
*/
|
||||
public setPublicViewportOptions(
|
||||
viewportOptionsEntry: PublicViewportOptions,
|
||||
viewportTypeDisplaySet?: string
|
||||
): ViewportOptions {
|
||||
const ohifViewportType = viewportTypeDisplaySet || viewportOptionsEntry.viewportType || STACK;
|
||||
const { presentationIds } = viewportOptionsEntry;
|
||||
let { toolGroupId = DEFAULT_TOOLGROUP_ID } = viewportOptionsEntry;
|
||||
// Just assign the orientation for any viewport type and let the viewport deal with it
|
||||
const orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation);
|
||||
|
||||
if (!viewportType) {
|
||||
viewportType = getCornerstoneViewportType(STACK);
|
||||
} else {
|
||||
viewportType = getCornerstoneViewportType(viewportOptionsEntry.viewportType);
|
||||
}
|
||||
|
||||
// map SAGITTAL, AXIAL, CORONAL orientation to be used by cornerstone
|
||||
if (viewportOptionsEntry.viewportType?.toLowerCase() !== STACK) {
|
||||
orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation);
|
||||
}
|
||||
const viewportType = getCornerstoneViewportType(ohifViewportType);
|
||||
|
||||
if (!toolGroupId) {
|
||||
toolGroupId = DEFAULT_TOOLGROUP_ID;
|
||||
|
||||
@ -9,7 +9,6 @@ type StackData = {
|
||||
isCompositeStack?: boolean;
|
||||
imageIds: string[];
|
||||
frameRate?: number;
|
||||
isClip?: boolean;
|
||||
initialImageIndex?: number | string | null;
|
||||
};
|
||||
|
||||
|
||||
@ -7,13 +7,10 @@ import type { Types } from '@cornerstonejs/core';
|
||||
export type PositionPresentation = {
|
||||
id: string;
|
||||
viewportType: string;
|
||||
presentation: {
|
||||
initialImageIndex: number;
|
||||
viewUp: Types.Point3;
|
||||
viewPlaneNormal: Types.Point3;
|
||||
zoom?: number;
|
||||
pan?: Types.Point2;
|
||||
};
|
||||
// The view reference has the basic information as to what image orientation/slice is shown
|
||||
viewReference: Types.ViewReference;
|
||||
// The position information has the zoom/pan and possibly other related information, but not LUT
|
||||
position: Types.ViewPresentation;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -1,16 +1,30 @@
|
||||
import type { Types } from '@ohif/core';
|
||||
import { Enums } from '@cornerstonejs/core';
|
||||
|
||||
const STACK = 'stack';
|
||||
const VOLUME = 'volume';
|
||||
const ORTHOGRAPHIC = 'orthographic';
|
||||
const VOLUME_3D = 'volume3d';
|
||||
const VIDEO = 'video';
|
||||
const WHOLESLIDE = 'wholeslide';
|
||||
|
||||
export default function getCornerstoneViewportType(viewportType: string): Enums.ViewportType {
|
||||
const lowerViewportType = viewportType.toLowerCase();
|
||||
export default function getCornerstoneViewportType(
|
||||
viewportType: string,
|
||||
displaySets?: Types.DisplaySet[]
|
||||
): Enums.ViewportType {
|
||||
const lowerViewportType =
|
||||
displaySets?.[0]?.viewportType?.toLowerCase() || viewportType.toLowerCase();
|
||||
if (lowerViewportType === STACK) {
|
||||
return Enums.ViewportType.STACK;
|
||||
}
|
||||
|
||||
if (lowerViewportType === VIDEO) {
|
||||
return Enums.ViewportType.VIDEO;
|
||||
}
|
||||
if (lowerViewportType === WHOLESLIDE) {
|
||||
return Enums.ViewportType.WholeSlide;
|
||||
}
|
||||
|
||||
if (lowerViewportType === VOLUME || lowerViewportType === ORTHOGRAPHIC) {
|
||||
return Enums.ViewportType.ORTHOGRAPHIC;
|
||||
}
|
||||
@ -19,5 +33,7 @@ export default function getCornerstoneViewportType(viewportType: string): Enums.
|
||||
return Enums.ViewportType.VOLUME_3D;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid viewport type: ${viewportType}. Valid types are: stack, volume`);
|
||||
throw new Error(
|
||||
`Invalid viewport type: ${viewportType}. Valid types are: stack, volume, video, wholeslide`
|
||||
);
|
||||
}
|
||||
|
||||
@ -168,7 +168,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
|
||||
// Area is the same for all series
|
||||
const { angle, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
|
||||
|
||||
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
|
||||
@ -117,7 +117,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
|
||||
// Area is the same for all series
|
||||
const { SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
|
||||
|
||||
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
|
||||
@ -165,7 +165,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
|
||||
const roundedLength = utils.roundNumber(length, 2);
|
||||
const roundedWidth = utils.roundNumber(width, 2);
|
||||
|
||||
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
|
||||
@ -171,7 +171,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
|
||||
// Area is the same for all series
|
||||
const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0];
|
||||
|
||||
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
|
||||
@ -168,7 +168,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
|
||||
// Area is the same for all series
|
||||
const { angle, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
|
||||
|
||||
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
|
||||
@ -171,7 +171,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
|
||||
// Area is the same for all series
|
||||
const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0];
|
||||
|
||||
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
|
||||
@ -168,7 +168,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
|
||||
// Area is the same for all series
|
||||
const { length, SeriesNumber, SOPInstanceUID, frameNumber, unit } = mappedAnnotations[0];
|
||||
|
||||
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
|
||||
@ -132,7 +132,7 @@ function getDisplayText(annotation, displaySet, customizationService) {
|
||||
|
||||
const displayText = [];
|
||||
|
||||
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
InstanceNumber = instance.InstanceNumber;
|
||||
|
||||
@ -139,7 +139,7 @@ function getDisplayText(annotation, displaySet, customizationService, displaySet
|
||||
|
||||
const displayTextArray = [];
|
||||
|
||||
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
InstanceNumber = instance.InstanceNumber;
|
||||
|
||||
@ -163,7 +163,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
|
||||
|
||||
const { value, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
|
||||
|
||||
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
|
||||
@ -172,7 +172,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
|
||||
// Area is the same for all series
|
||||
const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0];
|
||||
|
||||
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
|
||||
@ -138,7 +138,7 @@ function getDisplayText(annotation, displaySet, customizationService) {
|
||||
|
||||
const displayTextArray = [];
|
||||
|
||||
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
InstanceNumber = instance.InstanceNumber;
|
||||
|
||||
@ -176,7 +176,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
|
||||
const { xValues, yValues, units, isUnitless, SeriesNumber, SOPInstanceUID, frameNumber } =
|
||||
mappedAnnotations[0];
|
||||
|
||||
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
|
||||
|
||||
let InstanceNumber;
|
||||
if (instance) {
|
||||
|
||||
@ -190,6 +190,15 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
|
||||
params
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Provide direct access to the dicom web client for certain use cases
|
||||
* where the dicom web client is used by an external library such as the
|
||||
* microscopy viewer.
|
||||
* Note this instance only needs to support the wado queries, and may not
|
||||
* support any QIDO or STOW operations.
|
||||
*/
|
||||
getWadoDicomWebClient: () => wadoDicomWebClient,
|
||||
|
||||
bulkDataURI: async ({ StudyInstanceUID, BulkDataURI }) => {
|
||||
qidoDicomWebClient.headers = getAuthrorizationHeader();
|
||||
const options = {
|
||||
@ -373,6 +382,32 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
|
||||
dicomWebConfig
|
||||
);
|
||||
|
||||
/**
|
||||
* Adds the retrieve bulkdata function to naturalized DICOM data.
|
||||
* This is done recursively, for sub-sequences.
|
||||
*/
|
||||
const addRetrieveBulkDataNaturalized = (naturalized, instance = naturalized) => {
|
||||
for (const key of Object.keys(naturalized)) {
|
||||
const value = naturalized[key];
|
||||
|
||||
if (Array.isArray(value) && typeof value[0] === 'object') {
|
||||
// Fix recursive values
|
||||
value.forEach(child => addRetrieveBulkDataNaturalized(child, instance));
|
||||
continue;
|
||||
}
|
||||
|
||||
// The value.Value will be set with the bulkdata read value
|
||||
// in which case it isn't necessary to re-read this.
|
||||
if (value && value.BulkDataURI && !value.Value) {
|
||||
// handle the scenarios where bulkDataURI is relative path
|
||||
fixBulkDataURI(value, instance, dicomWebConfig);
|
||||
// Provide a method to fetch bulkdata
|
||||
value.retrieveBulkData = retrieveBulkData.bind(qidoDicomWebClient, value);
|
||||
}
|
||||
}
|
||||
return naturalized;
|
||||
};
|
||||
|
||||
/**
|
||||
* naturalizes the dataset, and adds a retrieve bulkdata method
|
||||
* to any values containing BulkDataURI.
|
||||
@ -387,48 +422,7 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
|
||||
return naturalized;
|
||||
}
|
||||
|
||||
Object.keys(naturalized).forEach(key => {
|
||||
const value = naturalized[key];
|
||||
|
||||
// The value.Value will be set with the bulkdata read value
|
||||
// in which case it isn't necessary to re-read this.
|
||||
if (value && value.BulkDataURI && !value.Value) {
|
||||
// handle the scenarios where bulkDataURI is relative path
|
||||
fixBulkDataURI(value, naturalized, dicomWebConfig);
|
||||
// Provide a method to fetch bulkdata
|
||||
value.retrieveBulkData = (options = {}) => {
|
||||
|
||||
const { mediaType } = options;
|
||||
const useOptions = {
|
||||
// The bulkdata fetches work with either multipart or
|
||||
// singlepart, so set multipart to false to let the server
|
||||
// decide which type to respond with.
|
||||
multipart: false,
|
||||
BulkDataURI: value.BulkDataURI,
|
||||
// The study instance UID is required if the bulkdata uri
|
||||
// is relative - that isn't disallowed by DICOMweb, but
|
||||
// isn't well specified in the standard, but is needed in
|
||||
// any implementation that stores static copies of the metadata
|
||||
StudyInstanceUID: naturalized.StudyInstanceUID,
|
||||
mediaTypes: mediaType
|
||||
? [{ mediaType }, { mediaType: 'application/octet-stream' }]
|
||||
: undefined,
|
||||
...options,
|
||||
};
|
||||
return qidoDicomWebClient.retrieveBulkData(useOptions).then(val => {
|
||||
// There are DICOM PDF cases where the first ArrayBuffer in the array is
|
||||
// the bulk data and DICOM video cases where the second ArrayBuffer is
|
||||
// the bulk data. Here we play it safe and do a find.
|
||||
const ret =
|
||||
(val instanceof Array && val.find(arrayBuffer => arrayBuffer?.byteLength)) ||
|
||||
undefined;
|
||||
value.Value = ret;
|
||||
return ret;
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
return naturalized;
|
||||
return addRetrieveBulkDataNaturalized(naturalized);
|
||||
};
|
||||
|
||||
// Async load series, store as retrieved
|
||||
@ -558,4 +552,34 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
|
||||
return IWebApiDataSource.create(implementation);
|
||||
}
|
||||
|
||||
/**
|
||||
* A bindable function that retrieves the bulk data against this as the
|
||||
* dicomweb client, and on the given value element.
|
||||
*
|
||||
* @param value - a bind value that stores the retrieve value to short circuit the
|
||||
* next retrieve instance.
|
||||
* @param options - to allow specifying the content type.
|
||||
*/
|
||||
function retrieveBulkData(value, options = {}) {
|
||||
const { mediaType } = options;
|
||||
const useOptions = {
|
||||
// The bulkdata fetches work with either multipart or
|
||||
// singlepart, so set multipart to false to let the server
|
||||
// decide which type to respond with.
|
||||
multipart: false,
|
||||
BulkDataURI: value.BulkDataURI,
|
||||
mediaTypes: mediaType ? [{ mediaType }, { mediaType: 'application/octet-stream' }] : undefined,
|
||||
...options,
|
||||
};
|
||||
return this.retrieveBulkData(useOptions).then(val => {
|
||||
// There are DICOM PDF cases where the first ArrayBuffer in the array is
|
||||
// the bulk data and DICOM video cases where the second ArrayBuffer is
|
||||
// the bulk data. Here we play it safe and do a find.
|
||||
const ret =
|
||||
(val instanceof Array && val.find(arrayBuffer => arrayBuffer?.byteLength)) || undefined;
|
||||
value.Value = ret;
|
||||
return ret;
|
||||
});
|
||||
}
|
||||
|
||||
export { createDicomWebApi };
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
import { fixBulkDataURI } from './fixBulkDataURI';
|
||||
|
||||
function isPrimitive(v: any) {
|
||||
return !(typeof v == 'object' || Array.isArray(v));
|
||||
}
|
||||
|
||||
const vrNumerics = new Set([
|
||||
'DS',
|
||||
'FL',
|
||||
'FD',
|
||||
'IS',
|
||||
'OD',
|
||||
'OF',
|
||||
'OL',
|
||||
'OV',
|
||||
'SL',
|
||||
'SS',
|
||||
'SV',
|
||||
'UL',
|
||||
'US',
|
||||
'UV',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Specialized for DICOM JSON format dataset cleaning.
|
||||
* @param obj
|
||||
* @returns
|
||||
*/
|
||||
export function cleanDenaturalizedDataset(
|
||||
obj: any,
|
||||
options?: {
|
||||
StudyInstanceUID: string;
|
||||
SeriesInstanceUID: string;
|
||||
dataSourceConfig: unknown;
|
||||
}
|
||||
): any {
|
||||
if (Array.isArray(obj)) {
|
||||
const newAry = obj.map(o => (isPrimitive(o) ? o : cleanDenaturalizedDataset(o, options)));
|
||||
return newAry;
|
||||
}
|
||||
if (isPrimitive(obj)) {
|
||||
return obj;
|
||||
}
|
||||
Object.keys(obj).forEach(key => {
|
||||
if (obj[key].Value === null && obj[key].vr) {
|
||||
delete obj[key].Value;
|
||||
} else if (Array.isArray(obj[key].Value) && obj[key].vr) {
|
||||
if (obj[key].Value.length === 1 && obj[key].Value[0].BulkDataURI) {
|
||||
if (options?.dataSourceConfig) {
|
||||
// Not needed unless data source is directly used for loading data.
|
||||
fixBulkDataURI(obj[key].Value[0], options, options.dataSourceConfig);
|
||||
}
|
||||
|
||||
obj[key].BulkDataURI = obj[key].Value[0].BulkDataURI;
|
||||
|
||||
// prevent mixed-content blockage
|
||||
if (window.location.protocol === 'https:' && obj[key].BulkDataURI.startsWith('http:')) {
|
||||
obj[key].BulkDataURI = obj[key].BulkDataURI.replace('http:', 'https:');
|
||||
}
|
||||
delete obj[key].Value;
|
||||
} else if (vrNumerics.has(obj[key].vr)) {
|
||||
obj[key].Value = obj[key].Value.map(v => +v);
|
||||
} else {
|
||||
obj[key].Value = obj[key].Value.map(entry => cleanDenaturalizedDataset(entry, options));
|
||||
}
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is required to make the denaturalized data transferrable when it has
|
||||
* added proxy values.
|
||||
*/
|
||||
export function transferDenaturalizedDataset(dataset) {
|
||||
const noNull = cleanDenaturalizedDataset(dataset);
|
||||
return JSON.parse(JSON.stringify(noNull));
|
||||
}
|
||||
@ -23,6 +23,8 @@ function fixBulkDataURI(value, instance, dicomWebConfig) {
|
||||
let { BulkDataURI } = value;
|
||||
const { bulkDataURI: uriConfig = {} } = dicomWebConfig;
|
||||
|
||||
BulkDataURI = uriConfig.transform?.(BulkDataURI) || BulkDataURI;
|
||||
|
||||
// Handle incorrectly prefixed origins
|
||||
const { startsWith, prefixWith = '' } = uriConfig;
|
||||
if (startsWith && BulkDataURI.startsWith(startsWith)) {
|
||||
@ -36,7 +38,7 @@ function fixBulkDataURI(value, instance, dicomWebConfig) {
|
||||
if (
|
||||
BulkDataURI.startsWith('series/') ||
|
||||
BulkDataURI.startsWith('bulkdata/') ||
|
||||
(uriConfig.relativeResolution === 'studies' && isInstanceStart)
|
||||
(uriConfig.relativeResolution === 'studies' && !isInstanceStart)
|
||||
) {
|
||||
value.BulkDataURI = `${dicomWebConfig.wadoRoot}/studies/${StudyInstanceUID}/${BulkDataURI}`;
|
||||
} else if (
|
||||
@ -49,7 +51,6 @@ function fixBulkDataURI(value, instance, dicomWebConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// in case it is relative path but starts at the server (e.g., /bulk/1e, note the missing http
|
||||
// in the beginning and the first character is /) There are two scenarios, whether the wado root
|
||||
// is absolute or relative. In case of absolute, we need to prepend the wado root to the bulkdata
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Fix multi-valued keys so that those which are strings split by
|
||||
* a backslash are returned as arrays.
|
||||
*/
|
||||
export function fixMultiValueKeys(naturalData, keys = ['ImageType']) {
|
||||
for (const key of keys) {
|
||||
if (typeof naturalData[key] === 'string') {
|
||||
naturalData[key] = naturalData[key].split('\\');
|
||||
}
|
||||
}
|
||||
return naturalData;
|
||||
}
|
||||
@ -1,3 +1,9 @@
|
||||
import { fixBulkDataURI } from './fixBulkDataURI';
|
||||
import {
|
||||
cleanDenaturalizedDataset,
|
||||
transferDenaturalizedDataset,
|
||||
} from './cleanDenaturalizedDataset';
|
||||
|
||||
export { fixBulkDataURI };
|
||||
export { fixMultiValueKeys } from './fixMultiValueKeys';
|
||||
|
||||
export { fixBulkDataURI, cleanDenaturalizedDataset, transferDenaturalizedDataset };
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { isImage } from '@ohif/core/src/utils/isImage';
|
||||
import sopClassDictionary from '@ohif/core/src/utils/sopClassDictionary';
|
||||
import ImageSet from '@ohif/core/src/classes/ImageSet';
|
||||
import isDisplaySetReconstructable from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
||||
import { utils, classes } from '@ohif/core';
|
||||
import { id } from './id';
|
||||
import getDisplaySetMessages from './getDisplaySetMessages';
|
||||
import getDisplaySetsFromUnsupportedSeries from './getDisplaySetsFromUnsupportedSeries';
|
||||
import { chartHandler } from './SOPClassHandlers/chartSOPClassHandler';
|
||||
|
||||
const { isImage, sopClassDictionary, isDisplaySetReconstructable } = utils;
|
||||
const { ImageSet } = classes;
|
||||
|
||||
const DEFAULT_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
|
||||
const DYNAMIC_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingDynamicImageVolume';
|
||||
const sopClassHandlerName = 'stack';
|
||||
@ -178,7 +178,6 @@ function getDisplaySetsFromSeries(instances) {
|
||||
|
||||
displaySet.setAttributes({
|
||||
sopClassUids,
|
||||
isClip: true,
|
||||
numImageFrames: instance.NumberOfFrames,
|
||||
instanceNumber: instance.InstanceNumber,
|
||||
acquisitionDatetime: instance.AcquisitionDateTime,
|
||||
@ -254,7 +253,8 @@ const sopClassUids = [
|
||||
sopClassDictionary.OphthalmicPhotography8BitImageStorage,
|
||||
sopClassDictionary.OphthalmicPhotography16BitImageStorage,
|
||||
sopClassDictionary.OphthalmicTomographyImageStorage,
|
||||
sopClassDictionary.VLWholeSlideMicroscopyImageStorage,
|
||||
// Handled by another sop class module
|
||||
// sopClassDictionary.VLWholeSlideMicroscopyImageStorage,
|
||||
sopClassDictionary.PositronEmissionTomographyImageStorage,
|
||||
sopClassDictionary.EnhancedPETImageStorage,
|
||||
sopClassDictionary.LegacyConvertedEnhancedPETImageStorage,
|
||||
|
||||
@ -17,6 +17,7 @@ import * as dicomWebUtils from './DicomWebDataSource/utils';
|
||||
import { createReportDialogPrompt } from './Panels';
|
||||
import createReportAsync from './Actions/createReportAsync';
|
||||
import StaticWadoClient from './DicomWebDataSource/utils/StaticWadoClient';
|
||||
import { cleanDenaturalizedDataset } from './DicomWebDataSource/utils';
|
||||
|
||||
const defaultExtension: Types.Extensions.Extension = {
|
||||
/**
|
||||
@ -56,4 +57,5 @@ export {
|
||||
createReportDialogPrompt,
|
||||
createReportAsync,
|
||||
StaticWadoClient,
|
||||
cleanDenaturalizedDataset,
|
||||
};
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { LoadingIndicatorProgress } from '@ohif/ui';
|
||||
import { cleanDenaturalizedDataset } from '@ohif/extension-default';
|
||||
|
||||
import './DicomMicroscopyViewport.css';
|
||||
import ViewportOverlay from './components/ViewportOverlay';
|
||||
import getDicomWebClient from './utils/dicomWebClient';
|
||||
import dcmjs from 'dcmjs';
|
||||
import cleanDenaturalizedDataset from './utils/cleanDenaturalizedDataset';
|
||||
import MicroscopyService from './services/MicroscopyService';
|
||||
|
||||
class DicomMicroscopyViewport extends Component {
|
||||
|
||||
@ -5,7 +5,6 @@ import getCommandsModule from './getCommandsModule';
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import getDicomMicroscopySopClassHandler from './DicomMicroscopySopClassHandler';
|
||||
import getDicomMicroscopySRSopClassHandler from './DicomMicroscopySRSopClassHandler';
|
||||
import MicroscopyService from './services/MicroscopyService';
|
||||
import { useResizeDetector } from 'react-resize-detector';
|
||||
@ -153,17 +152,8 @@ const extension: Types.Extensions.Extension = {
|
||||
* Each sop class handler is defined by a { name, sopClassUids, getDisplaySetsFromSeries}.
|
||||
* Examples include the default sop class handler provided by the default extension
|
||||
*/
|
||||
getSopClassHandlerModule({ servicesManager, commandsManager, extensionManager }) {
|
||||
return [
|
||||
getDicomMicroscopySopClassHandler({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}),
|
||||
getDicomMicroscopySRSopClassHandler({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}),
|
||||
];
|
||||
getSopClassHandlerModule(params) {
|
||||
return [getDicomMicroscopySRSopClassHandler(params)];
|
||||
},
|
||||
|
||||
getPanelModule,
|
||||
|
||||
@ -1,66 +0,0 @@
|
||||
import { dicomWebUtils } from '@ohif/extension-default';
|
||||
|
||||
function isPrimitive(v: any) {
|
||||
return !(typeof v == 'object' || Array.isArray(v));
|
||||
}
|
||||
|
||||
const vrNumerics = [
|
||||
'DS',
|
||||
'FL',
|
||||
'FD',
|
||||
'IS',
|
||||
'OD',
|
||||
'OF',
|
||||
'OL',
|
||||
'OV',
|
||||
'SL',
|
||||
'SS',
|
||||
'SV',
|
||||
'UL',
|
||||
'US',
|
||||
'UV',
|
||||
];
|
||||
|
||||
/**
|
||||
* Specialized for DICOM JSON format dataset cleaning.
|
||||
* @param obj
|
||||
* @returns
|
||||
*/
|
||||
export default function cleanDenaturalizedDataset(
|
||||
obj: any,
|
||||
options: {
|
||||
StudyInstanceUID: string;
|
||||
SeriesInstanceUID: string;
|
||||
dataSourceConfig: unknown;
|
||||
}
|
||||
): any {
|
||||
if (Array.isArray(obj)) {
|
||||
const newAry = obj.map(o => (isPrimitive(o) ? o : cleanDenaturalizedDataset(o, options)));
|
||||
return newAry;
|
||||
} else if (isPrimitive(obj)) {
|
||||
return obj;
|
||||
} else {
|
||||
Object.keys(obj).forEach(key => {
|
||||
if (obj[key].Value === null && obj[key].vr) {
|
||||
delete obj[key].Value;
|
||||
} else if (Array.isArray(obj[key].Value) && obj[key].vr) {
|
||||
if (obj[key].Value.length === 1 && obj[key].Value[0].BulkDataURI) {
|
||||
dicomWebUtils.fixBulkDataURI(obj[key].Value[0], options, options.dataSourceConfig);
|
||||
|
||||
obj[key].BulkDataURI = obj[key].Value[0].BulkDataURI;
|
||||
|
||||
// prevent mixed-content blockage
|
||||
if (window.location.protocol === 'https:' && obj[key].BulkDataURI.startsWith('http:')) {
|
||||
obj[key].BulkDataURI = obj[key].BulkDataURI.replace('http:', 'https:');
|
||||
}
|
||||
delete obj[key].Value;
|
||||
} else if (vrNumerics.includes(obj[key].vr)) {
|
||||
obj[key].Value = obj[key].Value.map(v => +v);
|
||||
} else {
|
||||
obj[key].Value = obj[key].Value.map(entry => cleanDenaturalizedDataset(entry, options));
|
||||
}
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { SOPClassHandlerId } from './id';
|
||||
import { utils } from '@ohif/core';
|
||||
import { utilities as csUtils, Enums as csEnums } from '@cornerstonejs/core';
|
||||
|
||||
const SOP_CLASS_UIDS = {
|
||||
VIDEO_MICROSCOPIC_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.2.1',
|
||||
@ -50,7 +51,7 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager)
|
||||
);
|
||||
})
|
||||
.map(instance => {
|
||||
const { Modality, SOPInstanceUID, SeriesDescription = 'VIDEO' } = instance;
|
||||
const { Modality, SOPInstanceUID, SeriesDescription = 'VIDEO', imageId } = instance;
|
||||
const { SeriesNumber, SeriesDate, SeriesInstanceUID, StudyInstanceUID, NumberOfFrames, url } =
|
||||
instance;
|
||||
const videoUrl = dataSource.retrieve.directURL({
|
||||
@ -72,7 +73,10 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager)
|
||||
SOPClassHandlerId,
|
||||
referencedImages: null,
|
||||
measurements: null,
|
||||
viewportType: csEnums.ViewportType.VIDEO,
|
||||
// The videoUrl is deprecated, the preferred URL is renderedUrl
|
||||
videoUrl,
|
||||
renderedUrl: videoUrl,
|
||||
instances: [instance],
|
||||
thumbnailSrc: dataSource.retrieve.directURL({
|
||||
instance,
|
||||
@ -80,12 +84,17 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager)
|
||||
defaultType: 'image/jpeg',
|
||||
tag: 'Absent',
|
||||
}),
|
||||
imageIds: [imageId],
|
||||
isDerivedDisplaySet: true,
|
||||
isLoaded: false,
|
||||
sopClassUids,
|
||||
numImageFrames: NumberOfFrames,
|
||||
instance,
|
||||
};
|
||||
csUtils.genericMetadataProvider.add(imageId, {
|
||||
type: 'imageUrlModule',
|
||||
metadata: { rendered: videoUrl },
|
||||
});
|
||||
return displaySet;
|
||||
});
|
||||
};
|
||||
|
||||
@ -267,7 +267,7 @@ function _mapMeasurementToDisplay(measurement, types, displaySetService) {
|
||||
|
||||
const displaySets = displaySetService.getDisplaySetsForSeries(referenceSeriesUID);
|
||||
|
||||
if (!displaySets[0] || !displaySets[0].images) {
|
||||
if (!displaySets[0]?.instances) {
|
||||
throw new Error('The tracked measurements panel should only be tracking "stack" displaySets.');
|
||||
}
|
||||
|
||||
|
||||
@ -7,11 +7,13 @@ import i18n from 'i18next';
|
||||
|
||||
// Allow this mode by excluding non-imaging modalities such as SR, SEG
|
||||
// Also, SM is not a simple imaging modalities, so exclude it.
|
||||
const NON_IMAGE_MODALITIES = ['SM', 'ECG', 'SR', 'SEG'];
|
||||
const NON_IMAGE_MODALITIES = ['ECG', 'SR', 'SEG', 'RTSTRUCT'];
|
||||
|
||||
const ohif = {
|
||||
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
|
||||
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
|
||||
wsiSopClassHandler:
|
||||
'@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler',
|
||||
thumbnailList: '@ohif/extension-default.panelModule.seriesList',
|
||||
measurements: '@ohif/extension-default.panelModule.measurements',
|
||||
};
|
||||
@ -150,16 +152,20 @@ function modeFactory() {
|
||||
viewports: [
|
||||
{
|
||||
namespace: tracked.viewport,
|
||||
displaySetsToDisplay: [ohif.sopClassHandler],
|
||||
displaySetsToDisplay: [
|
||||
ohif.sopClassHandler,
|
||||
dicomvideo.sopClassHandler,
|
||||
ohif.wsiSopClassHandler,
|
||||
],
|
||||
},
|
||||
{
|
||||
namespace: dicomsr.viewport,
|
||||
displaySetsToDisplay: [dicomsr.sopClassHandler],
|
||||
},
|
||||
{
|
||||
namespace: dicomvideo.viewport,
|
||||
displaySetsToDisplay: [dicomvideo.sopClassHandler],
|
||||
},
|
||||
// {
|
||||
// namespace: dicomvideo.viewport,
|
||||
// displaySetsToDisplay: [dicomvideo.sopClassHandler],
|
||||
// },
|
||||
{
|
||||
namespace: dicompdf.viewport,
|
||||
displaySetsToDisplay: [dicompdf.sopClassHandler],
|
||||
@ -188,6 +194,7 @@ function modeFactory() {
|
||||
sopClassHandlers: [
|
||||
dicomvideo.sopClassHandler,
|
||||
dicomSeg.sopClassHandler,
|
||||
ohif.wsiSopClassHandler,
|
||||
ohif.sopClassHandler,
|
||||
dicompdf.sopClassHandler,
|
||||
dicomsr.sopClassHandler,
|
||||
|
||||
@ -196,6 +196,8 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) {
|
||||
{ toolName: toolNames.Angle },
|
||||
{ toolName: toolNames.SegmentationDisplay },
|
||||
{ toolName: toolNames.WindowLevelRegion },
|
||||
{ toolName: toolNames.PlanarFreehandROI },
|
||||
{ toolName: toolNames.SplineROI },
|
||||
],
|
||||
disabled: [
|
||||
{
|
||||
|
||||
@ -7,12 +7,15 @@ import moreTools from './moreTools';
|
||||
|
||||
// Allow this mode by excluding non-imaging modalities such as SR, SEG
|
||||
// Also, SM is not a simple imaging modalities, so exclude it.
|
||||
const NON_IMAGE_MODALITIES = ['SM', 'ECG', 'SEG', 'RTSTRUCT'];
|
||||
const NON_IMAGE_MODALITIES = ['ECG', 'SEG', 'RTSTRUCT', 'RTPLAN', 'PR'];
|
||||
|
||||
const ohif = {
|
||||
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
|
||||
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
|
||||
thumbnailList: '@ohif/extension-default.panelModule.seriesList',
|
||||
wsiSopClassHandler:
|
||||
'@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler',
|
||||
measurements: '@ohif/extension-default.panelModule.measurements',
|
||||
};
|
||||
|
||||
const tracked = {
|
||||
@ -96,7 +99,6 @@ function modeFactory({ modeConfiguration }) {
|
||||
// },
|
||||
// ]);
|
||||
|
||||
|
||||
// Init Default and SR ToolGroups
|
||||
initToolGroups(extensionManager, toolGroupService, commandsManager, this.labelConfig);
|
||||
|
||||
@ -194,16 +196,20 @@ function modeFactory({ modeConfiguration }) {
|
||||
viewports: [
|
||||
{
|
||||
namespace: tracked.viewport,
|
||||
displaySetsToDisplay: [ohif.sopClassHandler],
|
||||
displaySetsToDisplay: [
|
||||
ohif.sopClassHandler,
|
||||
dicomvideo.sopClassHandler,
|
||||
ohif.wsiSopClassHandler,
|
||||
],
|
||||
},
|
||||
{
|
||||
namespace: dicomsr.viewport,
|
||||
displaySetsToDisplay: [dicomsr.sopClassHandler],
|
||||
},
|
||||
{
|
||||
namespace: dicomvideo.viewport,
|
||||
displaySetsToDisplay: [dicomvideo.sopClassHandler],
|
||||
},
|
||||
// {
|
||||
// namespace: dicomvideo.viewport,
|
||||
// displaySetsToDisplay: [dicomvideo.sopClassHandler],
|
||||
// },
|
||||
{
|
||||
namespace: dicompdf.viewport,
|
||||
displaySetsToDisplay: [dicompdf.sopClassHandler],
|
||||
@ -238,6 +244,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
dicomSeg.sopClassHandler,
|
||||
dicomPmap.sopClassHandler,
|
||||
ohif.sopClassHandler,
|
||||
ohif.wsiSopClassHandler,
|
||||
dicompdf.sopClassHandler,
|
||||
dicomsr.sopClassHandler,
|
||||
dicomRT.sopClassHandler,
|
||||
|
||||
@ -94,7 +94,8 @@ function modeFactory({ modeConfiguration }) {
|
||||
{
|
||||
namespace: '@ohif/extension-dicom-microscopy.viewportModule.microscopy-dicom',
|
||||
displaySetsToDisplay: [
|
||||
'@ohif/extension-dicom-microscopy.sopClassHandlerModule.DicomMicroscopySopClassHandler',
|
||||
// Share the sop class handler with cornerstone version of it
|
||||
'@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler',
|
||||
'@ohif/extension-dicom-microscopy.sopClassHandlerModule.DicomMicroscopySRSopClassHandler',
|
||||
],
|
||||
},
|
||||
@ -115,12 +116,8 @@ function modeFactory({ modeConfiguration }) {
|
||||
extensions: extensionDependencies,
|
||||
hangingProtocol: ['default'],
|
||||
|
||||
// Order is important in sop class handlers when two handlers both use
|
||||
// the same sop class under different situations. In that case, the more
|
||||
// general handler needs to come last. For this case, the dicomvideo must
|
||||
// come first to remove video transfer syntax before ohif uses images
|
||||
sopClassHandlers: [
|
||||
'@ohif/extension-dicom-microscopy.sopClassHandlerModule.DicomMicroscopySopClassHandler',
|
||||
'@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler',
|
||||
'@ohif/extension-dicom-microscopy.sopClassHandlerModule.DicomMicroscopySRSopClassHandler',
|
||||
dicomvideo.sopClassHandler,
|
||||
dicompdf.sopClassHandler,
|
||||
|
||||
@ -35,6 +35,8 @@ const setHeaders = (res, path) => {
|
||||
}
|
||||
if (path.indexOf('.pdf') !== -1) {
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
} else if (path.indexOf('mp4') !== -1) {
|
||||
res.setHeader('Content-Type', 'video/mp4');
|
||||
} else if (path.indexOf('frames') !== -1) {
|
||||
res.setHeader('Content-Type', 'multipart/related');
|
||||
} else {
|
||||
|
||||
@ -42,6 +42,10 @@ describe('OHIF Measurement Panel', function () {
|
||||
});
|
||||
|
||||
it('checks if image would jump when clicked on a measurement item', function () {
|
||||
cy.get('[data-cy="study-browser-thumbnail"][data-series="1"]').dblclick();
|
||||
cy.wait(250);
|
||||
cy.scrollToIndex(0);
|
||||
|
||||
// Add length measurement
|
||||
cy.addLengthMeasurement().wait(250);
|
||||
cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').as('yesBtn').click();
|
||||
|
||||
@ -36,9 +36,9 @@ Cypress.Commands.add('openStudy', PatientName => {
|
||||
cy.openStudyList();
|
||||
cy.get('#filter-patientNameOrId').type(PatientName);
|
||||
// cy.get('@getStudies').then(() => {
|
||||
cy.waitQueryList();
|
||||
// cy.waitQueryList();
|
||||
|
||||
cy.get('[data-cy="study-list-results"]', { timeout: 5000 })
|
||||
cy.get('[data-cy="study-list-results"]', { timeout: 15000 })
|
||||
.contains(PatientName)
|
||||
.first()
|
||||
.click({ force: true });
|
||||
@ -79,8 +79,9 @@ Cypress.Commands.add(
|
||||
);
|
||||
|
||||
Cypress.Commands.add('waitQueryList', () => {
|
||||
cy.get('[data-querying="false"]');
|
||||
cy.get('[data-querying="false"]', { timeout: 15000 });
|
||||
});
|
||||
|
||||
/**
|
||||
* Command to search for a Modality and open the study.
|
||||
*
|
||||
@ -109,7 +110,7 @@ Cypress.Commands.add('openStudyList', () => {
|
||||
cy.visit('/', { timeout: 15000 });
|
||||
|
||||
// For some reason cypress 12.x does not like to stub the network request
|
||||
// so we just wait here for 1 second
|
||||
// so we just wait here for querying to be done.
|
||||
// cy.wait('@getStudies');
|
||||
cy.waitQueryList();
|
||||
});
|
||||
|
||||
@ -59,6 +59,7 @@ window.config = {
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
relativeResolution: 'studies',
|
||||
transform: url => url.replace('/pixeldata.mp4', '/rendered'),
|
||||
},
|
||||
omitQuotationForMultipartRequest: true,
|
||||
},
|
||||
|
||||
@ -44,6 +44,7 @@ window.config = {
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
relativeResolution: 'studies',
|
||||
transform: url => url.replace('/pixeldata.mp4', '/index.mp4'),
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -115,6 +116,7 @@ window.config = {
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
relativeResolution: 'studies',
|
||||
transform: url => url.replace('/pixeldata.mp4', '/rendered'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -38,6 +38,7 @@ window.config = {
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
relativeResolution: 'studies',
|
||||
transform: url => url.replace('/pixeldata.mp4', '/rendered'),
|
||||
},
|
||||
omitQuotationForMultipartRequest: true,
|
||||
},
|
||||
|
||||
@ -168,33 +168,31 @@ function ViewerViewportGrid(props: withAppTypes) {
|
||||
const { displaySetInstanceUID: referencedDisplaySetInstanceUID } = measurement;
|
||||
|
||||
const updatedViewports = _getUpdatedViewports(viewportId, referencedDisplaySetInstanceUID);
|
||||
// Arbitrarily assign the viewport to element 0
|
||||
const viewport = updatedViewports?.[0];
|
||||
|
||||
if (!viewport) {
|
||||
if (!updatedViewports[0]) {
|
||||
console.warn(
|
||||
'ViewportGrid::Unable to navigate to viewport containing',
|
||||
referencedDisplaySetInstanceUID
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Arbitrarily assign the viewport to element 0
|
||||
// TODO - this should perform a search to find the most suitable viewport.
|
||||
updatedViewports[0] = { ...updatedViewports[0] };
|
||||
const [viewport] = updatedViewports;
|
||||
|
||||
viewport.viewportOptions ||= {};
|
||||
viewport.viewportOptions.orientation = 'acquisition';
|
||||
// Copy the viewport options to prevent modifying the internal data
|
||||
viewport.viewportOptions = {
|
||||
...viewport.viewportOptions,
|
||||
orientation: 'acquisition',
|
||||
// The preferred way to jump to the measurement view is to set the
|
||||
// view reference, as this can hold information such as the orientation
|
||||
// or zoom level required to display an annotation. The metadata attribute
|
||||
// of the measurement is a viewReference, so use it to show the measurement.
|
||||
// Longer term this should clear the view reference data
|
||||
viewReference: measurement.metadata,
|
||||
viewportType: measurement.metadata.volumeId ? 'volume' : null,
|
||||
};
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetByUID(referencedDisplaySetInstanceUID);
|
||||
// jump straight to the initial image index if we can
|
||||
if (displaySet.images && measurement.SOPInstanceUID) {
|
||||
for (let index = 0; index < displaySet.images.length; index++) {
|
||||
const image = displaySet.images[index];
|
||||
if (image.SOPInstanceUID === measurement.SOPInstanceUID) {
|
||||
viewport.viewportOptions.initialImageOptions = {
|
||||
index,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
viewportGridService.setDisplaySetsForViewports(updatedViewports);
|
||||
}
|
||||
);
|
||||
@ -338,11 +336,6 @@ function ViewerViewportGrid(props: withAppTypes) {
|
||||
);
|
||||
}
|
||||
|
||||
ViewerViewportGrid.propTypes = {
|
||||
viewportComponents: PropTypes.array.isRequired,
|
||||
servicesManager: PropTypes.instanceOf(Object).isRequired,
|
||||
};
|
||||
|
||||
function _getViewportComponent(displaySets, viewportComponents, uiNotificationService) {
|
||||
if (!displaySets || !displaySets.length) {
|
||||
return EmptyViewport;
|
||||
|
||||
@ -389,6 +389,7 @@ class MetadataProvider {
|
||||
case WADO_IMAGE_LOADER_TAGS.CINE_MODULE:
|
||||
metadata = {
|
||||
frameTime: instance.FrameTime,
|
||||
numberOfFrames: instance.NumberOfFrames ? Number(instance.NumberOfFrames) : 1,
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
interface IDisplaySet {
|
||||
type DisplaySet {
|
||||
displaySetInstanceUID: string;
|
||||
StudyInstanceUID: string;
|
||||
SeriesInstanceUID?: string;
|
||||
SeriesNumber?: string;
|
||||
unsupported?: boolean;
|
||||
viewportType?: string;
|
||||
instances: any[];
|
||||
instance?: any;
|
||||
}
|
||||
|
||||
export default IDisplaySet;
|
||||
export default DisplaySet;
|
||||
@ -131,7 +131,7 @@ export default class DisplaySetService extends PubSubService {
|
||||
: [...this.getDisplaySetCache().values()];
|
||||
|
||||
const displaySet = displaySets.find(ds => {
|
||||
return ds.images && ds.images.some(i => i.SOPInstanceUID === sopInstanceUID);
|
||||
return ds.instances?.some(i => i.SOPInstanceUID === sopInstanceUID);
|
||||
});
|
||||
|
||||
return displaySet;
|
||||
|
||||
@ -4,7 +4,7 @@ import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
import sortBy from '../../utils/sortBy';
|
||||
import ProtocolEngine from './ProtocolEngine';
|
||||
import { StudyMetadata } from '../../types/StudyMetadata';
|
||||
import IDisplaySet from '../DisplaySetService/IDisplaySet';
|
||||
import DisplaySet from '../DisplaySetService/DisplaySet';
|
||||
import { CommandsManager } from '../../classes';
|
||||
import * as HangingProtocol from '../../types/HangingProtocol';
|
||||
import { isDisplaySetFromUrl, sopInstanceLocation } from './custom-attribute/isDisplaySetFromUrl';
|
||||
@ -62,7 +62,7 @@ export default class HangingProtocolService extends PubSubService {
|
||||
_servicesManager: AppTypes.ServicesManager;
|
||||
protocolEngine: ProtocolEngine;
|
||||
customViewportSettings = [];
|
||||
displaySets: IDisplaySet[] = [];
|
||||
displaySets: DisplaySet[] = [];
|
||||
activeStudy: StudyMetadata;
|
||||
debugLogging: false;
|
||||
|
||||
@ -1126,10 +1126,7 @@ export default class HangingProtocolService extends PubSubService {
|
||||
* to match display sets to viewports.
|
||||
* @returns a display set sort function
|
||||
*/
|
||||
public getDisplaySetSortFunction(): (
|
||||
displaySetA: IDisplaySet,
|
||||
displaySetB: IDisplaySet
|
||||
) => number {
|
||||
public getDisplaySetSortFunction(): (displaySetA: DisplaySet, displaySetB: DisplaySet) => number {
|
||||
return (displaySetA, displaySetB) => {
|
||||
const seriesA = this._getSeriesSortInfoForDisplaySetSort(displaySetA);
|
||||
const seriesB = this._getSeriesSortInfoForDisplaySetSort(displaySetB);
|
||||
|
||||
@ -7,6 +7,7 @@ import type {
|
||||
BaseDataSourceConfigurationAPI,
|
||||
BaseDataSourceConfigurationAPIItem,
|
||||
} from './DataSourceConfigurationAPI';
|
||||
import type DisplaySet from '../services/DisplaySetService/DisplaySet';
|
||||
|
||||
export type * from '../services/ViewportGridService';
|
||||
export type * from '../services/CustomizationService/types';
|
||||
@ -31,4 +32,5 @@ export {
|
||||
DataSourceDefinition,
|
||||
BaseDataSourceConfigurationAPI,
|
||||
BaseDataSourceConfigurationAPIItem,
|
||||
DisplaySet,
|
||||
};
|
||||
|
||||
@ -22,6 +22,7 @@ describe('Top level exports', () => {
|
||||
'imageIdToURI',
|
||||
'roundNumber',
|
||||
'b64toBlob',
|
||||
'sopClassDictionary',
|
||||
'createStudyBrowserTabs',
|
||||
'formatDate',
|
||||
'formatTime',
|
||||
|
||||
@ -39,6 +39,7 @@ import {
|
||||
import { subscribeToNextViewportGridChange } from './subscribeToNextViewportGridChange';
|
||||
import { splitComma, getSplitParam } from './splitComma';
|
||||
import { createStudyBrowserTabs } from './createStudyBrowserTabs';
|
||||
import { sopClassDictionary } from './sopClassDictionary';
|
||||
|
||||
// Commented out unused functionality.
|
||||
// Need to implement new mechanism for derived displaySets using the displaySetManager.
|
||||
@ -69,6 +70,7 @@ const utils = {
|
||||
Queue,
|
||||
isDicomUid,
|
||||
isEqualWithin,
|
||||
sopClassDictionary,
|
||||
addAccessors,
|
||||
resolveObjectPath,
|
||||
hierarchicalListUtils,
|
||||
|
||||
@ -26,6 +26,8 @@ function seriesInfoSortingCriteria(firstSeries, secondSeries) {
|
||||
const bLowPriority = isLowPriorityModality(secondSeries.Modality ?? secondSeries.modality);
|
||||
|
||||
if (aLowPriority) {
|
||||
// Use the reverse sort order for low priority modalities so that the
|
||||
// most recent one comes up first as usually that is the one of interest.
|
||||
return bLowPriority ? defaultSeriesSort(secondSeries, firstSeries) : 1;
|
||||
} else if (bLowPriority) {
|
||||
return -1;
|
||||
|
||||
@ -267,3 +267,43 @@ We have removed the Polyfill.io script from the Viewer. If you require polyfills
|
||||
## Dynamic Modules
|
||||
|
||||
TBD
|
||||
|
||||
## Renaming some interfaces
|
||||
A few interfaces are being renamed to simple types to reflect the fact that
|
||||
they don't contain methods and are thus more properly simple types.
|
||||
|
||||
* IDisplaySet renamed to DisplaySet
|
||||
* Adding some field declarations to agree with actual usage
|
||||
|
||||
|
||||
## Use of ViewReference for navigation
|
||||
When navigating to measurements and storing/remembering navigation positions,
|
||||
the `viewport.getViewReference` is used to get a position, and `viewport.isReferenceViewable`
|
||||
used to check if a reference can be applied, and finally `viewport.setViewReference` to
|
||||
navigate to a view. Note that this changes the behaviour of navigation between
|
||||
MPR and Stack viewports, and also enables navigation of video and microscopy
|
||||
viewports in CS3D. This can cause some unexpected behaviour depending on how the
|
||||
frame of reference values are configured to allow for navigation.
|
||||
|
||||
The isReferenceViewable is used to determine when a view or measurement can be
|
||||
shown on a given view. For stack versus volume viewports, this can cause unexpected
|
||||
behaviour to be seen depending on how the view reference was fetched.
|
||||
|
||||
### `getViewReference` with `forFrameOfReference`
|
||||
When a view reference is fetched with the for frame of reference flag set to true,
|
||||
a reference will be returned which can be displayed on any viewport containing
|
||||
the same frame of reference and encompassing the given FOR and able to display the required
|
||||
orientation. Without this flag, a view reference is returned which will be
|
||||
displayed on a stack with the given image id, or a volume containing said image id
|
||||
or the specified volume.
|
||||
|
||||
### `isReferenceViewable` with navigation and/or orientation
|
||||
The is reference viewable will return false unless the given reference is directly
|
||||
viewable in the viewport as is. However, it can be passed various flags to determine
|
||||
whether the reference could be displayed if the viewport was modified in various ways,
|
||||
for example, by changing the position or orientation of the viewport. This allows
|
||||
checking for degrees of closeness so that the correct viewport can be chosen.
|
||||
|
||||
Note that this may result in displaying a measurement from one viewport on a completely
|
||||
different viewport, for example, showing a Probe tool from the stack viewport on
|
||||
an MPR view.
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
import StudyItem from '../StudyItem';
|
||||
import LegacyButtonGroup from '../LegacyButtonGroup';
|
||||
@ -9,6 +10,8 @@ import ThumbnailList from '../ThumbnailList';
|
||||
import { StringNumber } from '../../types';
|
||||
import StudyBrowserSort from '../StudyBrowserSort';
|
||||
|
||||
const { sortStudySeries } = utils;
|
||||
|
||||
const getTrackedSeries = displaySets => {
|
||||
let trackedSeries = 0;
|
||||
displaySets.forEach(displaySet => {
|
||||
@ -20,7 +23,7 @@ const getTrackedSeries = displaySets => {
|
||||
return trackedSeries;
|
||||
};
|
||||
|
||||
const noop = () => {};
|
||||
const noop = () => { };
|
||||
|
||||
const StudyBrowser = ({
|
||||
tabs,
|
||||
@ -36,11 +39,14 @@ const StudyBrowser = ({
|
||||
}: withAppTypes) => {
|
||||
const { t } = useTranslation('StudyBrowser');
|
||||
const { customizationService } = servicesManager?.services || {};
|
||||
|
||||
const { experimentalStudyBrowserSort } = window.config;
|
||||
const getTabContent = () => {
|
||||
const tabData = tabs.find(tab => tab.name === activeTabName);
|
||||
return tabData.studies.map(
|
||||
({ studyInstanceUid, date, description, numInstances, modalities, displaySets }) => {
|
||||
if (!experimentalStudyBrowserSort) {
|
||||
sortStudySeries(displaySets);
|
||||
}
|
||||
const isExpanded = expandedStudyInstanceUIDs.includes(studyInstanceUid);
|
||||
return (
|
||||
<React.Fragment key={studyInstanceUid}>
|
||||
@ -112,9 +118,7 @@ const StudyBrowser = ({
|
||||
);
|
||||
})}
|
||||
</LegacyButtonGroup>
|
||||
{window.config.experimentalStudyBrowserSort && (
|
||||
<StudyBrowserSort servicesManager={servicesManager} />
|
||||
)}
|
||||
{experimentalStudyBrowserSort && <StudyBrowserSort servicesManager={servicesManager} />}
|
||||
</div>
|
||||
<div className="ohif-scrollbar invisible-scrollbar flex flex-1 flex-col overflow-auto">
|
||||
{getTabContent()}
|
||||
|
||||
@ -101,14 +101,14 @@ const Thumbnail = ({
|
||||
{loadingProgress && loadingProgress === 1 && (
|
||||
<Icon
|
||||
name={'database'}
|
||||
className="w-3"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<DisplaySetMessageListTooltip
|
||||
messages={messages}
|
||||
id={`display-set-tooltip-${displaySetInstanceUID}`}
|
||||
/>
|
||||
className="w-3"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<DisplaySetMessageListTooltip
|
||||
messages={messages}
|
||||
id={`display-set-tooltip-${displaySetInstanceUID}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="break-all text-base text-white">{description}</div>
|
||||
</div>
|
||||
|
||||
48
yarn.lock
48
yarn.lock
@ -1505,7 +1505,7 @@
|
||||
lodash.clonedeep "^4.5.0"
|
||||
ndarray "^1.0.19"
|
||||
|
||||
"@cornerstonejs/adapters@^1.81.6", "@cornerstonejs/adapters@^1.82.0":
|
||||
"@cornerstonejs/adapters@^1.81.6":
|
||||
version "1.82.0"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.82.0.tgz#4c7ae6341ad7bcf5cf0213e5252fbac009da23b6"
|
||||
integrity sha512-hcLmba3b7jC1wEorCzaS4OSFBuHz5V5I3IJW1PHbwIAWNHiuKMcC4j6ndMeQhVXHtmy56HUQP+yKAPez59k8Yw==
|
||||
@ -1518,6 +1518,19 @@
|
||||
lodash.clonedeep "^4.5.0"
|
||||
ndarray "^1.0.19"
|
||||
|
||||
"@cornerstonejs/adapters@^1.82.4":
|
||||
version "1.83.1"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.83.1.tgz#69c6bd33dd2ef3f392d4adafac89c0f50776ebf9"
|
||||
integrity sha512-AQlgSMpxNfWEKHthZp050aEI1BEhgzSbc2MmX2NAvHdQglg7YYDusFxZMKXUBB3jr1X9eTP+4VcYwzp4hPyM0Q==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs2" "^7.17.8"
|
||||
"@cornerstonejs/tools" "^1.83.1"
|
||||
buffer "^6.0.3"
|
||||
dcmjs "^0.29.8"
|
||||
gl-matrix "^3.4.3"
|
||||
lodash.clonedeep "^4.5.0"
|
||||
ndarray "^1.0.19"
|
||||
|
||||
"@cornerstonejs/calculate-suv@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/calculate-suv/-/calculate-suv-1.1.0.tgz#35c8049d3eea03611225a8398d398cd81bf0031a"
|
||||
@ -1576,6 +1589,17 @@
|
||||
gl-matrix "^3.4.3"
|
||||
lodash.clonedeep "4.5.0"
|
||||
|
||||
"@cornerstonejs/core@^1.82.4", "@cornerstonejs/core@^1.83.1":
|
||||
version "1.83.1"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.83.1.tgz#626df83e896a5e8328012c681343f1158840dfc1"
|
||||
integrity sha512-VuLjcBZ+qPeUh+8RDrma7SvDGMEA/kXb1nsf+1yQTJQsqJmNiSyV+Pb7q/bn4X270KtGrEiyCJSNkb7b2zvfhQ==
|
||||
dependencies:
|
||||
"@kitware/vtk.js" "30.4.1"
|
||||
comlink "^4.4.1"
|
||||
detect-gpu "^5.0.22"
|
||||
gl-matrix "^3.4.3"
|
||||
lodash.clonedeep "4.5.0"
|
||||
|
||||
"@cornerstonejs/dicom-image-loader@^1.77.6":
|
||||
version "1.81.3"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.81.3.tgz#8d9165b351954999bbc566d811a9eedd2363d55c"
|
||||
@ -1590,12 +1614,12 @@
|
||||
pako "^2.0.4"
|
||||
uuid "^9.0.0"
|
||||
|
||||
"@cornerstonejs/streaming-image-volume-loader@^1.82.0":
|
||||
version "1.82.0"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.82.0.tgz#a513ca57236f13af0045f2118352e1919120db1c"
|
||||
integrity sha512-BW/ncOf0ouP82o8jQkj2hhWdjqlA23mNls1gYrpsWBUnyYQOBeto2XZ2LgIOfIgvCrtbhXlE03/nkwxdvTgp3w==
|
||||
"@cornerstonejs/streaming-image-volume-loader@^1.82.4":
|
||||
version "1.83.1"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.83.1.tgz#3939d754777490b48a319f4026ec5c230e646161"
|
||||
integrity sha512-LcrrS1v5OOF2CkhcITAMKcoV/W3jQcCMzOl4oQHGtw6fJSSWpbQZjsIU94YnSUyhr/I0St3uTedNk5sNNzY66A==
|
||||
dependencies:
|
||||
"@cornerstonejs/core" "^1.82.0"
|
||||
"@cornerstonejs/core" "^1.83.1"
|
||||
comlink "^4.4.1"
|
||||
|
||||
"@cornerstonejs/tools@^1.82.0":
|
||||
@ -1610,6 +1634,18 @@
|
||||
lodash.clonedeep "4.5.0"
|
||||
lodash.get "^4.4.2"
|
||||
|
||||
"@cornerstonejs/tools@^1.82.4", "@cornerstonejs/tools@^1.83.1":
|
||||
version "1.83.1"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.83.1.tgz#56761fe339bd575b6636a4b4e12ac702ceb2a246"
|
||||
integrity sha512-KbMIFgXhuMp2zl5ORLkbJY6SwioyGlDw91cU3Hu02WR2KNTbm2b8oIzTY09F4/UzI0n19064mmP5TsPMRPsjMw==
|
||||
dependencies:
|
||||
"@cornerstonejs/core" "^1.83.1"
|
||||
"@icr/polyseg-wasm" "0.4.0"
|
||||
"@types/offscreencanvas" "2019.7.3"
|
||||
comlink "^4.4.1"
|
||||
lodash.clonedeep "4.5.0"
|
||||
lodash.get "^4.4.2"
|
||||
|
||||
"@cornerstonejs/tools@^1.83.0":
|
||||
version "1.83.0"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.83.0.tgz#6148e72972d826abfa640924f5c34f0d4acd75a3"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user