feat: Add CS3D WSI and Video Viewports and add annotation navigation for MPR (#4182)

This commit is contained in:
Bill Wallace 2024-08-15 14:36:53 -04:00 committed by GitHub
parent b12e0211d4
commit 7599ec9421
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 781 additions and 513 deletions

View File

@ -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`)

View File

@ -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
View File

@ -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"
]
}

View File

@ -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');

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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",

View File

@ -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.

View File

@ -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}
/>
);
}

View File

@ -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)];
}

View File

@ -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 };

View File

@ -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,

View File

@ -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
);
}
/**

View File

@ -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;

View File

@ -9,7 +9,6 @@ type StackData = {
isCompositeStack?: boolean;
imageIds: string[];
frameRate?: number;
isClip?: boolean;
initialImageIndex?: number | string | null;
};

View File

@ -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;
};
/**

View File

@ -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`
);
}

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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) {

View File

@ -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;

View File

@ -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) {

View File

@ -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 };

View File

@ -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));
}

View File

@ -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

View File

@ -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;
}

View File

@ -1,3 +1,9 @@
import { fixBulkDataURI } from './fixBulkDataURI';
import {
cleanDenaturalizedDataset,
transferDenaturalizedDataset,
} from './cleanDenaturalizedDataset';
export { fixBulkDataURI };
export { fixMultiValueKeys } from './fixMultiValueKeys';
export { fixBulkDataURI, cleanDenaturalizedDataset, transferDenaturalizedDataset };

View File

@ -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,

View File

@ -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,
};

View File

@ -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 {

View File

@ -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,

View File

@ -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;
}
}

View File

@ -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;
});
};

View File

@ -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.');
}

View File

@ -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,

View File

@ -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: [
{

View File

@ -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,

View File

@ -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,

View File

@ -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 {

View File

@ -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();

View File

@ -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();
});

View File

@ -59,6 +59,7 @@ window.config = {
bulkDataURI: {
enabled: true,
relativeResolution: 'studies',
transform: url => url.replace('/pixeldata.mp4', '/rendered'),
},
omitQuotationForMultipartRequest: true,
},

View File

@ -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'),
},
},
},

View File

@ -38,6 +38,7 @@ window.config = {
bulkDataURI: {
enabled: true,
relativeResolution: 'studies',
transform: url => url.replace('/pixeldata.mp4', '/rendered'),
},
omitQuotationForMultipartRequest: true,
},

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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,
};

View File

@ -22,6 +22,7 @@ describe('Top level exports', () => {
'imageIdToURI',
'roundNumber',
'b64toBlob',
'sopClassDictionary',
'createStudyBrowserTabs',
'formatDate',
'formatTime',

View File

@ -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,

View File

@ -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;

View File

@ -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.

View File

@ -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()}

View File

@ -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>

View File

@ -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"