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 - yarn-berry
type: enum type: enum
parallelism: parallelism:
default: 1 default: 4
description: | description: |
Number of Circle machines to use for load balancing, min 1 Number of Circle machines to use for load balancing, min 1
(requires `parallel` and `record` flags in your `cypress-command`) (requires `parallel` and `record` flags in your `cypress-command`)

View File

@ -1,6 +1,7 @@
{ {
"recommendations": [ "recommendations": [
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"mikestead.dotenv", "mikestead.dotenv",
"bungcip.better-toml", "bungcip.better-toml",

102
.vscode/settings.json vendored
View File

@ -1,4 +1,5 @@
{ {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.rulers": [80, 120], "editor.rulers": [80, 120],
// === // ===
// Spacing // Spacing
@ -14,23 +15,100 @@
// Event Triggers // Event Triggers
// === // ===
"editor.formatOnSave": true, "editor.formatOnSave": true,
"eslint.autoFixOnSave": true,
"eslint.run": "onSave", "eslint.run": "onSave",
"eslint.validate": [
{
"language": "javascript",
"autoFix": true
},
{
"language": "javascriptreact",
"autoFix": true
}
],
"jest.autoRun": "off", "jest.autoRun": "off",
"prettier.disableLanguages": ["html"], "prettier.disableLanguages": ["html"],
"prettier.endOfLine": "lf", "prettier.endOfLine": "lf",
"workbench.colorCustomizations": {}, "workbench.colorCustomizations": {},
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit" "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 // const vtkRules = require('vtk.js/Utilities/config/dependency.js').webpack.core
// .rules; // .rules;
// ~~ RULES // ~~ RULES
const loadShadersRule = require('./rules/loadShaders.js'); // const loadShadersRule = require('./rules/loadShaders.js');
const loadWebWorkersRule = require('./rules/loadWebWorkers.js'); const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
const transpileJavaScriptRule = require('./rules/transpileJavaScript.js'); const transpileJavaScriptRule = require('./rules/transpileJavaScript.js');
const cssToJavaScript = require('./rules/cssToJavaScript.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'); const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

View File

@ -46,9 +46,9 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.82.0", "@cornerstonejs/adapters": "^1.82.4",
"@cornerstonejs/core": "^1.82.0", "@cornerstonejs/core": "^1.82.4",
"@cornerstonejs/tools": "^1.82.0", "@cornerstonejs/tools": "^1.82.4",
"classnames": "^2.3.2" "classnames": "^2.3.2"
} }
} }

View File

@ -42,9 +42,9 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^1.82.0", "@cornerstonejs/core": "^1.82.4",
"@cornerstonejs/streaming-image-volume-loader": "^1.82.0", "@cornerstonejs/streaming-image-volume-loader": "^1.82.4",
"@cornerstonejs/tools": "^1.82.0", "@cornerstonejs/tools": "^1.82.4",
"classnames": "^2.3.2" "classnames": "^2.3.2"
} }
} }

View File

@ -38,7 +38,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.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", "@icr/polyseg-wasm": "^0.4.0",
"@ohif/core": "3.9.0-beta.77", "@ohif/core": "3.9.0-beta.77",
"@ohif/ui": "3.9.0-beta.77", "@ohif/ui": "3.9.0-beta.77",
@ -55,10 +55,10 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.82.0", "@cornerstonejs/adapters": "^1.82.4",
"@cornerstonejs/core": "^1.82.0", "@cornerstonejs/core": "^1.82.4",
"@cornerstonejs/streaming-image-volume-loader": "^1.82.0", "@cornerstonejs/streaming-image-volume-loader": "^1.82.4",
"@cornerstonejs/tools": "^1.82.0", "@cornerstonejs/tools": "^1.82.4",
"@icr/polyseg-wasm": "^0.4.0", "@icr/polyseg-wasm": "^0.4.0",
"@kitware/vtk.js": "30.4.1", "@kitware/vtk.js": "30.4.1",
"html2canvas": "^1.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 { useResizeDetector } from 'react-resize-detector';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as cs3DTools from '@cornerstonejs/tools'; import * as cs3DTools from '@cornerstonejs/tools';
import { import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
Enums,
eventTarget,
getEnabledElement,
StackViewport,
utilities as csUtils,
} from '@cornerstonejs/core';
import { MeasurementService } from '@ohif/core'; import { MeasurementService } from '@ohif/core';
import { Notification, useViewportDialog, AllInOneMenu } from '@ohif/ui'; 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 { setEnabledElement } from '../state';
import './OHIFCornerstoneViewport.css'; import './OHIFCornerstoneViewport.css';
import CornerstoneOverlays from './Overlays/CornerstoneOverlays'; import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
import CinePlayer from '../components/CinePlayer'; import CinePlayer from '../components/CinePlayer';
import { Types } from '@ohif/core'; import type { Types } from '@ohif/core';
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners'; import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu'; import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
@ -106,6 +99,7 @@ const OHIFCornerstoneViewport = React.memo((props: withAppTypes) => {
viewportOptions, viewportOptions,
displaySetOptions, displaySetOptions,
servicesManager, servicesManager,
commandsManager,
onElementEnabled, onElementEnabled,
// eslint-disable-next-line react/prop-types // eslint-disable-next-line react/prop-types
onElementDisabled, onElementDisabled,
@ -149,14 +143,12 @@ const OHIFCornerstoneViewport = React.memo((props: withAppTypes) => {
const [appConfig] = useAppConfig(); const [appConfig] = useAppConfig();
const { const {
measurementService,
displaySetService, displaySetService,
toolbarService, toolbarService,
toolGroupService, toolGroupService,
syncGroupService, syncGroupService,
cornerstoneViewportService, cornerstoneViewportService,
cornerstoneCacheService, cornerstoneCacheService,
viewportGridService,
stateSyncService, stateSyncService,
viewportActionCornersService, viewportActionCornersService,
} = servicesManager.services; } = servicesManager.services;
@ -370,24 +362,12 @@ const OHIFCornerstoneViewport = React.memo((props: withAppTypes) => {
} }
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents( const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
measurementService,
displaySetService,
elementRef, elementRef,
viewportId, viewportId,
displaySets, servicesManager
viewportGridService,
cornerstoneViewportService
); );
_checkForCachedJumpToMeasurementEvents( _checkForCachedJumpToMeasurementEvents(elementRef, viewportId, displaySets, servicesManager);
measurementService,
displaySetService,
elementRef,
viewportId,
displaySets,
viewportGridService,
cornerstoneViewportService
);
return () => { return () => {
unsubscribeFromJumpToMeasurementEvents(); unsubscribeFromJumpToMeasurementEvents();
@ -471,15 +451,9 @@ const OHIFCornerstoneViewport = React.memo((props: withAppTypes) => {
); );
}, areEqual); }, areEqual);
function _subscribeToJumpToMeasurementEvents( function _subscribeToJumpToMeasurementEvents(elementRef, viewportId, servicesManager) {
measurementService, const { measurementService, cornerstoneViewportService } = servicesManager.services;
displaySetService,
elementRef,
viewportId,
displaySets,
viewportGridService,
cornerstoneViewportService
) {
const { unsubscribe } = measurementService.subscribe( const { unsubscribe } = measurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT, MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT,
props => { props => {
@ -491,27 +465,17 @@ function _subscribeToJumpToMeasurementEvents(
if (cacheJumpToMeasurementEvent.cornerstoneViewport === undefined) { if (cacheJumpToMeasurementEvent.cornerstoneViewport === undefined) {
// Decide on which viewport should handle this // Decide on which viewport should handle this
cacheJumpToMeasurementEvent.cornerstoneViewport = cacheJumpToMeasurementEvent.cornerstoneViewport =
cornerstoneViewportService.getViewportIdToJump( cornerstoneViewportService.getViewportIdToJump(jumpId, {
jumpId, displaySetInstanceUID: measurement.displaySetInstanceUID,
measurement.displaySetInstanceUID, ...measurement.metadata,
{ referencedImageId:
referencedImageId: measurement.referencedImageId || measurement.metadata?.referencedImageId,
measurement.referencedImageId || measurement.metadata?.referencedImageId, });
}
);
} }
if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) { if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) {
return; return;
} }
_jumpToMeasurement( _jumpToMeasurement(measurement, elementRef, viewportId, servicesManager);
measurement,
elementRef,
viewportId,
measurementService,
displaySetService,
viewportGridService,
cornerstoneViewportService
);
} }
); );
@ -520,13 +484,10 @@ function _subscribeToJumpToMeasurementEvents(
// Check if there is a queued jumpToMeasurement event // Check if there is a queued jumpToMeasurement event
function _checkForCachedJumpToMeasurementEvents( function _checkForCachedJumpToMeasurementEvents(
measurementService,
displaySetService,
elementRef, elementRef,
viewportId, viewportId,
displaySets, displaySets,
viewportGridService, servicesManager
cornerstoneViewportService
) { ) {
if (!cacheJumpToMeasurementEvent) { if (!cacheJumpToMeasurementEvent) {
return; return;
@ -544,37 +505,15 @@ function _checkForCachedJumpToMeasurementEvents(
const { measurement } = cacheJumpToMeasurementEvent; const { measurement } = cacheJumpToMeasurementEvent;
if (measurement && elementRef) { if (measurement && elementRef) {
if (displaysUIDs.includes(measurement?.displaySetInstanceUID)) { if (displaysUIDs.includes(measurement?.displaySetInstanceUID)) {
_jumpToMeasurement( _jumpToMeasurement(measurement, elementRef, viewportId, servicesManager);
measurement,
elementRef,
viewportId,
measurementService,
displaySetService,
viewportGridService,
cornerstoneViewportService
);
} }
} }
} }
function _jumpToMeasurement( function _jumpToMeasurement(measurement, targetElementRef, viewportId, servicesManager) {
measurement, const { viewportGridService } = servicesManager.services;
targetElementRef,
viewportId,
measurementService,
displaySetService,
viewportGridService,
cornerstoneViewportService
) {
const targetElement = targetElementRef.current; 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 // Todo: setCornerstoneMeasurementActive should be handled by the toolGroupManager
// to set it properly // to set it properly
@ -586,47 +525,15 @@ function _jumpToMeasurement(
if (enabledElement) { if (enabledElement) {
// See how the jumpToSlice() of Cornerstone3D deals with imageIdx param. // 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; const { metadata } = measurement;
let viewportCameraDirectionMatch = true; if (!viewport.isReferenceViewable(metadata, { withNavigation: true, withOrientation: true })) {
console.log("Reference isn't viewable, postponing until updated");
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) {
return; return;
} }
cs3DTools.utilities.jumpToSlice(targetElement, { viewport.setViewReference(metadata);
imageIndex: imageIdIndex,
});
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid); cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
// Jump to measurement consumed, remove. // Jump to measurement consumed, remove.

View File

@ -43,67 +43,40 @@ function CornerstoneImageScrollbar({
return; return;
} }
if (viewportData.viewportType === Enums.ViewportType.STACK) { const imageIndex = viewport.getCurrentImageIdIndex();
const imageIndex = viewport.getCurrentImageIdIndex(); const numberOfSlices = viewport.getNumberOfSlices();
setImageSliceData({ setImageSliceData({
imageIndex: imageIndex, imageIndex: imageIndex,
numberOfSlices: viewportData.data[0].imageIds.length, numberOfSlices,
}); });
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 });
}
}, [viewportId, viewportData]); }, [viewportId, viewportData]);
useEffect(() => { useEffect(() => {
if (viewportData?.viewportType !== Enums.ViewportType.STACK) { if (!viewportData) {
return; 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 updateIndex = event => {
const { newImageIdIndex } = event.detail; const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { imageIndex, newImageIdIndex = imageIndex } = event.detail;
const numberOfSlices = viewport.getNumberOfSlices();
// find the index of imageId in the imageIds // find the index of imageId in the imageIds
setImageSliceData({ setImageSliceData({
imageIndex: newImageIdIndex, imageIndex: newImageIdIndex,
numberOfSlices: viewportData.data[0].imageIds.length, numberOfSlices,
}); });
}; };
element.addEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, updateStackIndex); element.addEventListener(eventId, updateIndex);
return () => { return () => {
element.removeEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, updateStackIndex); element.removeEventListener(eventId, updateIndex);
};
}, [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);
}; };
}, [viewportData, element]); }, [viewportData, element]);
@ -112,7 +85,7 @@ function CornerstoneImageScrollbar({
onChange={evt => onImageScrollbarChange(evt, viewportId)} onChange={evt => onImageScrollbarChange(evt, viewportId)}
max={imageSliceData.numberOfSlices ? imageSliceData.numberOfSlices - 1 : 0} max={imageSliceData.numberOfSlices ? imageSliceData.numberOfSlices - 1 : 0}
height={scrollbarHeight} height={scrollbarHeight}
value={imageSliceData.imageIndex} value={imageSliceData.imageIndex || 0}
/> />
); );
} }

View File

@ -1,13 +1,19 @@
import OHIF from '@ohif/core'; 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 { utils } = OHIF;
const { denaturalizeDataset } = dcmjs.data.DicomMetaDictionary;
const { transferDenaturalizedDataset, fixMultiValueKeys } = dicomWebUtils;
const SOP_CLASS_UIDS = { const SOP_CLASS_UIDS = {
VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.6', VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.6',
}; };
const SOPClassHandlerId = const SOPClassHandlerId =
'@ohif/extension-dicom-microscopy.sopClassHandlerModule.DicomMicroscopySopClassHandler'; '@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler';
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) { function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
// If the series has no instances, stop here // If the series has no instances, stop here
@ -27,6 +33,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
} }
} }
let imageIdForThumbnail = null; let imageIdForThumbnail = null;
const dataSource = extensionManager.getActiveDataSource()[0];
if (singleFrameInstance) { if (singleFrameInstance) {
if (currentFrames == 1) { if (currentFrames == 1) {
// Not all DICOM server implementations support thumbnail service, // Not all DICOM server implementations support thumbnail service,
@ -35,7 +42,6 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
} }
if (!imageIdForThumbnail) { if (!imageIdForThumbnail) {
// use the thumbnail service provided by DICOM server // use the thumbnail service provided by DICOM server
const dataSource = extensionManager.getActiveDataSource()[0];
imageIdForThumbnail = dataSource.getImageIdsForInstance({ imageIdForThumbnail = dataSource.getImageIdsForInstance({
instance: singleFrameInstance, instance: singleFrameInstance,
thumbnail: true, thumbnail: true,
@ -83,6 +89,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
const displaySet = { const displaySet = {
plugin: 'microscopy', plugin: 'microscopy',
Modality: 'SM', Modality: 'SM',
viewportType: csEnums.ViewportType.WholeSlide,
altImageText: 'Microscopy', altImageText: 'Microscopy',
displaySetInstanceUID: utils.guid(), displaySetInstanceUID: utils.guid(),
SOPInstanceUID, SOPInstanceUID,
@ -102,13 +109,51 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
numInstances: 1, numInstances: 1,
imageIdForThumbnail, // thumbnail image imageIdForThumbnail, // thumbnail image
others: instances, // all other level instances in the image Pyramid others: instances, // all other level instances in the image Pyramid
instances,
othersFrameOfReferenceUID, 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]; 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 => { const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
}; };
@ -119,3 +164,7 @@ export default function getDicomMicroscopySopClassHandler({ servicesManager, ext
getDisplaySetsFromSeries, 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 getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
import { findNearbyToolData } from './utils/findNearbyToolData'; import { findNearbyToolData } from './utils/findNearbyToolData';
import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer'; import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer';
import { getSopClassHandlerModule } from './getSopClassHandlerModule';
const { helpers: volumeLoaderHelpers } = csStreamingImageVolumeLoader; const { helpers: volumeLoaderHelpers } = csStreamingImageVolumeLoader;
const { getDynamicVolumeInfo } = volumeLoaderHelpers ?? {}; const { getDynamicVolumeInfo } = volumeLoaderHelpers ?? {};
@ -218,6 +219,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
}, },
]; ];
}, },
getSopClassHandlerModule,
}; };
export type { PublicViewportOptions }; export type { PublicViewportOptions };

View File

@ -32,7 +32,7 @@ class CornerstoneCacheService {
} }
public async createViewportData( public async createViewportData(
displaySets: unknown[], displaySets: Types.DisplaySet[],
viewportOptions: Record<string, unknown>, viewportOptions: Record<string, unknown>,
dataSource: unknown, dataSource: unknown,
initialImageIndex?: number initialImageIndex?: number
@ -55,23 +55,29 @@ class CornerstoneCacheService {
viewportOptions.viewportType = viewportType; viewportOptions.viewportType = viewportType;
} }
const cs3DViewportType = getCornerstoneViewportType(viewportType); const cs3DViewportType = getCornerstoneViewportType(viewportType, displaySets);
let viewportData: StackViewportData | VolumeViewportData; let viewportData: StackViewportData | VolumeViewportData;
if (cs3DViewportType === Enums.ViewportType.STACK) {
viewportData = await this._getStackViewportData(
dataSource,
displaySets,
initialImageIndex,
cs3DViewportType
);
}
if ( if (
cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC || cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC ||
cs3DViewportType === Enums.ViewportType.VOLUME_3D cs3DViewportType === Enums.ViewportType.VOLUME_3D
) { ) {
viewportData = await this._getVolumeViewportData(dataSource, displaySets, cs3DViewportType); 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; viewportData.viewportType = cs3DViewportType;
@ -143,6 +149,25 @@ class CornerstoneCacheService {
return newViewportData; 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( private async _getStackViewportData(
dataSource, dataSource,
displaySets, displaySets,

View File

@ -65,6 +65,14 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
this.viewportGridResizeObserver = null; this.viewportGridResizeObserver = null;
this.servicesManager = servicesManager; 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 * 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 viewportId - The ID of the viewport.
* @param presentations - The presentations to apply to 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 const viewport = this.getCornerstoneViewport(viewportId) as
| Types.IStackViewport | Types.IStackViewport
| Types.IVolumeViewport; | Types.IVolumeViewport;
if (!viewport) { if (!viewport || !presentations) {
return;
}
if (!presentations) {
return; return;
} }
@ -196,17 +205,12 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
} }
} }
if (positionPresentation) { const viewRef = viewportInfo?.getViewReference() || positionPresentation?.viewReference;
const { viewPlaneNormal, viewUp, zoom, pan } = positionPresentation.presentation; if (viewRef) {
viewport.setCamera({ viewPlaneNormal, viewUp }); viewport.setViewReference(viewRef);
}
if (zoom !== undefined) { if (positionPresentation?.position) {
viewport.setZoom(zoom); viewport.setViewPresentation(positionPresentation.position);
}
if (pan !== undefined) {
viewport.setPan(pan);
}
} }
} }
@ -235,21 +239,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return; return;
} }
const { viewPlaneNormal, viewUp } = csViewport.getCamera();
const initialImageIndex = csViewport.getCurrentImageIdIndex() || 0;
const zoom = csViewport.getZoom();
const pan = csViewport.getPan();
return { return {
id: positionPresentationId, id: positionPresentationId,
viewportType: viewportInfo.getViewportType(), viewportType: viewportInfo.getViewportType(),
presentation: { viewReference: csViewport.getViewReference(),
initialImageIndex, position: csViewport.getViewPresentation({ pan: true, zoom: true }),
viewUp,
viewPlaneNormal,
zoom,
pan,
},
}; };
} }
@ -428,7 +422,14 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// override the viewportOptions and displaySetOptions with the public ones // 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 // since those are the newly set ones, we set them here so that it handles defaults
const displaySetOptions = viewportInfo.setPublicDisplaySetOptions(publicDisplaySetOptions); 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 element = viewportInfo.getElement();
const type = viewportInfo.getViewportType(); 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 * 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 * It returns `null` otherwise, indicating that a viewport needs display set/type
* The measurement that is desired to view. * 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 * @param activeViewportId - the index that was active at the time the jump
* was initiated. * was initiated.
* @return the viewportId that the measurement should be displayed in. * @return the viewportId that the measurement should be displayed in.
*/ */
public getViewportIdToJump( public getViewportIdToJump(activeViewportId: string, metadata): string {
activeViewportId: string, // First check if the active viewport can just be navigated to show the given item
displaySetInstanceUID: string, const activeViewport = this.getCornerstoneViewport(activeViewportId);
cameraProps: unknown if (activeViewport.isReferenceViewable(metadata, { withNavigation: true })) {
): string {
const viewportInfo = this.getViewportInfo(activeViewportId);
if (viewportInfo.getViewportType() === csEnums.ViewportType.VOLUME_3D) {
return null;
}
const { referencedImageId } = cameraProps;
if (viewportInfo?.contains(displaySetInstanceUID, referencedImageId)) {
return activeViewportId; return activeViewportId;
} }
return ( // Next, see if any viewport could be navigated to show the given item,
[...this.viewportsById.values()].find(viewportInfo => // without considering orientation changes.
viewportInfo.contains(displaySetInstanceUID, referencedImageId) for (const id of this.viewportsById.keys()) {
)?.viewportId ?? null 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( private async _setStackViewport(
@ -566,6 +612,9 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const { initialImageIndex, imageIds } = viewportData.data[0]; 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 = let initialImageIndexToUse =
presentations?.positionPresentation?.initialImageIndex ?? initialImageIndex; presentations?.positionPresentation?.initialImageIndex ?? initialImageIndex;
@ -586,20 +635,15 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
properties.voiRange = { lower, upper }; properties.voiRange = { lower, upper };
} }
if (voiInverted !== undefined) { properties.invert = voiInverted ?? properties.invert;
properties.invert = voiInverted; properties.colormap = colormap ?? properties.colormap;
}
if (colormap !== undefined) {
properties.colormap = colormap;
}
} }
this._handleOverlays(viewport); this._handleOverlays(viewport);
return viewport.setStack(imageIds, initialImageIndexToUse).then(() => { return viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
viewport.setProperties({ ...properties }); viewport.setProperties({ ...properties });
this.setPresentations(viewport.id, presentations); this.setPresentations(viewport.id, presentations, viewportInfo);
if (displayArea) { if (displayArea) {
viewport.setDisplayArea(displayArea); viewport.setDisplayArea(displayArea);
} }
@ -750,7 +794,6 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewport.id); const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewport.id);
const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]); const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]);
const displaySetModality = displaySet?.Modality; const displaySetModality = displaySet?.Modality;
// Todo: use presentations states // Todo: use presentations states
const volumesProperties = volumeInputArray.map((volumeInput, index) => { const volumesProperties = volumeInputArray.map((volumeInput, index) => {
const { volumeId } = volumeInput; const { volumeId } = volumeInput;
@ -786,7 +829,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
viewport.setProperties(properties, volumeId); viewport.setProperties(properties, volumeId);
}); });
this.setPresentations(viewport.id, presentations); this.setPresentations(viewport.id, presentations, viewportInfo);
this._handleOverlays(viewport); 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; displayArea?: Types.DisplayArea;
syncGroups?: SyncGroup[]; syncGroups?: SyncGroup[];
initialImageOptions?: InitialImageOptions; initialImageOptions?: InitialImageOptions;
rotation?: number;
flipHorizontal?: boolean;
viewReference?: Types.ViewReference;
customViewportProps?: Record<string, unknown>; customViewportProps?: Record<string, unknown>;
/* /*
* Allows drag and drop of display sets not matching viewport options, but * Allows drag and drop of display sets not matching viewport options, but
@ -49,6 +52,8 @@ export type PublicViewportOptions = {
background?: Types.Point3; background?: Types.Point3;
displayArea?: Types.DisplayArea; displayArea?: Types.DisplayArea;
syncGroups?: SyncGroup[]; syncGroups?: SyncGroup[];
rotation?: number;
flipHorizontal?: boolean;
initialImageOptions?: InitialImageOptions; initialImageOptions?: InitialImageOptions;
customViewportProps?: Record<string, unknown>; customViewportProps?: Record<string, unknown>;
allowUnmatchedView?: boolean; allowUnmatchedView?: boolean;
@ -130,6 +135,7 @@ class ViewportInfo {
private displaySetOptions: Array<DisplaySetOptions>; private displaySetOptions: Array<DisplaySetOptions>;
private viewportData: StackViewportData | VolumeViewportData; private viewportData: StackViewportData | VolumeViewportData;
private renderingEngineId: string; private renderingEngineId: string;
private viewReference: Types.ViewReference;
constructor(viewportId: string) { constructor(viewportId: string) {
this.viewportId = viewportId; this.viewportId = viewportId;
@ -201,6 +207,10 @@ class ViewportInfo {
return this.viewportId; return this.viewportId;
} }
public getViewReference(): Types.ViewReference {
return this.viewportOptions?.viewReference;
}
public setPublicDisplaySetOptions( public setPublicDisplaySetOptions(
publicDisplaySetOptions: PublicDisplaySetOptions[] | DisplaySetSelector[] publicDisplaySetOptions: PublicDisplaySetOptions[] | DisplaySetSelector[]
): Array<DisplaySetOptions> { ): Array<DisplaySetOptions> {
@ -232,21 +242,22 @@ class ViewportInfo {
return viewportData.data.displaySetInstanceUID === displaySetInstanceUID; return viewportData.data.displaySetInstanceUID === displaySetInstanceUID;
} }
public setPublicViewportOptions(viewportOptionsEntry: PublicViewportOptions): ViewportOptions { /**
let viewportType = viewportOptionsEntry.viewportType; *
const { toolGroupId = DEFAULT_TOOLGROUP_ID, presentationIds } = viewportOptionsEntry; * @param viewportOptionsEntry - the base values for the options
let orientation; * @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) { const viewportType = getCornerstoneViewportType(ohifViewportType);
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);
}
if (!toolGroupId) { if (!toolGroupId) {
toolGroupId = DEFAULT_TOOLGROUP_ID; toolGroupId = DEFAULT_TOOLGROUP_ID;

View File

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

View File

@ -7,13 +7,10 @@ import type { Types } from '@cornerstonejs/core';
export type PositionPresentation = { export type PositionPresentation = {
id: string; id: string;
viewportType: string; viewportType: string;
presentation: { // The view reference has the basic information as to what image orientation/slice is shown
initialImageIndex: number; viewReference: Types.ViewReference;
viewUp: Types.Point3; // The position information has the zoom/pan and possibly other related information, but not LUT
viewPlaneNormal: Types.Point3; position: Types.ViewPresentation;
zoom?: number;
pan?: Types.Point2;
};
}; };
/** /**

View File

@ -1,16 +1,30 @@
import type { Types } from '@ohif/core';
import { Enums } from '@cornerstonejs/core'; import { Enums } from '@cornerstonejs/core';
const STACK = 'stack'; const STACK = 'stack';
const VOLUME = 'volume'; const VOLUME = 'volume';
const ORTHOGRAPHIC = 'orthographic'; const ORTHOGRAPHIC = 'orthographic';
const VOLUME_3D = 'volume3d'; const VOLUME_3D = 'volume3d';
const VIDEO = 'video';
const WHOLESLIDE = 'wholeslide';
export default function getCornerstoneViewportType(viewportType: string): Enums.ViewportType { export default function getCornerstoneViewportType(
const lowerViewportType = viewportType.toLowerCase(); viewportType: string,
displaySets?: Types.DisplaySet[]
): Enums.ViewportType {
const lowerViewportType =
displaySets?.[0]?.viewportType?.toLowerCase() || viewportType.toLowerCase();
if (lowerViewportType === STACK) { if (lowerViewportType === STACK) {
return Enums.ViewportType.STACK; return Enums.ViewportType.STACK;
} }
if (lowerViewportType === VIDEO) {
return Enums.ViewportType.VIDEO;
}
if (lowerViewportType === WHOLESLIDE) {
return Enums.ViewportType.WholeSlide;
}
if (lowerViewportType === VOLUME || lowerViewportType === ORTHOGRAPHIC) { if (lowerViewportType === VOLUME || lowerViewportType === ORTHOGRAPHIC) {
return Enums.ViewportType.ORTHOGRAPHIC; return Enums.ViewportType.ORTHOGRAPHIC;
} }
@ -19,5 +33,7 @@ export default function getCornerstoneViewportType(viewportType: string): Enums.
return Enums.ViewportType.VOLUME_3D; 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 // Area is the same for all series
const { angle, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; 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; let InstanceNumber;
if (instance) { if (instance) {

View File

@ -117,7 +117,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
// Area is the same for all series // Area is the same for all series
const { SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; 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; let InstanceNumber;
if (instance) { if (instance) {

View File

@ -165,7 +165,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
const roundedLength = utils.roundNumber(length, 2); const roundedLength = utils.roundNumber(length, 2);
const roundedWidth = utils.roundNumber(width, 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; let InstanceNumber;
if (instance) { if (instance) {

View File

@ -171,7 +171,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
// Area is the same for all series // Area is the same for all series
const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0]; 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; let InstanceNumber;
if (instance) { if (instance) {

View File

@ -168,7 +168,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
// Area is the same for all series // Area is the same for all series
const { angle, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; 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; let InstanceNumber;
if (instance) { if (instance) {

View File

@ -171,7 +171,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
// Area is the same for all series // Area is the same for all series
const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0]; 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; let InstanceNumber;
if (instance) { if (instance) {

View File

@ -168,7 +168,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
// Area is the same for all series // Area is the same for all series
const { length, SeriesNumber, SOPInstanceUID, frameNumber, unit } = mappedAnnotations[0]; 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; let InstanceNumber;
if (instance) { if (instance) {

View File

@ -132,7 +132,7 @@ function getDisplayText(annotation, displaySet, customizationService) {
const displayText = []; const displayText = [];
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID); const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
let InstanceNumber; let InstanceNumber;
if (instance) { if (instance) {
InstanceNumber = instance.InstanceNumber; InstanceNumber = instance.InstanceNumber;

View File

@ -139,7 +139,7 @@ function getDisplayText(annotation, displaySet, customizationService, displaySet
const displayTextArray = []; const displayTextArray = [];
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID); const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
let InstanceNumber; let InstanceNumber;
if (instance) { if (instance) {
InstanceNumber = instance.InstanceNumber; InstanceNumber = instance.InstanceNumber;

View File

@ -163,7 +163,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
const { value, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; 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; let InstanceNumber;
if (instance) { if (instance) {

View File

@ -172,7 +172,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
// Area is the same for all series // Area is the same for all series
const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0]; 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; let InstanceNumber;
if (instance) { if (instance) {

View File

@ -138,7 +138,7 @@ function getDisplayText(annotation, displaySet, customizationService) {
const displayTextArray = []; const displayTextArray = [];
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID); const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
let InstanceNumber; let InstanceNumber;
if (instance) { if (instance) {
InstanceNumber = instance.InstanceNumber; InstanceNumber = instance.InstanceNumber;

View File

@ -176,7 +176,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
const { xValues, yValues, units, isUnitless, SeriesNumber, SOPInstanceUID, frameNumber } = const { xValues, yValues, units, isUnitless, SeriesNumber, SOPInstanceUID, frameNumber } =
mappedAnnotations[0]; mappedAnnotations[0];
const instance = displaySet.images.find(image => image.SOPInstanceUID === SOPInstanceUID); const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
let InstanceNumber; let InstanceNumber;
if (instance) { if (instance) {

View File

@ -190,6 +190,15 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
params 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 }) => { bulkDataURI: async ({ StudyInstanceUID, BulkDataURI }) => {
qidoDicomWebClient.headers = getAuthrorizationHeader(); qidoDicomWebClient.headers = getAuthrorizationHeader();
const options = { const options = {
@ -373,6 +382,32 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
dicomWebConfig 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 * naturalizes the dataset, and adds a retrieve bulkdata method
* to any values containing BulkDataURI. * to any values containing BulkDataURI.
@ -387,48 +422,7 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
return naturalized; return naturalized;
} }
Object.keys(naturalized).forEach(key => { return addRetrieveBulkDataNaturalized(naturalized);
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;
}; };
// Async load series, store as retrieved // Async load series, store as retrieved
@ -558,4 +552,34 @@ function createDicomWebApi(dicomWebConfig, servicesManager) {
return IWebApiDataSource.create(implementation); 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 }; 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; let { BulkDataURI } = value;
const { bulkDataURI: uriConfig = {} } = dicomWebConfig; const { bulkDataURI: uriConfig = {} } = dicomWebConfig;
BulkDataURI = uriConfig.transform?.(BulkDataURI) || BulkDataURI;
// Handle incorrectly prefixed origins // Handle incorrectly prefixed origins
const { startsWith, prefixWith = '' } = uriConfig; const { startsWith, prefixWith = '' } = uriConfig;
if (startsWith && BulkDataURI.startsWith(startsWith)) { if (startsWith && BulkDataURI.startsWith(startsWith)) {
@ -36,7 +38,7 @@ function fixBulkDataURI(value, instance, dicomWebConfig) {
if ( if (
BulkDataURI.startsWith('series/') || BulkDataURI.startsWith('series/') ||
BulkDataURI.startsWith('bulkdata/') || BulkDataURI.startsWith('bulkdata/') ||
(uriConfig.relativeResolution === 'studies' && isInstanceStart) (uriConfig.relativeResolution === 'studies' && !isInstanceStart)
) { ) {
value.BulkDataURI = `${dicomWebConfig.wadoRoot}/studies/${StudyInstanceUID}/${BulkDataURI}`; value.BulkDataURI = `${dicomWebConfig.wadoRoot}/studies/${StudyInstanceUID}/${BulkDataURI}`;
} else if ( } else if (
@ -49,7 +51,6 @@ function fixBulkDataURI(value, instance, dicomWebConfig) {
return; return;
} }
// in case it is relative path but starts at the server (e.g., /bulk/1e, note the missing http // 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 // 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 // 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 { 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 { utils, classes } from '@ohif/core';
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 { id } from './id'; import { id } from './id';
import getDisplaySetMessages from './getDisplaySetMessages'; import getDisplaySetMessages from './getDisplaySetMessages';
import getDisplaySetsFromUnsupportedSeries from './getDisplaySetsFromUnsupportedSeries'; import getDisplaySetsFromUnsupportedSeries from './getDisplaySetsFromUnsupportedSeries';
import { chartHandler } from './SOPClassHandlers/chartSOPClassHandler'; import { chartHandler } from './SOPClassHandlers/chartSOPClassHandler';
const { isImage, sopClassDictionary, isDisplaySetReconstructable } = utils;
const { ImageSet } = classes;
const DEFAULT_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume'; const DEFAULT_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
const DYNAMIC_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingDynamicImageVolume'; const DYNAMIC_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingDynamicImageVolume';
const sopClassHandlerName = 'stack'; const sopClassHandlerName = 'stack';
@ -178,7 +178,6 @@ function getDisplaySetsFromSeries(instances) {
displaySet.setAttributes({ displaySet.setAttributes({
sopClassUids, sopClassUids,
isClip: true,
numImageFrames: instance.NumberOfFrames, numImageFrames: instance.NumberOfFrames,
instanceNumber: instance.InstanceNumber, instanceNumber: instance.InstanceNumber,
acquisitionDatetime: instance.AcquisitionDateTime, acquisitionDatetime: instance.AcquisitionDateTime,
@ -254,7 +253,8 @@ const sopClassUids = [
sopClassDictionary.OphthalmicPhotography8BitImageStorage, sopClassDictionary.OphthalmicPhotography8BitImageStorage,
sopClassDictionary.OphthalmicPhotography16BitImageStorage, sopClassDictionary.OphthalmicPhotography16BitImageStorage,
sopClassDictionary.OphthalmicTomographyImageStorage, sopClassDictionary.OphthalmicTomographyImageStorage,
sopClassDictionary.VLWholeSlideMicroscopyImageStorage, // Handled by another sop class module
// sopClassDictionary.VLWholeSlideMicroscopyImageStorage,
sopClassDictionary.PositronEmissionTomographyImageStorage, sopClassDictionary.PositronEmissionTomographyImageStorage,
sopClassDictionary.EnhancedPETImageStorage, sopClassDictionary.EnhancedPETImageStorage,
sopClassDictionary.LegacyConvertedEnhancedPETImageStorage, sopClassDictionary.LegacyConvertedEnhancedPETImageStorage,

View File

@ -17,6 +17,7 @@ import * as dicomWebUtils from './DicomWebDataSource/utils';
import { createReportDialogPrompt } from './Panels'; import { createReportDialogPrompt } from './Panels';
import createReportAsync from './Actions/createReportAsync'; import createReportAsync from './Actions/createReportAsync';
import StaticWadoClient from './DicomWebDataSource/utils/StaticWadoClient'; import StaticWadoClient from './DicomWebDataSource/utils/StaticWadoClient';
import { cleanDenaturalizedDataset } from './DicomWebDataSource/utils';
const defaultExtension: Types.Extensions.Extension = { const defaultExtension: Types.Extensions.Extension = {
/** /**
@ -56,4 +57,5 @@ export {
createReportDialogPrompt, createReportDialogPrompt,
createReportAsync, createReportAsync,
StaticWadoClient, StaticWadoClient,
cleanDenaturalizedDataset,
}; };

View File

@ -1,12 +1,12 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { LoadingIndicatorProgress } from '@ohif/ui'; import { LoadingIndicatorProgress } from '@ohif/ui';
import { cleanDenaturalizedDataset } from '@ohif/extension-default';
import './DicomMicroscopyViewport.css'; import './DicomMicroscopyViewport.css';
import ViewportOverlay from './components/ViewportOverlay'; import ViewportOverlay from './components/ViewportOverlay';
import getDicomWebClient from './utils/dicomWebClient'; import getDicomWebClient from './utils/dicomWebClient';
import dcmjs from 'dcmjs'; import dcmjs from 'dcmjs';
import cleanDenaturalizedDataset from './utils/cleanDenaturalizedDataset';
import MicroscopyService from './services/MicroscopyService'; import MicroscopyService from './services/MicroscopyService';
class DicomMicroscopyViewport extends Component { class DicomMicroscopyViewport extends Component {

View File

@ -5,7 +5,6 @@ import getCommandsModule from './getCommandsModule';
import { Types } from '@ohif/core'; import { Types } from '@ohif/core';
import { useViewportGrid } from '@ohif/ui'; import { useViewportGrid } from '@ohif/ui';
import getDicomMicroscopySopClassHandler from './DicomMicroscopySopClassHandler';
import getDicomMicroscopySRSopClassHandler from './DicomMicroscopySRSopClassHandler'; import getDicomMicroscopySRSopClassHandler from './DicomMicroscopySRSopClassHandler';
import MicroscopyService from './services/MicroscopyService'; import MicroscopyService from './services/MicroscopyService';
import { useResizeDetector } from 'react-resize-detector'; 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}. * Each sop class handler is defined by a { name, sopClassUids, getDisplaySetsFromSeries}.
* Examples include the default sop class handler provided by the default extension * Examples include the default sop class handler provided by the default extension
*/ */
getSopClassHandlerModule({ servicesManager, commandsManager, extensionManager }) { getSopClassHandlerModule(params) {
return [ return [getDicomMicroscopySRSopClassHandler(params)];
getDicomMicroscopySopClassHandler({
servicesManager,
extensionManager,
}),
getDicomMicroscopySRSopClassHandler({
servicesManager,
extensionManager,
}),
];
}, },
getPanelModule, 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 { SOPClassHandlerId } from './id';
import { utils } from '@ohif/core'; import { utils } from '@ohif/core';
import { utilities as csUtils, Enums as csEnums } from '@cornerstonejs/core';
const SOP_CLASS_UIDS = { const SOP_CLASS_UIDS = {
VIDEO_MICROSCOPIC_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.2.1', 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 => { .map(instance => {
const { Modality, SOPInstanceUID, SeriesDescription = 'VIDEO' } = instance; const { Modality, SOPInstanceUID, SeriesDescription = 'VIDEO', imageId } = instance;
const { SeriesNumber, SeriesDate, SeriesInstanceUID, StudyInstanceUID, NumberOfFrames, url } = const { SeriesNumber, SeriesDate, SeriesInstanceUID, StudyInstanceUID, NumberOfFrames, url } =
instance; instance;
const videoUrl = dataSource.retrieve.directURL({ const videoUrl = dataSource.retrieve.directURL({
@ -72,7 +73,10 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager)
SOPClassHandlerId, SOPClassHandlerId,
referencedImages: null, referencedImages: null,
measurements: null, measurements: null,
viewportType: csEnums.ViewportType.VIDEO,
// The videoUrl is deprecated, the preferred URL is renderedUrl
videoUrl, videoUrl,
renderedUrl: videoUrl,
instances: [instance], instances: [instance],
thumbnailSrc: dataSource.retrieve.directURL({ thumbnailSrc: dataSource.retrieve.directURL({
instance, instance,
@ -80,12 +84,17 @@ const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager)
defaultType: 'image/jpeg', defaultType: 'image/jpeg',
tag: 'Absent', tag: 'Absent',
}), }),
imageIds: [imageId],
isDerivedDisplaySet: true, isDerivedDisplaySet: true,
isLoaded: false, isLoaded: false,
sopClassUids, sopClassUids,
numImageFrames: NumberOfFrames, numImageFrames: NumberOfFrames,
instance, instance,
}; };
csUtils.genericMetadataProvider.add(imageId, {
type: 'imageUrlModule',
metadata: { rendered: videoUrl },
});
return displaySet; return displaySet;
}); });
}; };

View File

@ -267,7 +267,7 @@ function _mapMeasurementToDisplay(measurement, types, displaySetService) {
const displaySets = displaySetService.getDisplaySetsForSeries(referenceSeriesUID); 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.'); 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 // Allow this mode by excluding non-imaging modalities such as SR, SEG
// Also, SM is not a simple imaging modalities, so exclude it. // 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 = { const ohif = {
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
wsiSopClassHandler:
'@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler',
thumbnailList: '@ohif/extension-default.panelModule.seriesList', thumbnailList: '@ohif/extension-default.panelModule.seriesList',
measurements: '@ohif/extension-default.panelModule.measurements', measurements: '@ohif/extension-default.panelModule.measurements',
}; };
@ -150,16 +152,20 @@ function modeFactory() {
viewports: [ viewports: [
{ {
namespace: tracked.viewport, namespace: tracked.viewport,
displaySetsToDisplay: [ohif.sopClassHandler], displaySetsToDisplay: [
ohif.sopClassHandler,
dicomvideo.sopClassHandler,
ohif.wsiSopClassHandler,
],
}, },
{ {
namespace: dicomsr.viewport, namespace: dicomsr.viewport,
displaySetsToDisplay: [dicomsr.sopClassHandler], displaySetsToDisplay: [dicomsr.sopClassHandler],
}, },
{ // {
namespace: dicomvideo.viewport, // namespace: dicomvideo.viewport,
displaySetsToDisplay: [dicomvideo.sopClassHandler], // displaySetsToDisplay: [dicomvideo.sopClassHandler],
}, // },
{ {
namespace: dicompdf.viewport, namespace: dicompdf.viewport,
displaySetsToDisplay: [dicompdf.sopClassHandler], displaySetsToDisplay: [dicompdf.sopClassHandler],
@ -188,6 +194,7 @@ function modeFactory() {
sopClassHandlers: [ sopClassHandlers: [
dicomvideo.sopClassHandler, dicomvideo.sopClassHandler,
dicomSeg.sopClassHandler, dicomSeg.sopClassHandler,
ohif.wsiSopClassHandler,
ohif.sopClassHandler, ohif.sopClassHandler,
dicompdf.sopClassHandler, dicompdf.sopClassHandler,
dicomsr.sopClassHandler, dicomsr.sopClassHandler,

View File

@ -196,6 +196,8 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) {
{ toolName: toolNames.Angle }, { toolName: toolNames.Angle },
{ toolName: toolNames.SegmentationDisplay }, { toolName: toolNames.SegmentationDisplay },
{ toolName: toolNames.WindowLevelRegion }, { toolName: toolNames.WindowLevelRegion },
{ toolName: toolNames.PlanarFreehandROI },
{ toolName: toolNames.SplineROI },
], ],
disabled: [ disabled: [
{ {

View File

@ -7,12 +7,15 @@ import moreTools from './moreTools';
// Allow this mode by excluding non-imaging modalities such as SR, SEG // Allow this mode by excluding non-imaging modalities such as SR, SEG
// Also, SM is not a simple imaging modalities, so exclude it. // 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 = { const ohif = {
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
thumbnailList: '@ohif/extension-default.panelModule.seriesList', thumbnailList: '@ohif/extension-default.panelModule.seriesList',
wsiSopClassHandler:
'@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler',
measurements: '@ohif/extension-default.panelModule.measurements',
}; };
const tracked = { const tracked = {
@ -96,7 +99,6 @@ function modeFactory({ modeConfiguration }) {
// }, // },
// ]); // ]);
// Init Default and SR ToolGroups // Init Default and SR ToolGroups
initToolGroups(extensionManager, toolGroupService, commandsManager, this.labelConfig); initToolGroups(extensionManager, toolGroupService, commandsManager, this.labelConfig);
@ -194,16 +196,20 @@ function modeFactory({ modeConfiguration }) {
viewports: [ viewports: [
{ {
namespace: tracked.viewport, namespace: tracked.viewport,
displaySetsToDisplay: [ohif.sopClassHandler], displaySetsToDisplay: [
ohif.sopClassHandler,
dicomvideo.sopClassHandler,
ohif.wsiSopClassHandler,
],
}, },
{ {
namespace: dicomsr.viewport, namespace: dicomsr.viewport,
displaySetsToDisplay: [dicomsr.sopClassHandler], displaySetsToDisplay: [dicomsr.sopClassHandler],
}, },
{ // {
namespace: dicomvideo.viewport, // namespace: dicomvideo.viewport,
displaySetsToDisplay: [dicomvideo.sopClassHandler], // displaySetsToDisplay: [dicomvideo.sopClassHandler],
}, // },
{ {
namespace: dicompdf.viewport, namespace: dicompdf.viewport,
displaySetsToDisplay: [dicompdf.sopClassHandler], displaySetsToDisplay: [dicompdf.sopClassHandler],
@ -238,6 +244,7 @@ function modeFactory({ modeConfiguration }) {
dicomSeg.sopClassHandler, dicomSeg.sopClassHandler,
dicomPmap.sopClassHandler, dicomPmap.sopClassHandler,
ohif.sopClassHandler, ohif.sopClassHandler,
ohif.wsiSopClassHandler,
dicompdf.sopClassHandler, dicompdf.sopClassHandler,
dicomsr.sopClassHandler, dicomsr.sopClassHandler,
dicomRT.sopClassHandler, dicomRT.sopClassHandler,

View File

@ -94,7 +94,8 @@ function modeFactory({ modeConfiguration }) {
{ {
namespace: '@ohif/extension-dicom-microscopy.viewportModule.microscopy-dicom', namespace: '@ohif/extension-dicom-microscopy.viewportModule.microscopy-dicom',
displaySetsToDisplay: [ 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', '@ohif/extension-dicom-microscopy.sopClassHandlerModule.DicomMicroscopySRSopClassHandler',
], ],
}, },
@ -115,12 +116,8 @@ function modeFactory({ modeConfiguration }) {
extensions: extensionDependencies, extensions: extensionDependencies,
hangingProtocol: ['default'], 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: [ sopClassHandlers: [
'@ohif/extension-dicom-microscopy.sopClassHandlerModule.DicomMicroscopySopClassHandler', '@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler',
'@ohif/extension-dicom-microscopy.sopClassHandlerModule.DicomMicroscopySRSopClassHandler', '@ohif/extension-dicom-microscopy.sopClassHandlerModule.DicomMicroscopySRSopClassHandler',
dicomvideo.sopClassHandler, dicomvideo.sopClassHandler,
dicompdf.sopClassHandler, dicompdf.sopClassHandler,

View File

@ -35,6 +35,8 @@ const setHeaders = (res, path) => {
} }
if (path.indexOf('.pdf') !== -1) { if (path.indexOf('.pdf') !== -1) {
res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Type', 'application/pdf');
} else if (path.indexOf('mp4') !== -1) {
res.setHeader('Content-Type', 'video/mp4');
} else if (path.indexOf('frames') !== -1) { } else if (path.indexOf('frames') !== -1) {
res.setHeader('Content-Type', 'multipart/related'); res.setHeader('Content-Type', 'multipart/related');
} else { } 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 () { 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 // Add length measurement
cy.addLengthMeasurement().wait(250); cy.addLengthMeasurement().wait(250);
cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').as('yesBtn').click(); 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.openStudyList();
cy.get('#filter-patientNameOrId').type(PatientName); cy.get('#filter-patientNameOrId').type(PatientName);
// cy.get('@getStudies').then(() => { // 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) .contains(PatientName)
.first() .first()
.click({ force: true }); .click({ force: true });
@ -79,8 +79,9 @@ Cypress.Commands.add(
); );
Cypress.Commands.add('waitQueryList', () => { 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. * Command to search for a Modality and open the study.
* *
@ -109,7 +110,7 @@ Cypress.Commands.add('openStudyList', () => {
cy.visit('/', { timeout: 15000 }); cy.visit('/', { timeout: 15000 });
// For some reason cypress 12.x does not like to stub the network request // 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.wait('@getStudies');
cy.waitQueryList(); cy.waitQueryList();
}); });

View File

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

View File

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

View File

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

View File

@ -168,33 +168,31 @@ function ViewerViewportGrid(props: withAppTypes) {
const { displaySetInstanceUID: referencedDisplaySetInstanceUID } = measurement; const { displaySetInstanceUID: referencedDisplaySetInstanceUID } = measurement;
const updatedViewports = _getUpdatedViewports(viewportId, referencedDisplaySetInstanceUID); const updatedViewports = _getUpdatedViewports(viewportId, referencedDisplaySetInstanceUID);
// Arbitrarily assign the viewport to element 0 if (!updatedViewports[0]) {
const viewport = updatedViewports?.[0];
if (!viewport) {
console.warn( console.warn(
'ViewportGrid::Unable to navigate to viewport containing', 'ViewportGrid::Unable to navigate to viewport containing',
referencedDisplaySetInstanceUID referencedDisplaySetInstanceUID
); );
return; 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 ||= {}; // Copy the viewport options to prevent modifying the internal data
viewport.viewportOptions.orientation = 'acquisition'; 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); 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) { function _getViewportComponent(displaySets, viewportComponents, uiNotificationService) {
if (!displaySets || !displaySets.length) { if (!displaySets || !displaySets.length) {
return EmptyViewport; return EmptyViewport;

View File

@ -389,6 +389,7 @@ class MetadataProvider {
case WADO_IMAGE_LOADER_TAGS.CINE_MODULE: case WADO_IMAGE_LOADER_TAGS.CINE_MODULE:
metadata = { metadata = {
frameTime: instance.FrameTime, frameTime: instance.FrameTime,
numberOfFrames: instance.NumberOfFrames ? Number(instance.NumberOfFrames) : 1,
}; };
break; break;

View File

@ -1,9 +1,12 @@
interface IDisplaySet { type DisplaySet {
displaySetInstanceUID: string; displaySetInstanceUID: string;
StudyInstanceUID: string; StudyInstanceUID: string;
SeriesInstanceUID?: string; SeriesInstanceUID?: string;
SeriesNumber?: string; SeriesNumber?: string;
unsupported?: boolean; 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()]; : [...this.getDisplaySetCache().values()];
const displaySet = displaySets.find(ds => { 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; return displaySet;

View File

@ -4,7 +4,7 @@ import { PubSubService } from '../_shared/pubSubServiceInterface';
import sortBy from '../../utils/sortBy'; import sortBy from '../../utils/sortBy';
import ProtocolEngine from './ProtocolEngine'; import ProtocolEngine from './ProtocolEngine';
import { StudyMetadata } from '../../types/StudyMetadata'; import { StudyMetadata } from '../../types/StudyMetadata';
import IDisplaySet from '../DisplaySetService/IDisplaySet'; import DisplaySet from '../DisplaySetService/DisplaySet';
import { CommandsManager } from '../../classes'; import { CommandsManager } from '../../classes';
import * as HangingProtocol from '../../types/HangingProtocol'; import * as HangingProtocol from '../../types/HangingProtocol';
import { isDisplaySetFromUrl, sopInstanceLocation } from './custom-attribute/isDisplaySetFromUrl'; import { isDisplaySetFromUrl, sopInstanceLocation } from './custom-attribute/isDisplaySetFromUrl';
@ -62,7 +62,7 @@ export default class HangingProtocolService extends PubSubService {
_servicesManager: AppTypes.ServicesManager; _servicesManager: AppTypes.ServicesManager;
protocolEngine: ProtocolEngine; protocolEngine: ProtocolEngine;
customViewportSettings = []; customViewportSettings = [];
displaySets: IDisplaySet[] = []; displaySets: DisplaySet[] = [];
activeStudy: StudyMetadata; activeStudy: StudyMetadata;
debugLogging: false; debugLogging: false;
@ -1126,10 +1126,7 @@ export default class HangingProtocolService extends PubSubService {
* to match display sets to viewports. * to match display sets to viewports.
* @returns a display set sort function * @returns a display set sort function
*/ */
public getDisplaySetSortFunction(): ( public getDisplaySetSortFunction(): (displaySetA: DisplaySet, displaySetB: DisplaySet) => number {
displaySetA: IDisplaySet,
displaySetB: IDisplaySet
) => number {
return (displaySetA, displaySetB) => { return (displaySetA, displaySetB) => {
const seriesA = this._getSeriesSortInfoForDisplaySetSort(displaySetA); const seriesA = this._getSeriesSortInfoForDisplaySetSort(displaySetA);
const seriesB = this._getSeriesSortInfoForDisplaySetSort(displaySetB); const seriesB = this._getSeriesSortInfoForDisplaySetSort(displaySetB);

View File

@ -7,6 +7,7 @@ import type {
BaseDataSourceConfigurationAPI, BaseDataSourceConfigurationAPI,
BaseDataSourceConfigurationAPIItem, BaseDataSourceConfigurationAPIItem,
} from './DataSourceConfigurationAPI'; } from './DataSourceConfigurationAPI';
import type DisplaySet from '../services/DisplaySetService/DisplaySet';
export type * from '../services/ViewportGridService'; export type * from '../services/ViewportGridService';
export type * from '../services/CustomizationService/types'; export type * from '../services/CustomizationService/types';
@ -31,4 +32,5 @@ export {
DataSourceDefinition, DataSourceDefinition,
BaseDataSourceConfigurationAPI, BaseDataSourceConfigurationAPI,
BaseDataSourceConfigurationAPIItem, BaseDataSourceConfigurationAPIItem,
DisplaySet,
}; };

View File

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

View File

@ -39,6 +39,7 @@ import {
import { subscribeToNextViewportGridChange } from './subscribeToNextViewportGridChange'; import { subscribeToNextViewportGridChange } from './subscribeToNextViewportGridChange';
import { splitComma, getSplitParam } from './splitComma'; import { splitComma, getSplitParam } from './splitComma';
import { createStudyBrowserTabs } from './createStudyBrowserTabs'; import { createStudyBrowserTabs } from './createStudyBrowserTabs';
import { sopClassDictionary } from './sopClassDictionary';
// Commented out unused functionality. // Commented out unused functionality.
// Need to implement new mechanism for derived displaySets using the displaySetManager. // Need to implement new mechanism for derived displaySets using the displaySetManager.
@ -69,6 +70,7 @@ const utils = {
Queue, Queue,
isDicomUid, isDicomUid,
isEqualWithin, isEqualWithin,
sopClassDictionary,
addAccessors, addAccessors,
resolveObjectPath, resolveObjectPath,
hierarchicalListUtils, hierarchicalListUtils,

View File

@ -26,6 +26,8 @@ function seriesInfoSortingCriteria(firstSeries, secondSeries) {
const bLowPriority = isLowPriorityModality(secondSeries.Modality ?? secondSeries.modality); const bLowPriority = isLowPriorityModality(secondSeries.Modality ?? secondSeries.modality);
if (aLowPriority) { 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; return bLowPriority ? defaultSeriesSort(secondSeries, firstSeries) : 1;
} else if (bLowPriority) { } else if (bLowPriority) {
return -1; return -1;

View File

@ -267,3 +267,43 @@ We have removed the Polyfill.io script from the Viewer. If you require polyfills
## Dynamic Modules ## Dynamic Modules
TBD 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 React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { utils } from '@ohif/core';
import StudyItem from '../StudyItem'; import StudyItem from '../StudyItem';
import LegacyButtonGroup from '../LegacyButtonGroup'; import LegacyButtonGroup from '../LegacyButtonGroup';
@ -9,6 +10,8 @@ import ThumbnailList from '../ThumbnailList';
import { StringNumber } from '../../types'; import { StringNumber } from '../../types';
import StudyBrowserSort from '../StudyBrowserSort'; import StudyBrowserSort from '../StudyBrowserSort';
const { sortStudySeries } = utils;
const getTrackedSeries = displaySets => { const getTrackedSeries = displaySets => {
let trackedSeries = 0; let trackedSeries = 0;
displaySets.forEach(displaySet => { displaySets.forEach(displaySet => {
@ -20,7 +23,7 @@ const getTrackedSeries = displaySets => {
return trackedSeries; return trackedSeries;
}; };
const noop = () => {}; const noop = () => { };
const StudyBrowser = ({ const StudyBrowser = ({
tabs, tabs,
@ -36,11 +39,14 @@ const StudyBrowser = ({
}: withAppTypes) => { }: withAppTypes) => {
const { t } = useTranslation('StudyBrowser'); const { t } = useTranslation('StudyBrowser');
const { customizationService } = servicesManager?.services || {}; const { customizationService } = servicesManager?.services || {};
const { experimentalStudyBrowserSort } = window.config;
const getTabContent = () => { const getTabContent = () => {
const tabData = tabs.find(tab => tab.name === activeTabName); const tabData = tabs.find(tab => tab.name === activeTabName);
return tabData.studies.map( return tabData.studies.map(
({ studyInstanceUid, date, description, numInstances, modalities, displaySets }) => { ({ studyInstanceUid, date, description, numInstances, modalities, displaySets }) => {
if (!experimentalStudyBrowserSort) {
sortStudySeries(displaySets);
}
const isExpanded = expandedStudyInstanceUIDs.includes(studyInstanceUid); const isExpanded = expandedStudyInstanceUIDs.includes(studyInstanceUid);
return ( return (
<React.Fragment key={studyInstanceUid}> <React.Fragment key={studyInstanceUid}>
@ -112,9 +118,7 @@ const StudyBrowser = ({
); );
})} })}
</LegacyButtonGroup> </LegacyButtonGroup>
{window.config.experimentalStudyBrowserSort && ( {experimentalStudyBrowserSort && <StudyBrowserSort servicesManager={servicesManager} />}
<StudyBrowserSort servicesManager={servicesManager} />
)}
</div> </div>
<div className="ohif-scrollbar invisible-scrollbar flex flex-1 flex-col overflow-auto"> <div className="ohif-scrollbar invisible-scrollbar flex flex-1 flex-col overflow-auto">
{getTabContent()} {getTabContent()}

View File

@ -101,14 +101,14 @@ const Thumbnail = ({
{loadingProgress && loadingProgress === 1 && ( {loadingProgress && loadingProgress === 1 && (
<Icon <Icon
name={'database'} name={'database'}
className="w-3" className="w-3"
/> />
)} )}
</div> </div>
<DisplaySetMessageListTooltip <DisplaySetMessageListTooltip
messages={messages} messages={messages}
id={`display-set-tooltip-${displaySetInstanceUID}`} id={`display-set-tooltip-${displaySetInstanceUID}`}
/> />
</div> </div>
<div className="break-all text-base text-white">{description}</div> <div className="break-all text-base text-white">{description}</div>
</div> </div>

View File

@ -1505,7 +1505,7 @@
lodash.clonedeep "^4.5.0" lodash.clonedeep "^4.5.0"
ndarray "^1.0.19" ndarray "^1.0.19"
"@cornerstonejs/adapters@^1.81.6", "@cornerstonejs/adapters@^1.82.0": "@cornerstonejs/adapters@^1.81.6":
version "1.82.0" version "1.82.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.82.0.tgz#4c7ae6341ad7bcf5cf0213e5252fbac009da23b6" resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.82.0.tgz#4c7ae6341ad7bcf5cf0213e5252fbac009da23b6"
integrity sha512-hcLmba3b7jC1wEorCzaS4OSFBuHz5V5I3IJW1PHbwIAWNHiuKMcC4j6ndMeQhVXHtmy56HUQP+yKAPez59k8Yw== integrity sha512-hcLmba3b7jC1wEorCzaS4OSFBuHz5V5I3IJW1PHbwIAWNHiuKMcC4j6ndMeQhVXHtmy56HUQP+yKAPez59k8Yw==
@ -1518,6 +1518,19 @@
lodash.clonedeep "^4.5.0" lodash.clonedeep "^4.5.0"
ndarray "^1.0.19" 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": "@cornerstonejs/calculate-suv@^1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/calculate-suv/-/calculate-suv-1.1.0.tgz#35c8049d3eea03611225a8398d398cd81bf0031a" resolved "https://registry.yarnpkg.com/@cornerstonejs/calculate-suv/-/calculate-suv-1.1.0.tgz#35c8049d3eea03611225a8398d398cd81bf0031a"
@ -1576,6 +1589,17 @@
gl-matrix "^3.4.3" gl-matrix "^3.4.3"
lodash.clonedeep "4.5.0" 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": "@cornerstonejs/dicom-image-loader@^1.77.6":
version "1.81.3" version "1.81.3"
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.81.3.tgz#8d9165b351954999bbc566d811a9eedd2363d55c" 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" pako "^2.0.4"
uuid "^9.0.0" uuid "^9.0.0"
"@cornerstonejs/streaming-image-volume-loader@^1.82.0": "@cornerstonejs/streaming-image-volume-loader@^1.82.4":
version "1.82.0" version "1.83.1"
resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.82.0.tgz#a513ca57236f13af0045f2118352e1919120db1c" resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.83.1.tgz#3939d754777490b48a319f4026ec5c230e646161"
integrity sha512-BW/ncOf0ouP82o8jQkj2hhWdjqlA23mNls1gYrpsWBUnyYQOBeto2XZ2LgIOfIgvCrtbhXlE03/nkwxdvTgp3w== integrity sha512-LcrrS1v5OOF2CkhcITAMKcoV/W3jQcCMzOl4oQHGtw6fJSSWpbQZjsIU94YnSUyhr/I0St3uTedNk5sNNzY66A==
dependencies: dependencies:
"@cornerstonejs/core" "^1.82.0" "@cornerstonejs/core" "^1.83.1"
comlink "^4.4.1" comlink "^4.4.1"
"@cornerstonejs/tools@^1.82.0": "@cornerstonejs/tools@^1.82.0":
@ -1610,6 +1634,18 @@
lodash.clonedeep "4.5.0" lodash.clonedeep "4.5.0"
lodash.get "^4.4.2" 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": "@cornerstonejs/tools@^1.83.0":
version "1.83.0" version "1.83.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.83.0.tgz#6148e72972d826abfa640924f5c34f0d4acd75a3" resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.83.0.tgz#6148e72972d826abfa640924f5c34f0d4acd75a3"