feat: Add CS3D WSI and Video Viewports and add annotation navigation for MPR (#4182)
This commit is contained in:
parent
b12e0211d4
commit
7599ec9421
@ -385,7 +385,7 @@ jobs:
|
|||||||
- yarn-berry
|
- 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`)
|
||||||
|
|||||||
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@ -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
102
.vscode/settings.json
vendored
@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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');
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)];
|
||||||
|
}
|
||||||
@ -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 };
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
import { fixBulkDataURI } from './fixBulkDataURI';
|
||||||
|
|
||||||
|
function isPrimitive(v: any) {
|
||||||
|
return !(typeof v == 'object' || Array.isArray(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
const vrNumerics = new Set([
|
||||||
|
'DS',
|
||||||
|
'FL',
|
||||||
|
'FD',
|
||||||
|
'IS',
|
||||||
|
'OD',
|
||||||
|
'OF',
|
||||||
|
'OL',
|
||||||
|
'OV',
|
||||||
|
'SL',
|
||||||
|
'SS',
|
||||||
|
'SV',
|
||||||
|
'UL',
|
||||||
|
'US',
|
||||||
|
'UV',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specialized for DICOM JSON format dataset cleaning.
|
||||||
|
* @param obj
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function cleanDenaturalizedDataset(
|
||||||
|
obj: any,
|
||||||
|
options?: {
|
||||||
|
StudyInstanceUID: string;
|
||||||
|
SeriesInstanceUID: string;
|
||||||
|
dataSourceConfig: unknown;
|
||||||
|
}
|
||||||
|
): any {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
const newAry = obj.map(o => (isPrimitive(o) ? o : cleanDenaturalizedDataset(o, options)));
|
||||||
|
return newAry;
|
||||||
|
}
|
||||||
|
if (isPrimitive(obj)) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
Object.keys(obj).forEach(key => {
|
||||||
|
if (obj[key].Value === null && obj[key].vr) {
|
||||||
|
delete obj[key].Value;
|
||||||
|
} else if (Array.isArray(obj[key].Value) && obj[key].vr) {
|
||||||
|
if (obj[key].Value.length === 1 && obj[key].Value[0].BulkDataURI) {
|
||||||
|
if (options?.dataSourceConfig) {
|
||||||
|
// Not needed unless data source is directly used for loading data.
|
||||||
|
fixBulkDataURI(obj[key].Value[0], options, options.dataSourceConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
obj[key].BulkDataURI = obj[key].Value[0].BulkDataURI;
|
||||||
|
|
||||||
|
// prevent mixed-content blockage
|
||||||
|
if (window.location.protocol === 'https:' && obj[key].BulkDataURI.startsWith('http:')) {
|
||||||
|
obj[key].BulkDataURI = obj[key].BulkDataURI.replace('http:', 'https:');
|
||||||
|
}
|
||||||
|
delete obj[key].Value;
|
||||||
|
} else if (vrNumerics.has(obj[key].vr)) {
|
||||||
|
obj[key].Value = obj[key].Value.map(v => +v);
|
||||||
|
} else {
|
||||||
|
obj[key].Value = obj[key].Value.map(entry => cleanDenaturalizedDataset(entry, options));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is required to make the denaturalized data transferrable when it has
|
||||||
|
* added proxy values.
|
||||||
|
*/
|
||||||
|
export function transferDenaturalizedDataset(dataset) {
|
||||||
|
const noNull = cleanDenaturalizedDataset(dataset);
|
||||||
|
return JSON.parse(JSON.stringify(noNull));
|
||||||
|
}
|
||||||
@ -23,6 +23,8 @@ function fixBulkDataURI(value, instance, dicomWebConfig) {
|
|||||||
let { BulkDataURI } = value;
|
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
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Fix multi-valued keys so that those which are strings split by
|
||||||
|
* a backslash are returned as arrays.
|
||||||
|
*/
|
||||||
|
export function fixMultiValueKeys(naturalData, keys = ['ImageType']) {
|
||||||
|
for (const key of keys) {
|
||||||
|
if (typeof naturalData[key] === 'string') {
|
||||||
|
naturalData[key] = naturalData[key].split('\\');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return naturalData;
|
||||||
|
}
|
||||||
@ -1,3 +1,9 @@
|
|||||||
import { fixBulkDataURI } from './fixBulkDataURI';
|
import { fixBulkDataURI } from './fixBulkDataURI';
|
||||||
|
import {
|
||||||
|
cleanDenaturalizedDataset,
|
||||||
|
transferDenaturalizedDataset,
|
||||||
|
} from './cleanDenaturalizedDataset';
|
||||||
|
|
||||||
export { fixBulkDataURI };
|
export { fixMultiValueKeys } from './fixMultiValueKeys';
|
||||||
|
|
||||||
|
export { fixBulkDataURI, cleanDenaturalizedDataset, transferDenaturalizedDataset };
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { isImage } from '@ohif/core/src/utils/isImage';
|
import { 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,
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -1,66 +0,0 @@
|
|||||||
import { dicomWebUtils } from '@ohif/extension-default';
|
|
||||||
|
|
||||||
function isPrimitive(v: any) {
|
|
||||||
return !(typeof v == 'object' || Array.isArray(v));
|
|
||||||
}
|
|
||||||
|
|
||||||
const vrNumerics = [
|
|
||||||
'DS',
|
|
||||||
'FL',
|
|
||||||
'FD',
|
|
||||||
'IS',
|
|
||||||
'OD',
|
|
||||||
'OF',
|
|
||||||
'OL',
|
|
||||||
'OV',
|
|
||||||
'SL',
|
|
||||||
'SS',
|
|
||||||
'SV',
|
|
||||||
'UL',
|
|
||||||
'US',
|
|
||||||
'UV',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specialized for DICOM JSON format dataset cleaning.
|
|
||||||
* @param obj
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export default function cleanDenaturalizedDataset(
|
|
||||||
obj: any,
|
|
||||||
options: {
|
|
||||||
StudyInstanceUID: string;
|
|
||||||
SeriesInstanceUID: string;
|
|
||||||
dataSourceConfig: unknown;
|
|
||||||
}
|
|
||||||
): any {
|
|
||||||
if (Array.isArray(obj)) {
|
|
||||||
const newAry = obj.map(o => (isPrimitive(o) ? o : cleanDenaturalizedDataset(o, options)));
|
|
||||||
return newAry;
|
|
||||||
} else if (isPrimitive(obj)) {
|
|
||||||
return obj;
|
|
||||||
} else {
|
|
||||||
Object.keys(obj).forEach(key => {
|
|
||||||
if (obj[key].Value === null && obj[key].vr) {
|
|
||||||
delete obj[key].Value;
|
|
||||||
} else if (Array.isArray(obj[key].Value) && obj[key].vr) {
|
|
||||||
if (obj[key].Value.length === 1 && obj[key].Value[0].BulkDataURI) {
|
|
||||||
dicomWebUtils.fixBulkDataURI(obj[key].Value[0], options, options.dataSourceConfig);
|
|
||||||
|
|
||||||
obj[key].BulkDataURI = obj[key].Value[0].BulkDataURI;
|
|
||||||
|
|
||||||
// prevent mixed-content blockage
|
|
||||||
if (window.location.protocol === 'https:' && obj[key].BulkDataURI.startsWith('http:')) {
|
|
||||||
obj[key].BulkDataURI = obj[key].BulkDataURI.replace('http:', 'https:');
|
|
||||||
}
|
|
||||||
delete obj[key].Value;
|
|
||||||
} else if (vrNumerics.includes(obj[key].vr)) {
|
|
||||||
obj[key].Value = obj[key].Value.map(v => +v);
|
|
||||||
} else {
|
|
||||||
obj[key].Value = obj[key].Value.map(entry => cleanDenaturalizedDataset(entry, options));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { SOPClassHandlerId } from './id';
|
import { 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;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -22,6 +22,7 @@ describe('Top level exports', () => {
|
|||||||
'imageIdToURI',
|
'imageIdToURI',
|
||||||
'roundNumber',
|
'roundNumber',
|
||||||
'b64toBlob',
|
'b64toBlob',
|
||||||
|
'sopClassDictionary',
|
||||||
'createStudyBrowserTabs',
|
'createStudyBrowserTabs',
|
||||||
'formatDate',
|
'formatDate',
|
||||||
'formatTime',
|
'formatTime',
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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()}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
48
yarn.lock
48
yarn.lock
@ -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"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user