feat: OHIF-1000: Study prefetcher (#2466)
* Enable study prefetcher and expose configs * Fix progress for all images * Enable study loading listener * Add new default for displaying progress bar for series * Use events instead of redux to improve performance * Add documentation, resolve issues with different datasets * CR Updates * CR Updates * CR Updates & Resolve issue when activating both stack and study prefetch * Add upward and update display set logic * Add new behavior to prefetching orders * Rollback display set check and activate cache check * Remove prefetch check * Check if displayset has images * Debounce check cache events * Remove usued debounce flags * Ignore prefetching same image * Use new image instead of image rendered * Continue prefetch after cache check * Update prefetch logic after checking cache * Remove log * Update displayset count default to 3 * Update netlify config Co-authored-by: Alireza <ar.sedghi@gmail.com>
This commit is contained in:
parent
ea0963d75f
commit
8f2604509d
@ -67,6 +67,33 @@ window.config = {
|
|||||||
extensions: [],
|
extensions: [],
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
filterQueryParam: false,
|
filterQueryParam: false,
|
||||||
|
/**
|
||||||
|
* Disable caching of servers configuration.
|
||||||
|
*
|
||||||
|
* There will be no effect if you update the servers property of this config
|
||||||
|
* while your application is running because this property is cached in local storage.
|
||||||
|
*/
|
||||||
|
disableServersCache: false,
|
||||||
|
/**
|
||||||
|
* OHIF's study prefetcher configuration.
|
||||||
|
*
|
||||||
|
* @param {boolean} enabled Whether to enable/disable OHIF's study prefetcher
|
||||||
|
* @param {('all'|'closest'|'downward'|'upward'|'topdown')} order Fetching order: all display sets, the closest ones, downward or top down fashion based on the currently selected display set
|
||||||
|
* @param {number} displaySetCount How much display sets should be prefetched at once (note: this attribute is ignored if order was set to 'all')
|
||||||
|
* @param {boolean} preventCache Prevent images to be cached in Cornerstone Tools's request pool manager
|
||||||
|
* @param {number} prefetchDisplaySetsTimeout Prefetch timeout
|
||||||
|
* @param {boolean} displayProgress Whether to display or not the progress bar in the display set
|
||||||
|
* @param {boolean} includeActiveDisplaySet Include or not the active display set while prefetching
|
||||||
|
*/
|
||||||
|
studyPrefetcher: {
|
||||||
|
enabled: true,
|
||||||
|
order: 'all',
|
||||||
|
displaySetCount: 1,
|
||||||
|
preventCache: false,
|
||||||
|
prefetchDisplaySetsTimeout: 300,
|
||||||
|
displayProgress: false,
|
||||||
|
includeActiveDisplaySet: true,
|
||||||
|
},
|
||||||
servers: {
|
servers: {
|
||||||
dicomWeb: [
|
dicomWeb: [
|
||||||
{
|
{
|
||||||
@ -81,7 +108,14 @@ window.config = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Supported Keys: https://craig.is/killing/mice
|
/**
|
||||||
|
* Hotkey definitions.
|
||||||
|
* Supported Keys: https://craig.is/killing/mice
|
||||||
|
*
|
||||||
|
* @param {string} commandName
|
||||||
|
* @param {string} label
|
||||||
|
* @param {array} keys
|
||||||
|
*/
|
||||||
hotkeys: [
|
hotkeys: [
|
||||||
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
|
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
|
||||||
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
|
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
|
||||||
@ -97,14 +131,16 @@ window.config = {
|
|||||||
keys: ['v'],
|
keys: ['v'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
/* Configuration passed to the bundled cornerstone extension
|
/**
|
||||||
|
* Configuration passed to the bundled cornerstone extension
|
||||||
*
|
*
|
||||||
* The cornerstone extension is currently tightly coupled to the platform.
|
* The cornerstone extension is currently tightly coupled to the platform.
|
||||||
* Until we're able to decouple it, this key will serve as a workaround to
|
* Until we're able to decouple it, this key will serve as a workaround to
|
||||||
* pass it configuration.
|
* pass it configuration.
|
||||||
|
*
|
||||||
|
* @param {boolean} hideHandles Whether to show/hide annotation "handles"
|
||||||
*/
|
*/
|
||||||
cornerstoneExtensionConfig: {
|
cornerstoneExtensionConfig: {
|
||||||
/* Whether to show/hide annotation "handles" */
|
|
||||||
hideHandles: true,
|
hideHandles: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
37
docs/latest/viewer/studyPrefetcher.md
Normal file
37
docs/latest/viewer/studyPrefetcher.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Viewer: Study Prefetcher & Stack Prefetch
|
||||||
|
|
||||||
|
Prefetching the stack of images of the active viewport is enabled by default for
|
||||||
|
cornerstone viewports via cornerstone tools's stack prefetch functionality.
|
||||||
|
|
||||||
|
In order to have a more customizable alternative for prefetching, OHIF provides
|
||||||
|
a built-in study prefetcher functionality which allows the user to set the order
|
||||||
|
in which the display sets are prefetched, the granularity, the timeout and
|
||||||
|
whether or not display a progress bar in the display set (close to the thumbnail
|
||||||
|
image) in the study browser.
|
||||||
|
|
||||||
|
You can customize these options via the viewer's configuration:
|
||||||
|
|
||||||
|
```js
|
||||||
|
window.config = {
|
||||||
|
/**
|
||||||
|
* OHIF's study prefetcher configuration.
|
||||||
|
*
|
||||||
|
* @param {boolean} enabled Whether to enable/disable OHIF's study prefetcher
|
||||||
|
* @param {('all'|'closest'|'downward'|'upward'|'topdown')} order Fetching order: all display sets, the closest ones, downward or top down fashion based on the currently selected display set
|
||||||
|
* @param {number} displaySetCount How much display sets should be prefetched at once (note: this attribute is ignored if order was set to 'all')
|
||||||
|
* @param {boolean} preventCache Prevent images to be cached in Cornerstone Tools's request pool manager
|
||||||
|
* @param {number} prefetchDisplaySetsTimeout Prefetch timeout
|
||||||
|
* @param {boolean} displayProgress Whether to display or not the progress bar in the display set
|
||||||
|
* @param {boolean} includeActiveDisplaySet Include or not the active display set while prefetching
|
||||||
|
*/
|
||||||
|
studyPrefetcher: {
|
||||||
|
enabled: true,
|
||||||
|
order: 'all',
|
||||||
|
displaySetCount: 1,
|
||||||
|
preventCache: false,
|
||||||
|
prefetchDisplaySetsTimeout: 300,
|
||||||
|
displayProgress: false,
|
||||||
|
includeActiveDisplaySet: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
@ -85,12 +85,32 @@ Tools can be configured through extension configuration using the tools key:
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Stack prefetch configuration
|
||||||
|
|
||||||
|
Stack prefetch can be configured through extension configuration using the
|
||||||
|
stackPrefetch key:
|
||||||
|
|
||||||
|
```js
|
||||||
|
...
|
||||||
|
cornerstoneExtensionConfig: {
|
||||||
|
stackPrefetch: {
|
||||||
|
enabled: true,
|
||||||
|
maxImagesToPrefetch: Infinity,
|
||||||
|
preserveExistingPool: false,
|
||||||
|
maxSimultaneousRequests: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
## Annotate Tools Configuration
|
## Annotate Tools Configuration
|
||||||
|
|
||||||
*We currently support one property for annotation tools.*
|
_We currently support one property for annotation tools._
|
||||||
|
|
||||||
### Hide handles
|
### Hide handles
|
||||||
This extension configuration allows you to toggle on/off handle rendering for all annotate tools:
|
|
||||||
|
This extension configuration allows you to toggle on/off handle rendering for
|
||||||
|
all annotate tools:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
...
|
...
|
||||||
@ -119,3 +139,4 @@ This extension configuration allows you to toggle on/off handle rendering for al
|
|||||||
[cornerstone-tools]: https://github.com/cornerstonejs/cornerstoneTools
|
[cornerstone-tools]: https://github.com/cornerstonejs/cornerstoneTools
|
||||||
[cornerstone]: https://github.com/cornerstonejs/cornerstone
|
[cornerstone]: https://github.com/cornerstonejs/cornerstone
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
|
```
|
||||||
|
|||||||
@ -53,7 +53,11 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
// Currently justing using escape hatch + commands
|
// Currently justing using escape hatch + commands
|
||||||
// activeTool: activeButton && activeButton.command,
|
// activeTool: activeButton && activeButton.command,
|
||||||
...dataFromStore,
|
...dataFromStore,
|
||||||
isStackPrefetchEnabled: isActive,
|
isStackPrefetchEnabled: ownProps.hasOwnProperty('isStackPrefetchEnabled')
|
||||||
|
? ownProps.isStackPrefetchEnabled
|
||||||
|
: ownProps.stackPrefetch
|
||||||
|
? ownProps.stackPrefetch.enabled
|
||||||
|
: isActive,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
frameRate,
|
frameRate,
|
||||||
//stack: viewportSpecificData.stack,
|
//stack: viewportSpecificData.stack,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import OHIFCornerstoneViewportOverlay from './components/OHIFCornerstoneViewportOverlay'
|
import OHIFCornerstoneViewportOverlay from './components/OHIFCornerstoneViewportOverlay';
|
||||||
import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
|
import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
|
||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@ -15,6 +15,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
customProps: {},
|
customProps: {},
|
||||||
|
isStackPrefetchEnabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -23,6 +24,8 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
viewportIndex: PropTypes.number,
|
viewportIndex: PropTypes.number,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
customProps: PropTypes.object,
|
customProps: PropTypes.object,
|
||||||
|
stackPrefetch: PropTypes.object,
|
||||||
|
isStackPrefetchEnabled: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static id = 'OHIFCornerstoneViewport';
|
static id = 'OHIFCornerstoneViewport';
|
||||||
@ -180,7 +183,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
displaySet.displaySetInstanceUID !==
|
displaySet.displaySetInstanceUID !==
|
||||||
prevDisplaySet.displaySetInstanceUID ||
|
prevDisplaySet.displaySetInstanceUID ||
|
||||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||||
) {
|
) {
|
||||||
@ -232,7 +235,12 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const warningsOverlay = props => {
|
const warningsOverlay = props => {
|
||||||
return <OHIFCornerstoneViewportOverlay {...props} inconsistencyWarnings={inconsistencyWarnings} />
|
return (
|
||||||
|
<OHIFCornerstoneViewportOverlay
|
||||||
|
{...props}
|
||||||
|
inconsistencyWarnings={inconsistencyWarnings}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -244,10 +252,11 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
onNewImageDebounced={newImageHandler}
|
onNewImageDebounced={newImageHandler}
|
||||||
onNewImageDebounceTime={300}
|
onNewImageDebounceTime={300}
|
||||||
viewportOverlayComponent={warningsOverlay}
|
viewportOverlayComponent={warningsOverlay}
|
||||||
|
stackPrefetch={this.props.stackPrefetch}
|
||||||
|
isStackPrefetchEnabled={this.props.isStackPrefetchEnabled}
|
||||||
// ~~ Connected (From REDUX)
|
// ~~ Connected (From REDUX)
|
||||||
// frameRate={frameRate}
|
// frameRate={frameRate}
|
||||||
// isPlaying={false}
|
// isPlaying={false}
|
||||||
// isStackPrefetchEnabled={true}
|
|
||||||
// onElementEnabled={() => {}}
|
// onElementEnabled={() => {}}
|
||||||
// setViewportActive{() => {}}
|
// setViewportActive{() => {}}
|
||||||
{...this.props.customProps}
|
{...this.props.customProps}
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export default {
|
|||||||
preRegistration({ servicesManager, configuration = {} }) {
|
preRegistration({ servicesManager, configuration = {} }) {
|
||||||
init({ servicesManager, configuration });
|
init({ servicesManager, configuration });
|
||||||
},
|
},
|
||||||
getViewportModule({ commandsManager }) {
|
getViewportModule({ commandsManager, appConfig }) {
|
||||||
const ExtendedOHIFCornerstoneViewport = props => {
|
const ExtendedOHIFCornerstoneViewport = props => {
|
||||||
/**
|
/**
|
||||||
* TODO: This appears to be used to set the redux parameters for
|
* TODO: This appears to be used to set the redux parameters for
|
||||||
@ -48,8 +48,17 @@ export default {
|
|||||||
jumpData.refreshViewports = false;
|
jumpData.refreshViewports = false;
|
||||||
commandsManager.runCommand('jumpToImage', jumpData);
|
commandsManager.runCommand('jumpToImage', jumpData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { studyPrefetcher } = appConfig;
|
||||||
|
const isStackPrefetchEnabled =
|
||||||
|
studyPrefetcher && !studyPrefetcher.enabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OHIFCornerstoneViewport {...props} onNewImage={onNewImageHandler} />
|
<OHIFCornerstoneViewport
|
||||||
|
{...props}
|
||||||
|
onNewImage={onNewImageHandler}
|
||||||
|
isStackPrefetchEnabled={isStackPrefetchEnabled}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -37,7 +37,14 @@ export default function init({ servicesManager, configuration }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { csToolsConfig } = configuration;
|
const {
|
||||||
|
csToolsConfig,
|
||||||
|
stackPrefetch = {
|
||||||
|
maxImagesToPrefetch: Infinity,
|
||||||
|
preserveExistingPool: false,
|
||||||
|
maxSimultaneousRequests: 20,
|
||||||
|
},
|
||||||
|
} = configuration;
|
||||||
const metadataProvider = OHIF.cornerstone.metadataProvider;
|
const metadataProvider = OHIF.cornerstone.metadataProvider;
|
||||||
|
|
||||||
cornerstone.metaData.addProvider(
|
cornerstone.metaData.addProvider(
|
||||||
@ -52,7 +59,7 @@ export default function init({ servicesManager, configuration }) {
|
|||||||
autoResizeViewports: false,
|
autoResizeViewports: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
initCornerstoneTools(defaultCsToolsConfig);
|
initCornerstoneTools({ ...defaultCsToolsConfig, ...stackPrefetch });
|
||||||
|
|
||||||
const toolsGroupedByType = {
|
const toolsGroupedByType = {
|
||||||
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
|
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
|
||||||
|
|||||||
@ -37,8 +37,8 @@ export default function(configuration = {}) {
|
|||||||
|
|
||||||
// Configure stack prefetch
|
// Configure stack prefetch
|
||||||
cornerstoneTools.stackPrefetch.setConfiguration({
|
cornerstoneTools.stackPrefetch.setConfiguration({
|
||||||
maxImagesToPrefetch: Infinity,
|
maxImagesToPrefetch: configuration.maxImagesToPrefetch,
|
||||||
preserveExistingPool: false,
|
preserveExistingPool: configuration.preserveExistingPool,
|
||||||
maxSimultaneousRequests: 20,
|
maxSimultaneousRequests: configuration.maxSimultaneousRequests,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||||
import {
|
import debounce from 'lodash.debounce';
|
||||||
clearStudyLoadingProgress,
|
|
||||||
setStudyLoadingProgress,
|
|
||||||
} from '../redux/actions';
|
|
||||||
import StackManager from '../utils/StackManager';
|
import StackManager from '../utils/StackManager';
|
||||||
|
import { StudyPrefetcher } from './StudyPrefetcher';
|
||||||
|
|
||||||
class BaseLoadingListener {
|
class BaseLoadingListener {
|
||||||
constructor(stack, options = {}) {
|
constructor(stack, options = {}) {
|
||||||
this.id = BaseLoadingListener.getNewId();
|
this.id = BaseLoadingListener.getNewId();
|
||||||
this.stack = stack;
|
this.stack = stack;
|
||||||
this.startListening();
|
|
||||||
this.statsItemsLimit = options.statsItemsLimit || 2;
|
this.statsItemsLimit = options.statsItemsLimit || 2;
|
||||||
this.stats = {
|
this.stats = {
|
||||||
items: [],
|
items: [],
|
||||||
@ -60,6 +58,9 @@ class BaseLoadingListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getProgressId() {
|
_getProgressId() {
|
||||||
|
/**
|
||||||
|
* TODO: The id key should be configurable.
|
||||||
|
*/
|
||||||
const displaySetInstanceUID = this.stack.displaySetInstanceUID;
|
const displaySetInstanceUID = this.stack.displaySetInstanceUID;
|
||||||
return 'StackProgress:' + displaySetInstanceUID;
|
return 'StackProgress:' + displaySetInstanceUID;
|
||||||
}
|
}
|
||||||
@ -96,11 +97,18 @@ class BaseLoadingListener {
|
|||||||
class DICOMFileLoadingListener extends BaseLoadingListener {
|
class DICOMFileLoadingListener extends BaseLoadingListener {
|
||||||
constructor(stack, options) {
|
constructor(stack, options) {
|
||||||
super(stack, options);
|
super(stack, options);
|
||||||
|
|
||||||
|
this.imageLoadProgressEventHandler = this._imageLoadProgressEventHandle.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
|
|
||||||
this._dataSetUrl = this._getDataSetUrl(stack);
|
this._dataSetUrl = this._getDataSetUrl(stack);
|
||||||
this._lastLoaded = 0;
|
this._lastLoaded = 0;
|
||||||
|
|
||||||
// Check how many instances has already been download (cached)
|
// Check how many instances has already been download (cached)
|
||||||
this._checkCachedData();
|
this._checkCachedData();
|
||||||
|
|
||||||
|
this.startListening();
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkCachedData() {
|
_checkCachedData() {
|
||||||
@ -127,10 +135,6 @@ class DICOMFileLoadingListener extends BaseLoadingListener {
|
|||||||
startListening() {
|
startListening() {
|
||||||
const imageLoadProgressEventName = this._getImageLoadProgressEventName();
|
const imageLoadProgressEventName = this._getImageLoadProgressEventName();
|
||||||
|
|
||||||
this.imageLoadProgressEventHandler = this._imageLoadProgressEventHandle.bind(
|
|
||||||
this
|
|
||||||
);
|
|
||||||
|
|
||||||
this.stopListening();
|
this.stopListening();
|
||||||
|
|
||||||
cornerstone.events.addEventListener(
|
cornerstone.events.addEventListener(
|
||||||
@ -200,17 +204,57 @@ class DICOMFileLoadingListener extends BaseLoadingListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const StudyLoadingListenerEvents = {
|
||||||
|
OnProgress: 'StudyLoadingListenerEvents.OnProgress',
|
||||||
|
};
|
||||||
|
|
||||||
|
function promiseState(promise, callback) {
|
||||||
|
// Symbols and RegExps are never content-equal
|
||||||
|
var uniqueValue = window['Symbol'] ? Symbol('unique') : /unique/;
|
||||||
|
|
||||||
|
function notifyPendingOrResolved(value) {
|
||||||
|
if (value === uniqueValue) {
|
||||||
|
return callback('pending');
|
||||||
|
} else {
|
||||||
|
return callback('fulfilled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyRejected(reason) {
|
||||||
|
return callback('rejected');
|
||||||
|
}
|
||||||
|
|
||||||
|
var race = [promise, Promise.resolve(uniqueValue)];
|
||||||
|
Promise.race(race).then(notifyPendingOrResolved, notifyRejected);
|
||||||
|
}
|
||||||
|
|
||||||
class StackLoadingListener extends BaseLoadingListener {
|
class StackLoadingListener extends BaseLoadingListener {
|
||||||
constructor(stack, options = {}) {
|
constructor(stack, options = {}) {
|
||||||
options.statsItemsLimit = 20;
|
options.statsItemsLimit = 20;
|
||||||
|
|
||||||
super(stack, options);
|
super(stack, options);
|
||||||
|
|
||||||
|
this.imageLoadedEventHandler = this._imageLoadedEventHandler.bind(this);
|
||||||
|
this.imageCachePromiseRemovedEventHandler = this._imageCachePromiseRemovedEventHandler.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
|
|
||||||
this.imageDataMap = this._convertImageIdsArrayToMap(stack.imageIds);
|
this.imageDataMap = this._convertImageIdsArrayToMap(stack.imageIds);
|
||||||
this.framesStatus = this._createArray(stack.imageIds.length, false);
|
this.framesStatus = this._createArray(stack.imageIds.length, false);
|
||||||
this.loadedCount = 0;
|
this.loadedCount = 0;
|
||||||
|
|
||||||
// Check how many instances has already been download (cached)
|
// Check how many instances has already been download (cached)
|
||||||
this._checkCachedData();
|
this._debouncedSetProgressData = debounce((...args) => {
|
||||||
|
this._setProgressData(...args);
|
||||||
|
|
||||||
|
/** After checking cache, continue prefetch */
|
||||||
|
const studyPrefetcher = StudyPrefetcher.getInstance();
|
||||||
|
studyPrefetcher.prefetch(studyPrefetcher.getElement());
|
||||||
|
}, 300);
|
||||||
|
const debounced = true;
|
||||||
|
this._checkCachedData(debounced);
|
||||||
|
|
||||||
|
this.startListening();
|
||||||
}
|
}
|
||||||
|
|
||||||
_convertImageIdsArrayToMap(imageIds) {
|
_convertImageIdsArrayToMap(imageIds) {
|
||||||
@ -238,18 +282,37 @@ class StackLoadingListener extends BaseLoadingListener {
|
|||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkCachedData() {
|
/**
|
||||||
// const imageIds = this.stack.imageIds;
|
* Check if image id is cached in cornerstone.
|
||||||
// TODO: No way to check status of Promise.
|
*
|
||||||
/*for(let i = 0; i < imageIds.length; i++) {
|
* @param {string} imageId
|
||||||
const imageId = imageIds[i];
|
* @returns
|
||||||
|
*/
|
||||||
|
isImageCached(imageId) {
|
||||||
|
const image = cornerstone.imageCache.imageCache[imageId];
|
||||||
|
return image && image.sizeInBytes;
|
||||||
|
}
|
||||||
|
|
||||||
const imagePromise = cornerstone.imageCache.getImageLoadObject(imageId).promise;
|
_checkCachedData(debounced = false) {
|
||||||
|
const imageIds = this.stack.imageIds;
|
||||||
|
|
||||||
if (imagePromise && (imagePromise.state() === 'resolved')) {
|
for (let i = 0; i < imageIds.length; i++) {
|
||||||
this._updateFrameStatus(imageId, true);
|
const imageId = imageIds[i];
|
||||||
}
|
|
||||||
}*/
|
const imageObject = cornerstone.imageCache.getImageLoadObject(imageId);
|
||||||
|
|
||||||
|
if (this.isImageCached(imageId)) {
|
||||||
|
this._updateFrameStatus(imageId, true, debounced);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageObject && imageObject.promise) {
|
||||||
|
promiseState(imageObject.promise, state => {
|
||||||
|
if (state === 'fulfilled') {
|
||||||
|
this._updateFrameStatus(imageId, true, debounced);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getImageLoadedEventName() {
|
_getImageLoadedEventName() {
|
||||||
@ -272,11 +335,6 @@ class StackLoadingListener extends BaseLoadingListener {
|
|||||||
const imageLoadedEventName = this._getImageLoadedEventName();
|
const imageLoadedEventName = this._getImageLoadedEventName();
|
||||||
const imageCachePromiseRemovedEventName = this._getImageCachePromiseRemoveEventName();
|
const imageCachePromiseRemovedEventName = this._getImageCachePromiseRemoveEventName();
|
||||||
|
|
||||||
this.imageLoadedEventHandler = this._imageLoadedEventHandler.bind(this);
|
|
||||||
this.imageCachePromiseRemovedEventHandler = this._imageCachePromiseRemovedEventHandler.bind(
|
|
||||||
this
|
|
||||||
);
|
|
||||||
|
|
||||||
this.stopListening();
|
this.stopListening();
|
||||||
|
|
||||||
cornerstone.events.addEventListener(
|
cornerstone.events.addEventListener(
|
||||||
@ -303,7 +361,7 @@ class StackLoadingListener extends BaseLoadingListener {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateFrameStatus(imageId, loaded) {
|
_updateFrameStatus(imageId, loaded, debounced) {
|
||||||
const imageData = this.imageDataMap.get(imageId);
|
const imageData = this.imageDataMap.get(imageId);
|
||||||
|
|
||||||
if (!imageData || imageData.loaded === loaded) {
|
if (!imageData || imageData.loaded === loaded) {
|
||||||
@ -318,7 +376,7 @@ class StackLoadingListener extends BaseLoadingListener {
|
|||||||
imageData.loaded = loaded;
|
imageData.loaded = loaded;
|
||||||
this.framesStatus[imageData.index] = loaded;
|
this.framesStatus[imageData.index] = loaded;
|
||||||
this.loadedCount += loaded ? 1 : -1;
|
this.loadedCount += loaded ? 1 : -1;
|
||||||
this._updateProgress();
|
this._updateProgress(debounced);
|
||||||
}
|
}
|
||||||
|
|
||||||
_setProgressData(progressId, progressData) {
|
_setProgressData(progressId, progressData) {
|
||||||
@ -339,7 +397,7 @@ class StackLoadingListener extends BaseLoadingListener {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateProgress() {
|
_updateProgress(debounced) {
|
||||||
const totalFramesCount = this.stack.imageIds.length;
|
const totalFramesCount = this.stack.imageIds.length;
|
||||||
const loadedFramesCount = this.loadedCount;
|
const loadedFramesCount = this.loadedCount;
|
||||||
const loadingFramesCount = totalFramesCount - loadedFramesCount;
|
const loadingFramesCount = totalFramesCount - loadedFramesCount;
|
||||||
@ -357,6 +415,11 @@ class StackLoadingListener extends BaseLoadingListener {
|
|||||||
framesStatus: this.framesStatus,
|
framesStatus: this.framesStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (debounced) {
|
||||||
|
this._debouncedSetProgressData(progressId, progressData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._setProgressData(progressId, progressData);
|
this._setProgressData(progressId, progressData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,11 +434,13 @@ class StackLoadingListener extends BaseLoadingListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
progressBar += ']';
|
progressBar += ']';
|
||||||
log.info(`${displaySetInstanceUID}: ${progressBar}`);
|
console.info(`${displaySetInstanceUID}: ${progressBar}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StudyLoadingListener {
|
class StudyLoadingListener {
|
||||||
|
static events = StudyLoadingListenerEvents;
|
||||||
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this.listeners = {};
|
this.listeners = {};
|
||||||
this.options = options;
|
this.options = options;
|
||||||
@ -454,14 +519,36 @@ class StudyLoadingListener {
|
|||||||
|
|
||||||
_getSchema(stack) {
|
_getSchema(stack) {
|
||||||
const imageId = stack.imageIds[0];
|
const imageId = stack.imageIds[0];
|
||||||
|
if (!imageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const colonIndex = imageId.indexOf(':');
|
const colonIndex = imageId.indexOf(':');
|
||||||
return imageId.substring(0, colonIndex);
|
return imageId.substring(0, colonIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Singleton
|
|
||||||
static getInstance(options) {
|
static getInstance(options) {
|
||||||
|
/**
|
||||||
|
* TODO: Use a different alternative without the use of events.
|
||||||
|
*/
|
||||||
|
const DEFAULT_OPTIONS = {
|
||||||
|
_setProgressData: (progressId, progressData) => {
|
||||||
|
const event = new CustomEvent(StudyLoadingListenerEvents.OnProgress, {
|
||||||
|
detail: { progressId, progressData },
|
||||||
|
});
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
},
|
||||||
|
_clearProgressById: progressId => {
|
||||||
|
const event = new CustomEvent(StudyLoadingListenerEvents.OnProgress, {
|
||||||
|
detail: { progressId, percentComplete: 0 },
|
||||||
|
});
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
if (!StudyLoadingListener._instance) {
|
if (!StudyLoadingListener._instance) {
|
||||||
StudyLoadingListener._instance = new StudyLoadingListener(options);
|
StudyLoadingListener._instance = new StudyLoadingListener(
|
||||||
|
options || DEFAULT_OPTIONS
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return StudyLoadingListener._instance;
|
return StudyLoadingListener._instance;
|
||||||
|
|||||||
@ -1,14 +1,28 @@
|
|||||||
import log from '../log.js';
|
|
||||||
import OHIFError from './OHIFError';
|
|
||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
|
||||||
import getImageId from '../utils/getImageId.js';
|
import getImageId from '../utils/getImageId.js';
|
||||||
|
|
||||||
|
const noop = () => {};
|
||||||
|
|
||||||
export class StudyPrefetcher {
|
export class StudyPrefetcher {
|
||||||
constructor(studies) {
|
options = {
|
||||||
|
order: 'closest',
|
||||||
|
displaySetCount: 1,
|
||||||
|
onImageCached: noop,
|
||||||
|
requestType: 'prefetch',
|
||||||
|
preventCache: false,
|
||||||
|
prefetchDisplaySetsTimeout: 300,
|
||||||
|
includeActiveDisplaySet: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(studies, options) {
|
||||||
this.studies = studies || [];
|
this.studies = studies || [];
|
||||||
this.prefetchDisplaySetsTimeout = 300;
|
|
||||||
this.lastActiveViewportElement = null;
|
if (options) {
|
||||||
|
this.options = { ...this.options, ...options };
|
||||||
|
this.options.requestType = 'prefetch';
|
||||||
|
}
|
||||||
|
|
||||||
cornerstone.events.addEventListener(
|
cornerstone.events.addEventListener(
|
||||||
'cornerstoneimagecachefull.StudyPrefetcher',
|
'cornerstoneimagecachefull.StudyPrefetcher',
|
||||||
@ -16,6 +30,9 @@ export class StudyPrefetcher {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove previously set event listeners and stop prefetching.
|
||||||
|
*/
|
||||||
destroy() {
|
destroy() {
|
||||||
this.stopPrefetching();
|
this.stopPrefetching();
|
||||||
cornerstone.events.removeEventListener(
|
cornerstone.events.removeEventListener(
|
||||||
@ -24,68 +41,117 @@ export class StudyPrefetcher {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance() {
|
/**
|
||||||
|
* Get StudyPrefetcher singleton instance.
|
||||||
|
*
|
||||||
|
* @param {array} studies
|
||||||
|
* @param {object} options
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
static getInstance(studies = [], options) {
|
||||||
if (!StudyPrefetcher.instance) {
|
if (!StudyPrefetcher.instance) {
|
||||||
StudyPrefetcher.instance = new StudyPrefetcher([]);
|
StudyPrefetcher.instance = new StudyPrefetcher(studies, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options) {
|
||||||
|
this.options = { ...this.options, ...options };
|
||||||
|
this.options.requestType = 'prefetch';
|
||||||
}
|
}
|
||||||
|
|
||||||
return StudyPrefetcher.instance;
|
return StudyPrefetcher.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OHIF study metadata instances.
|
||||||
|
*
|
||||||
|
* @param {array} studies
|
||||||
|
*/
|
||||||
setStudies(studies) {
|
setStudies(studies) {
|
||||||
this.stopPrefetching();
|
this.stopPrefetching();
|
||||||
this.studies = studies;
|
this.studies = studies;
|
||||||
}
|
}
|
||||||
|
|
||||||
prefetch() {
|
/**
|
||||||
|
* Get previously prefetched element.
|
||||||
|
*
|
||||||
|
* @returns {HTMLElement} Previously prefetched element.
|
||||||
|
*/
|
||||||
|
getElement() {
|
||||||
|
return this.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch related display sets based on cornerstone viewport element
|
||||||
|
* with previously set options.
|
||||||
|
*
|
||||||
|
* @param {*} element
|
||||||
|
* @param {string} displaySetInstanceUID the display set instance uid
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
prefetch(element, displaySetInstanceUID) {
|
||||||
if (!this.studies || !this.studies.length) {
|
if (!this.studies || !this.studies.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.element = element;
|
||||||
|
this.enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
|
||||||
this.stopPrefetching();
|
this.stopPrefetching();
|
||||||
this.prefetchDisplaySets();
|
this.prefetchDisplaySets(displaySetInstanceUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop prefetching images.
|
||||||
|
*/
|
||||||
stopPrefetching() {
|
stopPrefetching() {
|
||||||
cornerstoneTools.requestPoolManager.clearRequestStack('prefetch');
|
cornerstoneTools.requestPoolManager.clearRequestStack('prefetch');
|
||||||
}
|
}
|
||||||
|
|
||||||
prefetchDisplaySetsAsync(timeout) {
|
/**
|
||||||
timeout = timeout || this.prefetchDisplaySetsTimeout;
|
* Prefetch display sets async.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} element cornerstone viewport element
|
||||||
|
* @param {number} timeout
|
||||||
|
*/
|
||||||
|
prefetchDisplaySetsAsync(element, timeout) {
|
||||||
|
this.enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
timeout = timeout || this.options.prefetchDisplaySetsTimeout;
|
||||||
clearTimeout(this.prefetchDisplaySetsHandler);
|
clearTimeout(this.prefetchDisplaySetsHandler);
|
||||||
this.prefetchDisplaySetsHandler = setTimeout(() => {
|
this.prefetchDisplaySetsHandler = setTimeout(() => {
|
||||||
this.prefetchDisplaySets();
|
this.prefetchDisplaySets(element);
|
||||||
}, timeout);
|
}, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
prefetchDisplaySets() {
|
/**
|
||||||
// TODO: Allow passing in config
|
* Extract all image ids from all display sets to be fetched and
|
||||||
let config = {
|
* call method to add images to request pool manager.
|
||||||
order: 'closest',
|
*
|
||||||
displaySetCount: 1,
|
* @param {string} displaySetInstanceUID the display set instance uid
|
||||||
};
|
*/
|
||||||
|
prefetchDisplaySets(displaySetInstanceUID) {
|
||||||
const displaySetsToPrefetch = this.getDisplaySetsToPrefetch(config);
|
const displaySetsToPrefetch = this.getDisplaySetsToPrefetch(
|
||||||
|
displaySetInstanceUID
|
||||||
|
);
|
||||||
const imageIds = this.getImageIdsFromDisplaySets(displaySetsToPrefetch);
|
const imageIds = this.getImageIdsFromDisplaySets(displaySetsToPrefetch);
|
||||||
|
|
||||||
this.prefetchImageIds(imageIds);
|
this.prefetchImageIds(imageIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add image ids to request pool manager.
|
||||||
|
*
|
||||||
|
* @param {array} imageIds
|
||||||
|
*/
|
||||||
prefetchImageIds(imageIds) {
|
prefetchImageIds(imageIds) {
|
||||||
const nonCachedImageIds = this.filterCachedImageIds(imageIds);
|
const nonCachedImageIds = this.filterCachedImageIds(imageIds);
|
||||||
const requestPoolManager = cornerstoneTools.requestPoolManager;
|
const requestPoolManager = cornerstoneTools.requestPoolManager;
|
||||||
const requestType = 'prefetch';
|
|
||||||
const preventCache = false;
|
|
||||||
const noop = () => {};
|
|
||||||
|
|
||||||
nonCachedImageIds.forEach(imageId => {
|
nonCachedImageIds.forEach(imageId => {
|
||||||
requestPoolManager.addRequest(
|
requestPoolManager.addRequest(
|
||||||
{},
|
{},
|
||||||
imageId,
|
imageId,
|
||||||
requestType,
|
this.options.requestType,
|
||||||
preventCache,
|
this.options.preventCache,
|
||||||
noop,
|
() => {},
|
||||||
noop
|
noop
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -93,26 +159,45 @@ export class StudyPrefetcher {
|
|||||||
requestPoolManager.startGrabbing();
|
requestPoolManager.startGrabbing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get study by cornerstone image instance.
|
||||||
|
*
|
||||||
|
* @param {object} image
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
getStudy(image) {
|
getStudy(image) {
|
||||||
const StudyInstanceUID = cornerstone.metaData.get(
|
const StudyInstanceUID = cornerstone.metaData.get(
|
||||||
'StudyInstanceUID',
|
'StudyInstanceUID',
|
||||||
image.imageId
|
image.imageId
|
||||||
);
|
);
|
||||||
return OHIF.viewer.Studies.find(
|
const studies = this.studies;
|
||||||
study => study.StudyInstanceUID === StudyInstanceUID
|
return studies.find(
|
||||||
|
study => study.getData().StudyInstanceUID === StudyInstanceUID
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get study series by cornerstone image instance.
|
||||||
|
*
|
||||||
|
* @param {object} study OHIF study instance
|
||||||
|
* @param {object} image cornerstone image instance object
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
getSeries(study, image) {
|
getSeries(study, image) {
|
||||||
const SeriesInstanceUID = cornerstone.metaData.get(
|
const SeriesInstanceUID = cornerstone.metaData.get(
|
||||||
'SeriesInstanceUID',
|
'SeriesInstanceUID',
|
||||||
image.imageId
|
image.imageId
|
||||||
);
|
);
|
||||||
const studyMetadata = OHIF.viewerbase.getStudyMetadata(study);
|
return study.getSeriesByUID(SeriesInstanceUID);
|
||||||
|
|
||||||
return studyMetadata.getSeriesByUID(SeriesInstanceUID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get sop instance by cornerstone image instance.
|
||||||
|
*
|
||||||
|
* @param {array} series
|
||||||
|
* @param {object} image
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
getInstance(series, image) {
|
getInstance(series, image) {
|
||||||
const instanceMetadata = cornerstone.metaData.get(
|
const instanceMetadata = cornerstone.metaData.get(
|
||||||
'instance',
|
'instance',
|
||||||
@ -121,7 +206,33 @@ export class StudyPrefetcher {
|
|||||||
return series.getInstanceByUID(instanceMetadata.SOPInstanceUID);
|
return series.getInstanceByUID(instanceMetadata.SOPInstanceUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveDisplaySet(displaySets, instance) {
|
/**
|
||||||
|
* Returns the display set with given uid.
|
||||||
|
*
|
||||||
|
* @param {string} displaySetInstanceUID the display set instance uid
|
||||||
|
* @returns {object} displaySet
|
||||||
|
*/
|
||||||
|
getDisplaySetByUID(displaySetInstanceUID) {
|
||||||
|
let displaySet;
|
||||||
|
this.studies.forEach(study => {
|
||||||
|
const ds = study.displaySets.find(
|
||||||
|
ds => ds.displaySetInstanceUID === displaySetInstanceUID
|
||||||
|
);
|
||||||
|
if (ds) {
|
||||||
|
displaySet = ds;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return displaySet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get display set by SOPInstanceUID.
|
||||||
|
*
|
||||||
|
* @param {array} displaySets
|
||||||
|
* @param {object} instance
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getDisplaySetBySOPInstanceUID(displaySets, instance) {
|
||||||
return displaySets.find(displaySet => {
|
return displaySets.find(displaySet => {
|
||||||
return displaySet.images.some(displaySetImage => {
|
return displaySet.images.some(displaySetImage => {
|
||||||
return displaySetImage.SOPInstanceUID === instance.SOPInstanceUID;
|
return displaySetImage.SOPInstanceUID === instance.SOPInstanceUID;
|
||||||
@ -129,25 +240,48 @@ export class StudyPrefetcher {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getDisplaySetsToPrefetch(config) {
|
/**
|
||||||
|
* Get active viewport image based on cornerstone viewport element.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getActiveViewportImage() {
|
||||||
|
if (!this.enabledElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.enabledElement.image;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch display sets based on cornerstone viewport element image.
|
||||||
|
*
|
||||||
|
* @param {string} displaySetInstanceUID the display set instance uid
|
||||||
|
* @returns {array} displaySets
|
||||||
|
*/
|
||||||
|
getDisplaySetsToPrefetch(displaySetInstanceUID) {
|
||||||
const image = this.getActiveViewportImage();
|
const image = this.getActiveViewportImage();
|
||||||
|
|
||||||
if (!image || !config || !config.displaySetCount) {
|
if (!image) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/*const study = this.getStudy(image);
|
const study = this.getStudy(image);
|
||||||
const series = this.getSeries(study, image);
|
const series = this.getSeries(study, image);
|
||||||
const instance = this.getInstance(series, image);*/
|
const instance = this.getInstance(series, image);
|
||||||
const displaySets = study.displaySets;
|
const displaySets = study.displaySets;
|
||||||
const activeDisplaySet = null; //this.getActiveDisplaySet(displaySets, instance);
|
const activeDisplaySet = displaySetInstanceUID
|
||||||
|
? this.getDisplaySetByUID(displaySetInstanceUID)
|
||||||
|
: this.getDisplaySetBySOPInstanceUID(displaySets, instance);
|
||||||
|
|
||||||
const prefetchMethodMap = {
|
const prefetchMethodMap = {
|
||||||
topdown: 'getFirstDisplaySets',
|
topdown: 'getFirstDisplaySets',
|
||||||
downward: 'getNextDisplaySets',
|
downward: 'getNextDisplaySets',
|
||||||
|
upward: 'getPreviousDisplaySets',
|
||||||
closest: 'getClosestDisplaySets',
|
closest: 'getClosestDisplaySets',
|
||||||
|
all: 'getAllDisplaySets',
|
||||||
};
|
};
|
||||||
|
|
||||||
const prefetchOrder = config.order;
|
const prefetchOrder = this.options.order;
|
||||||
const methodName = prefetchMethodMap[prefetchOrder];
|
const methodName = prefetchMethodMap[prefetchOrder];
|
||||||
const getDisplaySets = this[methodName];
|
const getDisplaySets = this[methodName];
|
||||||
|
|
||||||
@ -163,18 +297,59 @@ export class StudyPrefetcher {
|
|||||||
this,
|
this,
|
||||||
displaySets,
|
displaySets,
|
||||||
activeDisplaySet,
|
activeDisplaySet,
|
||||||
config.displaySetCount
|
this.options.displaySetCount,
|
||||||
|
this.options.includeActiveDisplaySet
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFirstDisplaySets(displaySets, activeDisplaySet, displaySetCount) {
|
/**
|
||||||
|
* Get all display sets.
|
||||||
|
*
|
||||||
|
* @param {array} displaySets
|
||||||
|
* @param {object} activeDisplaySet
|
||||||
|
* @param {number} displaySetCount
|
||||||
|
* @param {boolean} includeActiveDisplaySet
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getAllDisplaySets(
|
||||||
|
displaySets,
|
||||||
|
activeDisplaySet,
|
||||||
|
displaySetCount,
|
||||||
|
includeActiveDisplaySet
|
||||||
|
) {
|
||||||
|
const length = displaySets.length;
|
||||||
|
const selectedDisplaySets = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const displaySet = displaySets[i];
|
||||||
|
selectedDisplaySets.push(displaySet);
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedDisplaySets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all display sets in order after the active display set.
|
||||||
|
*
|
||||||
|
* @param {array} displaySets
|
||||||
|
* @param {object} activeDisplaySet
|
||||||
|
* @param {number} displaySetCount
|
||||||
|
* @param {boolean} includeActiveDisplaySet
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getFirstDisplaySets(
|
||||||
|
displaySets,
|
||||||
|
activeDisplaySet,
|
||||||
|
displaySetCount,
|
||||||
|
includeActiveDisplaySet
|
||||||
|
) {
|
||||||
const length = displaySets.length;
|
const length = displaySets.length;
|
||||||
const selectedDisplaySets = [];
|
const selectedDisplaySets = [];
|
||||||
|
|
||||||
for (let i = 0; i < length && displaySetCount; i++) {
|
for (let i = 0; i < length && displaySetCount; i++) {
|
||||||
const displaySet = displaySets[i];
|
const displaySet = displaySets[i];
|
||||||
|
|
||||||
if (displaySet !== activeDisplaySet) {
|
if (includeActiveDisplaySet || displaySet !== activeDisplaySet) {
|
||||||
selectedDisplaySets.push(displaySet);
|
selectedDisplaySets.push(displaySet);
|
||||||
displaySetCount--;
|
displaySetCount--;
|
||||||
}
|
}
|
||||||
@ -183,21 +358,78 @@ export class StudyPrefetcher {
|
|||||||
return selectedDisplaySets;
|
return selectedDisplaySets;
|
||||||
}
|
}
|
||||||
|
|
||||||
getNextDisplaySets(displaySets, activeDisplaySet, displaySetCount) {
|
/**
|
||||||
|
* Get all display sets before the active display set.
|
||||||
|
*
|
||||||
|
* @param {array} displaySets
|
||||||
|
* @param {object} activeDisplaySet
|
||||||
|
* @param {number} displaySetCount
|
||||||
|
* @param {boolean} includeActiveDisplaySet
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getPreviousDisplaySets(
|
||||||
|
displaySets,
|
||||||
|
activeDisplaySet,
|
||||||
|
displaySetCount,
|
||||||
|
includeActiveDisplaySet
|
||||||
|
) {
|
||||||
const activeDisplaySetIndex = displaySets.indexOf(activeDisplaySet);
|
const activeDisplaySetIndex = displaySets.indexOf(activeDisplaySet);
|
||||||
const begin = activeDisplaySetIndex + 1;
|
const end = includeActiveDisplaySet
|
||||||
const end = Math.min(begin + displaySetCount, displaySets.length);
|
? activeDisplaySetIndex + 1
|
||||||
|
: activeDisplaySetIndex;
|
||||||
|
const previousDisplaySets = displaySets.slice(0, end);
|
||||||
|
return previousDisplaySets.reverse().slice(0, displaySetCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all display sets after the active display set.
|
||||||
|
*
|
||||||
|
* @param {array} displaySets
|
||||||
|
* @param {object} activeDisplaySet
|
||||||
|
* @param {number} displaySetCount
|
||||||
|
* @param {boolean} includeActiveDisplaySet
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getNextDisplaySets(
|
||||||
|
displaySets,
|
||||||
|
activeDisplaySet,
|
||||||
|
displaySetCount,
|
||||||
|
includeActiveDisplaySet
|
||||||
|
) {
|
||||||
|
const activeDisplaySetIndex = displaySets.indexOf(activeDisplaySet);
|
||||||
|
const begin = includeActiveDisplaySet
|
||||||
|
? activeDisplaySetIndex
|
||||||
|
: activeDisplaySetIndex + 1;
|
||||||
|
const end = Math.min(begin + displaySetCount, displaySets.length);
|
||||||
return displaySets.slice(begin, end);
|
return displaySets.slice(begin, end);
|
||||||
}
|
}
|
||||||
|
|
||||||
getClosestDisplaySets(displaySets, activeDisplaySet, displaySetCount) {
|
/**
|
||||||
|
* Get all display set closest to the active display set.
|
||||||
|
*
|
||||||
|
* @param {array} displaySets
|
||||||
|
* @param {object} activeDisplaySet
|
||||||
|
* @param {number} displaySetCount
|
||||||
|
* @param {boolean} includeActiveDisplaySet
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getClosestDisplaySets(
|
||||||
|
displaySets,
|
||||||
|
activeDisplaySet,
|
||||||
|
displaySetCount,
|
||||||
|
includeActiveDisplaySet
|
||||||
|
) {
|
||||||
const activeDisplaySetIndex = displaySets.indexOf(activeDisplaySet);
|
const activeDisplaySetIndex = displaySets.indexOf(activeDisplaySet);
|
||||||
const length = displaySets.length;
|
const length = displaySets.length;
|
||||||
const selectedDisplaySets = [];
|
const selectedDisplaySets = [];
|
||||||
let left = activeDisplaySetIndex - 1;
|
let left = activeDisplaySetIndex - 1;
|
||||||
let right = activeDisplaySetIndex + 1;
|
let right = activeDisplaySetIndex + 1;
|
||||||
|
|
||||||
|
if (includeActiveDisplaySet) {
|
||||||
|
selectedDisplaySets.push(displaySets[activeDisplaySetIndex]);
|
||||||
|
displaySetCount--;
|
||||||
|
}
|
||||||
|
|
||||||
while ((left >= 0 || right < length) && displaySetCount) {
|
while ((left >= 0 || right < length) && displaySetCount) {
|
||||||
if (left >= 0) {
|
if (left >= 0) {
|
||||||
selectedDisplaySets.push(displaySets[left]);
|
selectedDisplaySets.push(displaySets[left]);
|
||||||
@ -215,6 +447,12 @@ export class StudyPrefetcher {
|
|||||||
return selectedDisplaySets;
|
return selectedDisplaySets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all image ids from display sets.
|
||||||
|
*
|
||||||
|
* @param {array} displaySets
|
||||||
|
* @returns {array} image ids
|
||||||
|
*/
|
||||||
getImageIdsFromDisplaySets(displaySets) {
|
getImageIdsFromDisplaySets(displaySets) {
|
||||||
let imageIds = [];
|
let imageIds = [];
|
||||||
|
|
||||||
@ -225,9 +463,19 @@ export class StudyPrefetcher {
|
|||||||
return imageIds;
|
return imageIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all image ids from a given display set.
|
||||||
|
*
|
||||||
|
* @param {array} displaySet
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
getImageIdsFromDisplaySet(displaySet) {
|
getImageIdsFromDisplaySet(displaySet) {
|
||||||
const imageIds = [];
|
const imageIds = [];
|
||||||
|
|
||||||
|
if (!displaySet.images || displaySet.images.length < 1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: This duplicates work done by the stack manager
|
// TODO: This duplicates work done by the stack manager
|
||||||
displaySet.images.forEach(image => {
|
displaySet.images.forEach(image => {
|
||||||
const numFrames = image.numFrames;
|
const numFrames = image.numFrames;
|
||||||
@ -245,15 +493,30 @@ export class StudyPrefetcher {
|
|||||||
return imageIds;
|
return imageIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter cached image ids from a set of image ids.
|
||||||
|
*
|
||||||
|
* @param {array} imageIds
|
||||||
|
* @returns {array} images not cached
|
||||||
|
*/
|
||||||
filterCachedImageIds(imageIds) {
|
filterCachedImageIds(imageIds) {
|
||||||
return imageIds.filter(imageId => !this.isImageCached(imageId));
|
return imageIds.filter(imageId => !this.isImageCached(imageId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if image id is cached in cornerstone.
|
||||||
|
*
|
||||||
|
* @param {string} imageId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
isImageCached(imageId) {
|
isImageCached(imageId) {
|
||||||
const image = cornerstone.imageCache.imageCache[imageId];
|
const image = cornerstone.imageCache.imageCache[imageId];
|
||||||
return image && image.sizeInBytes;
|
return image && image.sizeInBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warns that cache is full and stops prefetching.
|
||||||
|
*/
|
||||||
cacheFullHandler = () => {
|
cacheFullHandler = () => {
|
||||||
log.warn('Cache full');
|
log.warn('Cache full');
|
||||||
this.stopPrefetching();
|
this.stopPrefetching();
|
||||||
|
|||||||
@ -497,6 +497,12 @@ class StudyMetadata extends Metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._displaySets.some(ds =>
|
||||||
|
ds.displaySetInstanceUID === displaySet.displaySetInstanceUID)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._displaySets.splice(insertIndex, 0, displaySet);
|
this._displaySets.splice(insertIndex, 0, displaySet);
|
||||||
this.displaySets = this._displaySets;
|
this.displaySets = this._displaySets;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,17 +78,6 @@ export const setActiveViewportSpecificData = viewportSpecificData => ({
|
|||||||
/**
|
/**
|
||||||
* NOT-VIEWPORT
|
* NOT-VIEWPORT
|
||||||
*/
|
*/
|
||||||
export const setStudyLoadingProgress = (progressId, progressData) => ({
|
|
||||||
type: 'SET_STUDY_LOADING_PROGRESS',
|
|
||||||
progressId,
|
|
||||||
progressData,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const clearStudyLoadingProgress = progressId => ({
|
|
||||||
type: 'CLEAR_STUDY_LOADING_PROGRESS',
|
|
||||||
progressId,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setUserPreferences = state => ({
|
export const setUserPreferences = state => ({
|
||||||
type: SET_USER_PREFERENCES,
|
type: SET_USER_PREFERENCES,
|
||||||
state,
|
state,
|
||||||
@ -134,8 +123,6 @@ const actions = {
|
|||||||
/**
|
/**
|
||||||
* NOT-VIEWPORT
|
* NOT-VIEWPORT
|
||||||
*/
|
*/
|
||||||
setStudyLoadingProgress,
|
|
||||||
clearStudyLoadingProgress,
|
|
||||||
setUserPreferences,
|
setUserPreferences,
|
||||||
setExtensionData,
|
setExtensionData,
|
||||||
setTimepoints,
|
setTimepoints,
|
||||||
|
|||||||
@ -11,8 +11,6 @@ describe('actions', () => {
|
|||||||
'setLayout',
|
'setLayout',
|
||||||
'clearViewportSpecificData',
|
'clearViewportSpecificData',
|
||||||
'setActiveViewportSpecificData',
|
'setActiveViewportSpecificData',
|
||||||
'setStudyLoadingProgress',
|
|
||||||
'clearStudyLoadingProgress',
|
|
||||||
'setUserPreferences',
|
'setUserPreferences',
|
||||||
'setExtensionData',
|
'setExtensionData',
|
||||||
'setTimepoints',
|
'setTimepoints',
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import extensions from './extensions';
|
import extensions from './extensions';
|
||||||
import loading from './loading';
|
|
||||||
import preferences from './preferences';
|
import preferences from './preferences';
|
||||||
import servers from './servers';
|
import servers from './servers';
|
||||||
import studies from './studies';
|
import studies from './studies';
|
||||||
@ -8,7 +7,6 @@ import viewports from './viewports';
|
|||||||
|
|
||||||
const reducers = {
|
const reducers = {
|
||||||
extensions,
|
extensions,
|
||||||
loading,
|
|
||||||
preferences,
|
preferences,
|
||||||
servers,
|
servers,
|
||||||
studies,
|
studies,
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
import cloneDeep from 'lodash.clonedeep';
|
|
||||||
|
|
||||||
const defaultState = {
|
|
||||||
progress: {},
|
|
||||||
lastUpdated: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const loading = (state = defaultState, action) => {
|
|
||||||
let progress;
|
|
||||||
let lastUpdated;
|
|
||||||
switch (action.type) {
|
|
||||||
case 'SET_STUDY_LOADING_PROGRESS':
|
|
||||||
progress = cloneDeep(state).progress;
|
|
||||||
progress[action.progressId] = action.progressData;
|
|
||||||
|
|
||||||
// This is a workaround so we can easily identify changes
|
|
||||||
// to the progress object without doing deep comparison.
|
|
||||||
lastUpdated = new Date().getTime();
|
|
||||||
|
|
||||||
return Object.assign({}, state, { progress, lastUpdated });
|
|
||||||
case 'CLEAR_STUDY_LOADING_PROGRESS':
|
|
||||||
progress = cloneDeep(state).progress;
|
|
||||||
delete progress[action.progressId];
|
|
||||||
|
|
||||||
lastUpdated = new Date().getTime();
|
|
||||||
|
|
||||||
return Object.assign({}, state, { progress, lastUpdated });
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default loading;
|
|
||||||
@ -2,7 +2,7 @@
|
|||||||
import './ImageThumbnail.styl';
|
import './ImageThumbnail.styl';
|
||||||
|
|
||||||
import { utils } from '@ohif/core';
|
import { utils } from '@ohif/core';
|
||||||
import React, { useState, useEffect, createRef } from 'react';
|
import React, { useState, useEffect, createRef, useCallback } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@ -23,6 +23,7 @@ function ImageThumbnail(props) {
|
|||||||
imageId,
|
imageId,
|
||||||
stackPercentComplete,
|
stackPercentComplete,
|
||||||
error: propsError,
|
error: propsError,
|
||||||
|
showProgressBar,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [isLoading, setLoading] = useState(false);
|
const [isLoading, setLoading] = useState(false);
|
||||||
@ -39,7 +40,8 @@ function ImageThumbnail(props) {
|
|||||||
loadingOrError = <ViewportLoadingIndicator />;
|
loadingOrError = <ViewportLoadingIndicator />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showStackLoadingProgressBar = stackPercentComplete !== undefined;
|
const showStackLoadingProgressBar =
|
||||||
|
showProgressBar && stackPercentComplete !== undefined;
|
||||||
|
|
||||||
const shouldRenderToCanvas = () => {
|
const shouldRenderToCanvas = () => {
|
||||||
return imageId && !imageSrc;
|
return imageId && !imageSrc;
|
||||||
@ -71,11 +73,11 @@ function ImageThumbnail(props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const purgeCancelablePromise = () => {
|
const purgeCancelablePromise = useCallback(() => {
|
||||||
if (cancelablePromise) {
|
if (cancelablePromise) {
|
||||||
cancelablePromise.cancel();
|
cancelablePromise.cancel();
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@ -96,7 +98,13 @@ function ImageThumbnail(props) {
|
|||||||
setImagePromise();
|
setImagePromise();
|
||||||
fetchImagePromise();
|
fetchImagePromise();
|
||||||
}
|
}
|
||||||
}, [fetchImagePromise, image.imageId, imageId, purgeCancelablePromise, setImagePromise]);
|
}, [
|
||||||
|
fetchImagePromise,
|
||||||
|
image.imageId,
|
||||||
|
imageId,
|
||||||
|
purgeCancelablePromise,
|
||||||
|
setImagePromise,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('ImageThumbnail', { active: active })}>
|
<div className={classNames('ImageThumbnail', { active: active })}>
|
||||||
@ -104,14 +112,14 @@ function ImageThumbnail(props) {
|
|||||||
{shouldRenderToCanvas() ? (
|
{shouldRenderToCanvas() ? (
|
||||||
<canvas ref={canvasRef} width={width} height={height} />
|
<canvas ref={canvasRef} width={width} height={height} />
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
className="static-image"
|
className="static-image"
|
||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
//width={this.props.width}
|
//width={this.props.width}
|
||||||
height={height}
|
height={height}
|
||||||
alt={''}
|
alt={''}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{loadingOrError}
|
{loadingOrError}
|
||||||
{showStackLoadingProgressBar && (
|
{showStackLoadingProgressBar && (
|
||||||
@ -135,6 +143,7 @@ ImageThumbnail.propTypes = {
|
|||||||
width: PropTypes.number,
|
width: PropTypes.number,
|
||||||
height: PropTypes.number,
|
height: PropTypes.number,
|
||||||
stackPercentComplete: PropTypes.number.isRequired,
|
stackPercentComplete: PropTypes.number.isRequired,
|
||||||
|
showProgressBar: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
ImageThumbnail.defaultProps = {
|
ImageThumbnail.defaultProps = {
|
||||||
@ -143,6 +152,7 @@ ImageThumbnail.defaultProps = {
|
|||||||
stackPercentComplete: 0,
|
stackPercentComplete: 0,
|
||||||
width: 217,
|
width: 217,
|
||||||
height: 123,
|
height: 123,
|
||||||
|
showProgressBar: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ImageThumbnail;
|
export default ImageThumbnail;
|
||||||
|
|||||||
@ -9,6 +9,7 @@ function StudyBrowser(props) {
|
|||||||
onThumbnailClick,
|
onThumbnailClick,
|
||||||
onThumbnailDoubleClick,
|
onThumbnailDoubleClick,
|
||||||
supportsDrag,
|
supportsDrag,
|
||||||
|
showThumbnailProgressBar,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -28,7 +29,6 @@ function StudyBrowser(props) {
|
|||||||
numImageFrames,
|
numImageFrames,
|
||||||
SeriesDescription,
|
SeriesDescription,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
stackPercentComplete,
|
|
||||||
hasWarnings,
|
hasWarnings,
|
||||||
} = thumb;
|
} = thumb;
|
||||||
|
|
||||||
@ -54,13 +54,13 @@ function StudyBrowser(props) {
|
|||||||
SeriesDescription={SeriesDescription}
|
SeriesDescription={SeriesDescription}
|
||||||
SeriesNumber={SeriesNumber}
|
SeriesNumber={SeriesNumber}
|
||||||
hasWarnings={hasWarnings}
|
hasWarnings={hasWarnings}
|
||||||
stackPercentComplete={stackPercentComplete}
|
|
||||||
// Events
|
// Events
|
||||||
onClick={onThumbnailClick.bind(
|
onClick={onThumbnailClick.bind(
|
||||||
undefined,
|
undefined,
|
||||||
displaySetInstanceUID
|
displaySetInstanceUID
|
||||||
)}
|
)}
|
||||||
onDoubleClick={onThumbnailDoubleClick}
|
onDoubleClick={onThumbnailDoubleClick}
|
||||||
|
showProgressBar={showThumbnailProgressBar}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -95,6 +95,7 @@ StudyBrowser.propTypes = {
|
|||||||
supportsDrag: PropTypes.bool,
|
supportsDrag: PropTypes.bool,
|
||||||
onThumbnailClick: PropTypes.func,
|
onThumbnailClick: PropTypes.func,
|
||||||
onThumbnailDoubleClick: PropTypes.func,
|
onThumbnailDoubleClick: PropTypes.func,
|
||||||
|
showThumbnailProgressBar: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
StudyBrowser.defaultProps = {
|
StudyBrowser.defaultProps = {
|
||||||
@ -102,6 +103,7 @@ StudyBrowser.defaultProps = {
|
|||||||
supportsDrag: true,
|
supportsDrag: true,
|
||||||
onThumbnailClick: noop,
|
onThumbnailClick: noop,
|
||||||
onThumbnailDoubleClick: noop,
|
onThumbnailDoubleClick: noop,
|
||||||
|
showThumbnailProgressBar: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { StudyBrowser };
|
export { StudyBrowser };
|
||||||
|
|||||||
@ -1,34 +1,36 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useDrag } from 'react-dnd';
|
import { useDrag } from 'react-dnd';
|
||||||
|
import { classes } from '@ohif/core';
|
||||||
import ImageThumbnail from './ImageThumbnail';
|
import ImageThumbnail from './ImageThumbnail';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Icon } from './../../elements/Icon';
|
import { Icon } from './../../elements/Icon';
|
||||||
import { Tooltip } from './../tooltip';
|
import { Tooltip } from './../tooltip';
|
||||||
import { OverlayTrigger } from './../overlayTrigger';
|
import { OverlayTrigger } from './../overlayTrigger';
|
||||||
|
|
||||||
import './Thumbnail.styl';
|
import './Thumbnail.styl';
|
||||||
|
|
||||||
|
const StudyLoadingListener = classes.StudyLoadingListener;
|
||||||
|
|
||||||
function ThumbnailFooter({
|
function ThumbnailFooter({
|
||||||
SeriesDescription,
|
SeriesDescription,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
InstanceNumber,
|
InstanceNumber,
|
||||||
numImageFrames,
|
numImageFrames,
|
||||||
hasWarnings
|
hasWarnings,
|
||||||
}) {
|
}) {
|
||||||
const [inconsistencyWarnings, inconsistencyWarningsSet] = useState([]);
|
const [inconsistencyWarnings, inconsistencyWarningsSet] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unmounted = false
|
let unmounted = false;
|
||||||
hasWarnings.then(response => {
|
hasWarnings.then(response => {
|
||||||
if (!unmounted) {
|
if (!unmounted) {
|
||||||
inconsistencyWarningsSet(response)
|
inconsistencyWarningsSet(response);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
return () => {
|
return () => {
|
||||||
unmounted = true
|
unmounted = true;
|
||||||
}
|
};
|
||||||
}, [])
|
}, [hasWarnings]);
|
||||||
|
|
||||||
const infoOnly = !SeriesDescription;
|
const infoOnly = !SeriesDescription;
|
||||||
|
|
||||||
@ -41,7 +43,7 @@ function ThumbnailFooter({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getWarningContent = (inconsistencyWarnings) => {
|
const getWarningContent = inconsistencyWarnings => {
|
||||||
if (Array.isArray(inconsistencyWarnings)) {
|
if (Array.isArray(inconsistencyWarnings)) {
|
||||||
const listedWarnings = inconsistencyWarnings.map((warn, index) => {
|
const listedWarnings = inconsistencyWarnings.map((warn, index) => {
|
||||||
return <li key={index}>{warn}</li>;
|
return <li key={index}>{warn}</li>;
|
||||||
@ -54,8 +56,8 @@ function ThumbnailFooter({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getWarningInfo = (SeriesNumber, inconsistencyWarnings) => {
|
const getWarningInfo = (SeriesNumber, inconsistencyWarnings) => {
|
||||||
return(
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{inconsistencyWarnings && inconsistencyWarnings.length != 0 ? (
|
{inconsistencyWarnings && inconsistencyWarnings.length != 0 ? (
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
key={SeriesNumber}
|
key={SeriesNumber}
|
||||||
@ -67,7 +69,9 @@ function ThumbnailFooter({
|
|||||||
id="tooltip-left"
|
id="tooltip-left"
|
||||||
>
|
>
|
||||||
<div className="warningTitle">Series Inconsistencies</div>
|
<div className="warningTitle">Series Inconsistencies</div>
|
||||||
<div className="warningContent">{getWarningContent(inconsistencyWarnings)}</div>
|
<div className="warningContent">
|
||||||
|
{getWarningContent(inconsistencyWarnings)}
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -79,9 +83,9 @@ function ThumbnailFooter({
|
|||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
) : (
|
) : (
|
||||||
<React.Fragment></React.Fragment>
|
<React.Fragment></React.Fragment>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const getSeriesInformation = (
|
const getSeriesInformation = (
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
@ -92,21 +96,27 @@ function ThumbnailFooter({
|
|||||||
if (!SeriesNumber && !InstanceNumber && !numImageFrames) {
|
if (!SeriesNumber && !InstanceNumber && !numImageFrames) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const seriesInformation =
|
const seriesInformation = (
|
||||||
<div className="series-information">
|
<div className="series-information">
|
||||||
{getInfo(SeriesNumber, 'S:')}
|
{getInfo(SeriesNumber, 'S:')}
|
||||||
{getInfo(InstanceNumber, 'I:')}
|
{getInfo(InstanceNumber, 'I:')}
|
||||||
{getInfo(numImageFrames, '', 'image-frames')}
|
{getInfo(numImageFrames, '', 'image-frames')}
|
||||||
{getWarningInfo(SeriesNumber, inconsistencyWarnings)}
|
{getWarningInfo(SeriesNumber, inconsistencyWarnings)}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (seriesInformation);
|
return seriesInformation;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('series-details', { 'info-only': infoOnly })}>
|
<div className={classNames('series-details', { 'info-only': infoOnly })}>
|
||||||
<div className="series-description">{SeriesDescription}</div>
|
<div className="series-description">{SeriesDescription}</div>
|
||||||
{getSeriesInformation(SeriesNumber, InstanceNumber, numImageFrames, inconsistencyWarnings)}
|
{getSeriesInformation(
|
||||||
|
SeriesNumber,
|
||||||
|
InstanceNumber,
|
||||||
|
numImageFrames,
|
||||||
|
inconsistencyWarnings
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -124,14 +134,37 @@ function Thumbnail(props) {
|
|||||||
SeriesDescription,
|
SeriesDescription,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
hasWarnings,
|
hasWarnings,
|
||||||
stackPercentComplete,
|
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
onClick,
|
onClick,
|
||||||
onDoubleClick,
|
onDoubleClick,
|
||||||
onMouseDown,
|
onMouseDown,
|
||||||
supportsDrag,
|
supportsDrag,
|
||||||
|
showProgressBar,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const [stackPercentComplete, setStackPercentComplete] = useState(0);
|
||||||
|
useEffect(() => {
|
||||||
|
const onProgressChange = ({ detail }) => {
|
||||||
|
const { progressId, progressData } = detail;
|
||||||
|
if (`StackProgress:${displaySetInstanceUID}` === progressId) {
|
||||||
|
const percent = progressData ? progressData.percentComplete : 0;
|
||||||
|
setStackPercentComplete(percent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener(
|
||||||
|
StudyLoadingListener.events.OnProgress,
|
||||||
|
onProgressChange
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener(
|
||||||
|
StudyLoadingListener.events.OnProgress,
|
||||||
|
onProgressChange
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [displaySetInstanceUID]);
|
||||||
|
|
||||||
const [collectedProps, drag, dragPreview] = useDrag({
|
const [collectedProps, drag, dragPreview] = useDrag({
|
||||||
// `droppedItem` in `dropTarget`
|
// `droppedItem` in `dropTarget`
|
||||||
// The only data it will have access to
|
// The only data it will have access to
|
||||||
@ -164,6 +197,7 @@ function Thumbnail(props) {
|
|||||||
imageId={imageId}
|
imageId={imageId}
|
||||||
error={error}
|
error={error}
|
||||||
stackPercentComplete={stackPercentComplete}
|
stackPercentComplete={stackPercentComplete}
|
||||||
|
showProgressBar={showProgressBar}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* SHOW TEXT ALTERNATIVE */}
|
{/* SHOW TEXT ALTERNATIVE */}
|
||||||
@ -203,6 +237,7 @@ Thumbnail.propTypes = {
|
|||||||
onDoubleClick: PropTypes.func,
|
onDoubleClick: PropTypes.func,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
onMouseDown: PropTypes.func,
|
onMouseDown: PropTypes.func,
|
||||||
|
showProgressBar: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
Thumbnail.defaultProps = {
|
Thumbnail.defaultProps = {
|
||||||
|
|||||||
@ -5,6 +5,15 @@ window.config = {
|
|||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
filterQueryParam: false,
|
filterQueryParam: false,
|
||||||
disableServersCache: false,
|
disableServersCache: false,
|
||||||
|
studyPrefetcher: {
|
||||||
|
enabled: true,
|
||||||
|
order: 'closest',
|
||||||
|
displaySetCount: 3,
|
||||||
|
preventCache: false,
|
||||||
|
prefetchDisplaySetsTimeout: 300,
|
||||||
|
displayProgress: true,
|
||||||
|
includeActiveDisplaySet: true,
|
||||||
|
},
|
||||||
servers: {
|
servers: {
|
||||||
dicomWeb: [
|
dicomWeb: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +1,15 @@
|
|||||||
window.config = {
|
window.config = {
|
||||||
routerBasename: '/pwa',
|
routerBasename: '/pwa',
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
|
studyPrefetcher: {
|
||||||
|
enabled: true,
|
||||||
|
order: 'closest',
|
||||||
|
displaySetCount: 3,
|
||||||
|
preventCache: false,
|
||||||
|
prefetchDisplaySetsTimeout: 300,
|
||||||
|
displayProgress: true,
|
||||||
|
includeActiveDisplaySet: true,
|
||||||
|
},
|
||||||
servers: {
|
servers: {
|
||||||
dicomWeb: [
|
dicomWeb: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,42 +1,23 @@
|
|||||||
import { Component } from "react";
|
import { useEffect } from 'react';
|
||||||
import PropTypes from "prop-types";
|
import OHIF from '@ohif/core';
|
||||||
import OHIF from "@ohif/core";
|
|
||||||
|
|
||||||
class StudyLoadingMonitor extends Component {
|
const { StudyLoadingListener } = OHIF.classes;
|
||||||
static propTypes = {
|
|
||||||
studies: PropTypes.array.isRequired,
|
|
||||||
setStudyLoadingProgress: PropTypes.func.isRequired,
|
|
||||||
clearStudyLoadingProgress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
const StudyLoadingMonitor = ({ studies }) => {
|
||||||
// TODO: This is pretty ugly. The thing is that the StudyLoadingListener
|
useEffect(() => {
|
||||||
// needs to update the Redux store, but shouldn't know that it exists.
|
const studyLoadingListener = StudyLoadingListener.getInstance();
|
||||||
// I am therefore passing in some functions to update the store instead,
|
|
||||||
// but this should definitely be cleaned up somehow.
|
if (studies && studies.length > 0) {
|
||||||
const options = {
|
studyLoadingListener.clear();
|
||||||
_setProgressData: (progressId, progressData) => {
|
studyLoadingListener.addStudies(studies);
|
||||||
this.props.setStudyLoadingProgress(progressId, progressData);
|
}
|
||||||
},
|
|
||||||
_clearProgressById: progressId => {
|
return () => {
|
||||||
this.props.clearStudyLoadingProgress(progressId);
|
studyLoadingListener.clear();
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
}, [studies]);
|
||||||
|
|
||||||
const { StudyLoadingListener } = OHIF.classes;
|
return null;
|
||||||
this.studyLoadingListener = StudyLoadingListener.getInstance(options);
|
};
|
||||||
this.studyLoadingListener.clear();
|
|
||||||
this.studyLoadingListener.addStudies(this.props.studies);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
// Destroy stack loading listeners when we close the viewer
|
|
||||||
this.studyLoadingListener.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StudyLoadingMonitor;
|
export default StudyLoadingMonitor;
|
||||||
|
|||||||
15
platform/viewer/src/components/StudyPrefetcher.css
Normal file
15
platform/viewer/src/components/StudyPrefetcher.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.StudyPrefetcher {
|
||||||
|
height: 5px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
background-color: var(--default-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.StudyPrefetcher .item {
|
||||||
|
height: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.StudyPrefetcher .item.cached {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
@ -1,29 +1,93 @@
|
|||||||
import { Component } from "react";
|
import React, { useEffect } from 'react';
|
||||||
import PropTypes from "prop-types";
|
import { classes, utils } from '@ohif/core';
|
||||||
import OHIF from "@ohif/core";
|
import PropTypes from 'prop-types';
|
||||||
|
import cs from 'cornerstone-core';
|
||||||
|
|
||||||
const { StudyPrefetcher } = OHIF.classes;
|
import './StudyPrefetcher.css';
|
||||||
|
|
||||||
class StudyPrefetcherComponent extends Component {
|
const StudyPrefetcher = ({ studies, options }) => {
|
||||||
static propTypes = {
|
useEffect(() => {
|
||||||
studies: PropTypes.array
|
const studyPrefetcher = classes.StudyPrefetcher.getInstance(
|
||||||
};
|
studies,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const studiesMetadata = studies.map(s =>
|
||||||
|
utils.studyMetadataManager.get(s.StudyInstanceUID)
|
||||||
|
);
|
||||||
|
studyPrefetcher.setStudies(studiesMetadata);
|
||||||
|
|
||||||
componentDidMount() {
|
const onNewImage = ({ detail }) => {
|
||||||
const { studies } = this.props;
|
/**
|
||||||
|
* When images are cached the viewport will load instantly and
|
||||||
|
* the display sets will not be available at this point in time.
|
||||||
|
*
|
||||||
|
* This code add display sets and updates the study prefetcher metadata.
|
||||||
|
*/
|
||||||
|
const studiesMetadata = studies.map(s => {
|
||||||
|
const studyMetadata = utils.studyMetadataManager.get(
|
||||||
|
s.StudyInstanceUID
|
||||||
|
);
|
||||||
|
const displaySets = studyMetadata.getDisplaySets();
|
||||||
|
if (!displaySets || displaySets.length < 1) {
|
||||||
|
s.displaySets.forEach(ds => studyMetadata.addDisplaySet(ds));
|
||||||
|
}
|
||||||
|
return studyMetadata;
|
||||||
|
});
|
||||||
|
studyPrefetcher.setStudies(studiesMetadata);
|
||||||
|
|
||||||
this.studyPrefetcher = StudyPrefetcher.getInstance();
|
const study = studyPrefetcher.getStudy(detail.image);
|
||||||
this.studyPrefetcher.setStudies(studies);
|
const series = studyPrefetcher.getSeries(study, detail.image);
|
||||||
}
|
const instance = studyPrefetcher.getInstance(series, detail.image);
|
||||||
|
|
||||||
render() {
|
if (study.displaySets && study.displaySets.length > 0) {
|
||||||
return null;
|
const {
|
||||||
}
|
displaySetInstanceUID,
|
||||||
|
} = studyPrefetcher.getDisplaySetBySOPInstanceUID(
|
||||||
|
study.displaySets,
|
||||||
|
instance
|
||||||
|
);
|
||||||
|
studyPrefetcher.prefetch(detail.element, displaySetInstanceUID);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
componentWillUnmount() {
|
const onElementEnabled = ({ detail }) => {
|
||||||
// Stop prefetching when we close the viewer
|
detail.element.addEventListener(cs.EVENTS.NEW_IMAGE, onNewImage);
|
||||||
this.studyPrefetcher.destroy();
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StudyPrefetcherComponent;
|
cs.events.addEventListener(cs.EVENTS.ELEMENT_ENABLED, onElementEnabled);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cs.events.removeEventListener(
|
||||||
|
cs.EVENTS.ELEMENT_ENABLED,
|
||||||
|
onElementEnabled
|
||||||
|
);
|
||||||
|
studyPrefetcher.destroy();
|
||||||
|
};
|
||||||
|
}, [options, studies]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
StudyPrefetcher.propTypes = {
|
||||||
|
studies: PropTypes.array.isRequired,
|
||||||
|
options: PropTypes.shape({
|
||||||
|
enabled: PropTypes.bool,
|
||||||
|
order: PropTypes.string,
|
||||||
|
displaySetCount: PropTypes.number,
|
||||||
|
preventCache: PropTypes.bool,
|
||||||
|
prefetchDisplaySetsTimeout: PropTypes.number,
|
||||||
|
includeActiveDisplaySet: PropTypes.bool,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
StudyPrefetcher.defaultProps = {
|
||||||
|
options: {
|
||||||
|
order: 'closest',
|
||||||
|
displaySetCount: 1,
|
||||||
|
preventCache: false,
|
||||||
|
prefetchDisplaySetsTimeout: 300,
|
||||||
|
includeActiveDisplaySet: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StudyPrefetcher;
|
||||||
|
|||||||
@ -1,44 +1,11 @@
|
|||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { StudyBrowser } from '@ohif/ui';
|
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
|
||||||
import findDisplaySetByUID from './findDisplaySetByUID';
|
import findDisplaySetByUID from './findDisplaySetByUID';
|
||||||
import { servicesManager } from './../App.js';
|
import { servicesManager } from './../App.js';
|
||||||
|
import { StudyBrowser } from '../../../ui/src/components/studyBrowser/StudyBrowser';
|
||||||
const { studyMetadataManager } = OHIF.utils;
|
|
||||||
|
|
||||||
const { setActiveViewportSpecificData } = OHIF.redux.actions;
|
const { setActiveViewportSpecificData } = OHIF.redux.actions;
|
||||||
|
|
||||||
// TODO
|
|
||||||
// - Determine in which display set is active from Redux (activeViewportIndex and layout viewportData)
|
|
||||||
// - Pass in errors and stack loading progress from Redux
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
|
||||||
// If we know that the stack loading progress details have changed,
|
|
||||||
// we can try to update the component state so that the thumbnail
|
|
||||||
// progress bar is updated
|
|
||||||
const stackLoadingProgressMap = state.loading.progress;
|
|
||||||
const studiesWithLoadingData = cloneDeep(ownProps.studies);
|
|
||||||
|
|
||||||
studiesWithLoadingData.forEach(study => {
|
|
||||||
study.thumbnails.forEach(data => {
|
|
||||||
const { displaySetInstanceUID } = data;
|
|
||||||
const stackId = `StackProgress:${displaySetInstanceUID}`;
|
|
||||||
const stackProgressData = stackLoadingProgressMap[stackId];
|
|
||||||
|
|
||||||
let stackPercentComplete = 0;
|
|
||||||
if (stackProgressData) {
|
|
||||||
stackPercentComplete = stackProgressData.percentComplete;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.stackPercentComplete = stackPercentComplete;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
studies: studiesWithLoadingData,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, ownProps) => {
|
const mapDispatchToProps = (dispatch, ownProps) => {
|
||||||
return {
|
return {
|
||||||
onThumbnailClick: displaySetInstanceUID => {
|
onThumbnailClick: displaySetInstanceUID => {
|
||||||
@ -50,7 +17,10 @@ const mapDispatchToProps = (dispatch, ownProps) => {
|
|||||||
if (displaySet.isDerived) {
|
if (displaySet.isDerived) {
|
||||||
const { Modality } = displaySet;
|
const { Modality } = displaySet;
|
||||||
if (Modality === 'SEG' && servicesManager) {
|
if (Modality === 'SEG' && servicesManager) {
|
||||||
const {LoggerService, UINotificationService} = servicesManager.services;
|
const {
|
||||||
|
LoggerService,
|
||||||
|
UINotificationService,
|
||||||
|
} = servicesManager.services;
|
||||||
const onDisplaySetLoadFailureHandler = error => {
|
const onDisplaySetLoadFailureHandler = error => {
|
||||||
LoggerService.error({ error, message: error.message });
|
LoggerService.error({ error, message: error.message });
|
||||||
UINotificationService.show({
|
UINotificationService.show({
|
||||||
@ -61,20 +31,25 @@ const mapDispatchToProps = (dispatch, ownProps) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const {referencedDisplaySet, activatedLabelmapPromise} = displaySet.getSourceDisplaySet(
|
const {
|
||||||
|
referencedDisplaySet,
|
||||||
|
activatedLabelmapPromise,
|
||||||
|
} = displaySet.getSourceDisplaySet(
|
||||||
ownProps.studyMetadata,
|
ownProps.studyMetadata,
|
||||||
true,
|
true,
|
||||||
onDisplaySetLoadFailureHandler
|
onDisplaySetLoadFailureHandler
|
||||||
);
|
);
|
||||||
displaySet = referencedDisplaySet;
|
displaySet = referencedDisplaySet;
|
||||||
|
|
||||||
activatedLabelmapPromise.then((activatedLabelmapIndex) => {
|
activatedLabelmapPromise.then(activatedLabelmapIndex => {
|
||||||
const selectionFired = new CustomEvent("extensiondicomsegmentationsegselected", {
|
const selectionFired = new CustomEvent(
|
||||||
"detail": {"activatedLabelmapIndex":activatedLabelmapIndex}
|
'extensiondicomsegmentationsegselected',
|
||||||
});
|
{
|
||||||
|
detail: { activatedLabelmapIndex: activatedLabelmapIndex },
|
||||||
|
}
|
||||||
|
);
|
||||||
document.dispatchEvent(selectionFired);
|
document.dispatchEvent(selectionFired);
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
displaySet = displaySet.getSourceDisplaySet(ownProps.studyMetadata);
|
displaySet = displaySet.getSourceDisplaySet(ownProps.studyMetadata);
|
||||||
}
|
}
|
||||||
@ -96,7 +71,7 @@ const mapDispatchToProps = (dispatch, ownProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ConnectedStudyBrowser = connect(
|
const ConnectedStudyBrowser = connect(
|
||||||
mapStateToProps,
|
null,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(StudyBrowser);
|
)(StudyBrowser);
|
||||||
|
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
import { connect } from "react-redux";
|
|
||||||
import StudyLoadingMonitor from "../components/StudyLoadingMonitor.js";
|
|
||||||
import OHIF from "@ohif/core";
|
|
||||||
|
|
||||||
const {
|
|
||||||
setStudyLoadingProgress,
|
|
||||||
clearStudyLoadingProgress
|
|
||||||
} = OHIF.redux.actions;
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
|
||||||
return {
|
|
||||||
setStudyLoadingProgress: (progressId, progressData) => {
|
|
||||||
dispatch(setStudyLoadingProgress(progressId, progressData));
|
|
||||||
},
|
|
||||||
clearStudyLoadingProgress: progressId => {
|
|
||||||
dispatch(clearStudyLoadingProgress(progressId));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedStudyLoadingMonitor = connect(
|
|
||||||
null,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(StudyLoadingMonitor);
|
|
||||||
|
|
||||||
export default ConnectedStudyLoadingMonitor;
|
|
||||||
@ -1,11 +1,10 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useLogger } from '@ohif/ui';
|
|
||||||
|
|
||||||
import OHIF, { MODULE_TYPES, DICOMSR } from '@ohif/core';
|
import OHIF, { MODULE_TYPES, DICOMSR } from '@ohif/core';
|
||||||
import { withDialog } from '@ohif/ui';
|
import { withDialog } from '@ohif/ui';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
import ConnectedHeader from './ConnectedHeader.js';
|
import ConnectedHeader from './ConnectedHeader.js';
|
||||||
import ToolbarRow from './ToolbarRow.js';
|
import ToolbarRow from './ToolbarRow.js';
|
||||||
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
||||||
@ -14,7 +13,6 @@ import SidePanel from './../components/SidePanel.js';
|
|||||||
import ErrorBoundaryDialog from './../components/ErrorBoundaryDialog';
|
import ErrorBoundaryDialog from './../components/ErrorBoundaryDialog';
|
||||||
import { extensionManager } from './../App.js';
|
import { extensionManager } from './../App.js';
|
||||||
import { ReconstructionIssues } from './../../../core/src/enums.js';
|
import { ReconstructionIssues } from './../../../core/src/enums.js';
|
||||||
import dcmjs from 'dcmjs';
|
|
||||||
|
|
||||||
// Contexts
|
// Contexts
|
||||||
import WhiteLabelingContext from '../context/WhiteLabelingContext.js';
|
import WhiteLabelingContext from '../context/WhiteLabelingContext.js';
|
||||||
@ -22,8 +20,8 @@ import UserManagerContext from '../context/UserManagerContext';
|
|||||||
import AppContext from '../context/AppContext';
|
import AppContext from '../context/AppContext';
|
||||||
|
|
||||||
import './Viewer.css';
|
import './Viewer.css';
|
||||||
import { finished } from 'stream';
|
import StudyPrefetcher from '../components/StudyPrefetcher.js';
|
||||||
import { cornerstoneWADOImageLoader } from 'cornerstone-wado-image-loader';
|
import StudyLoadingMonitor from '../components/StudyLoadingMonitor';
|
||||||
|
|
||||||
class Viewer extends Component {
|
class Viewer extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -197,11 +195,17 @@ class Viewer extends Component {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeViewport = this.props.viewports[this.props.activeViewportIndex];
|
const activeViewport = this.props.viewports[
|
||||||
const activeDisplaySetInstanceUID =
|
this.props.activeViewportIndex
|
||||||
activeViewport ? activeViewport.displaySetInstanceUID : undefined;
|
];
|
||||||
|
const activeDisplaySetInstanceUID = activeViewport
|
||||||
|
? activeViewport.displaySetInstanceUID
|
||||||
|
: undefined;
|
||||||
this.setState({
|
this.setState({
|
||||||
thumbnails: _mapStudiesToThumbnails(studies, activeDisplaySetInstanceUID),
|
thumbnails: _mapStudiesToThumbnails(
|
||||||
|
studies,
|
||||||
|
activeDisplaySetInstanceUID
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,24 +215,31 @@ class Viewer extends Component {
|
|||||||
studies,
|
studies,
|
||||||
isStudyLoaded,
|
isStudyLoaded,
|
||||||
activeViewportIndex,
|
activeViewportIndex,
|
||||||
viewports
|
viewports,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const activeViewport = viewports[activeViewportIndex];
|
const activeViewport = viewports[activeViewportIndex];
|
||||||
const activeDisplaySetInstanceUID =
|
const activeDisplaySetInstanceUID = activeViewport
|
||||||
activeViewport ? activeViewport.displaySetInstanceUID : undefined;
|
? activeViewport.displaySetInstanceUID
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const prevActiveViewport = prevProps.viewports[prevProps.activeViewportIndex];
|
const prevActiveViewport =
|
||||||
const prevActiveDisplaySetInstanceUID =
|
prevProps.viewports[prevProps.activeViewportIndex];
|
||||||
prevActiveViewport ? prevActiveViewport.displaySetInstanceUID : undefined;
|
const prevActiveDisplaySetInstanceUID = prevActiveViewport
|
||||||
|
? prevActiveViewport.displaySetInstanceUID
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (studies !== prevProps.studies ||
|
if (
|
||||||
|
studies !== prevProps.studies ||
|
||||||
activeViewportIndex !== prevProps.activeViewportIndex ||
|
activeViewportIndex !== prevProps.activeViewportIndex ||
|
||||||
activeDisplaySetInstanceUID !== prevActiveDisplaySetInstanceUID
|
activeDisplaySetInstanceUID !== prevActiveDisplaySetInstanceUID
|
||||||
) {
|
) {
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
thumbnails: _mapStudiesToThumbnails(studies, activeDisplaySetInstanceUID),
|
thumbnails: _mapStudiesToThumbnails(
|
||||||
|
studies,
|
||||||
|
activeDisplaySetInstanceUID
|
||||||
|
),
|
||||||
|
activeDisplaySetInstanceUID,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (isStudyLoaded && isStudyLoaded !== prevProps.isStudyLoaded) {
|
if (isStudyLoaded && isStudyLoaded !== prevProps.isStudyLoaded) {
|
||||||
@ -288,14 +299,12 @@ class Viewer extends Component {
|
|||||||
</UserManagerContext.Consumer>
|
</UserManagerContext.Consumer>
|
||||||
)}
|
)}
|
||||||
</WhiteLabelingContext.Consumer>
|
</WhiteLabelingContext.Consumer>
|
||||||
|
|
||||||
{/* TOOLBAR */}
|
{/* TOOLBAR */}
|
||||||
<ErrorBoundaryDialog context="ToolbarRow">
|
<ErrorBoundaryDialog context="ToolbarRow">
|
||||||
<ToolbarRow
|
<ToolbarRow
|
||||||
activeViewport={
|
activeViewport={
|
||||||
this.props.viewports[this.props.activeViewportIndex]
|
this.props.viewports[this.props.activeViewportIndex]
|
||||||
}
|
}
|
||||||
isDerivedDisplaySetsLoaded={this.props.isDerivedDisplaySetsLoaded}
|
|
||||||
isLeftSidePanelOpen={this.state.isLeftSidePanelOpen}
|
isLeftSidePanelOpen={this.state.isLeftSidePanelOpen}
|
||||||
isRightSidePanelOpen={this.state.isRightSidePanelOpen}
|
isRightSidePanelOpen={this.state.isRightSidePanelOpen}
|
||||||
selectedLeftSidePanel={
|
selectedLeftSidePanel={
|
||||||
@ -332,10 +341,9 @@ class Viewer extends Component {
|
|||||||
studies={this.props.studies}
|
studies={this.props.studies}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundaryDialog>
|
</ErrorBoundaryDialog>
|
||||||
|
<AppContext.Consumer>
|
||||||
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
|
{appContext => <StudyLoadingMonitor studies={this.props.studies} />}
|
||||||
{/*<StudyPrefetcher studies={this.props.studies} />*/}
|
</AppContext.Consumer>
|
||||||
|
|
||||||
{/* VIEWPORTS + SIDEPANELS */}
|
{/* VIEWPORTS + SIDEPANELS */}
|
||||||
<div className="FlexboxLayout">
|
<div className="FlexboxLayout">
|
||||||
{/* LEFT */}
|
{/* LEFT */}
|
||||||
@ -348,10 +356,24 @@ class Viewer extends Component {
|
|||||||
activeIndex={this.props.activeViewportIndex}
|
activeIndex={this.props.activeViewportIndex}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ConnectedStudyBrowser
|
<AppContext.Consumer>
|
||||||
studies={this.state.thumbnails}
|
{appContext => {
|
||||||
studyMetadata={this.props.studies}
|
const { appConfig } = appContext;
|
||||||
/>
|
const { studyPrefetcher } = appConfig;
|
||||||
|
const { thumbnails } = this.state;
|
||||||
|
return (
|
||||||
|
<ConnectedStudyBrowser
|
||||||
|
studies={thumbnails}
|
||||||
|
studyMetadata={this.props.studies}
|
||||||
|
showThumbnailProgressBar={
|
||||||
|
studyPrefetcher &&
|
||||||
|
studyPrefetcher.enabled &&
|
||||||
|
studyPrefetcher.displayProgress
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</AppContext.Consumer>
|
||||||
)}
|
)}
|
||||||
</SidePanel>
|
</SidePanel>
|
||||||
</ErrorBoundaryDialog>
|
</ErrorBoundaryDialog>
|
||||||
@ -359,6 +381,22 @@ class Viewer extends Component {
|
|||||||
{/* MAIN */}
|
{/* MAIN */}
|
||||||
<div className={classNames('main-content')}>
|
<div className={classNames('main-content')}>
|
||||||
<ErrorBoundaryDialog context="ViewerMain">
|
<ErrorBoundaryDialog context="ViewerMain">
|
||||||
|
<AppContext.Consumer>
|
||||||
|
{appContext => {
|
||||||
|
const { appConfig } = appContext;
|
||||||
|
const { studyPrefetcher } = appConfig;
|
||||||
|
const { studies } = this.props;
|
||||||
|
return (
|
||||||
|
studyPrefetcher &&
|
||||||
|
studyPrefetcher.enabled && (
|
||||||
|
<StudyPrefetcher
|
||||||
|
studies={studies}
|
||||||
|
options={studyPrefetcher}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</AppContext.Consumer>
|
||||||
<ConnectedViewerMain
|
<ConnectedViewerMain
|
||||||
studies={this.props.studies}
|
studies={this.props.studies}
|
||||||
isStudyLoaded={this.props.isStudyLoaded}
|
isStudyLoaded={this.props.isStudyLoaded}
|
||||||
@ -410,7 +448,10 @@ export default withDialog(Viewer);
|
|||||||
* @param {*object} displaySet
|
* @param {*object} displaySet
|
||||||
* @returns {[string]} an array of strings containing the warnings
|
* @returns {[string]} an array of strings containing the warnings
|
||||||
*/
|
*/
|
||||||
const _checkForSeriesInconsistencesWarnings = async function (displaySet, studies) {
|
const _checkForSeriesInconsistencesWarnings = async function(
|
||||||
|
displaySet,
|
||||||
|
studies
|
||||||
|
) {
|
||||||
if (displaySet.inconsistencyWarnings) {
|
if (displaySet.inconsistencyWarnings) {
|
||||||
// warnings already checked and cached in displaySet
|
// warnings already checked and cached in displaySet
|
||||||
return displaySet.inconsistencyWarnings;
|
return displaySet.inconsistencyWarnings;
|
||||||
@ -418,23 +459,34 @@ const _checkForSeriesInconsistencesWarnings = async function (displaySet, studie
|
|||||||
const inconsistencyWarnings = [];
|
const inconsistencyWarnings = [];
|
||||||
|
|
||||||
if (displaySet.Modality !== 'SEG') {
|
if (displaySet.Modality !== 'SEG') {
|
||||||
if (displaySet.reconstructionIssues && displaySet.reconstructionIssues.length !== 0) {
|
if (
|
||||||
|
displaySet.reconstructionIssues &&
|
||||||
|
displaySet.reconstructionIssues.length !== 0
|
||||||
|
) {
|
||||||
displaySet.reconstructionIssues.forEach(warning => {
|
displaySet.reconstructionIssues.forEach(warning => {
|
||||||
switch (warning) {
|
switch (warning) {
|
||||||
case ReconstructionIssues.DATASET_4D:
|
case ReconstructionIssues.DATASET_4D:
|
||||||
inconsistencyWarnings.push('The dataset is 4D.');
|
inconsistencyWarnings.push('The dataset is 4D.');
|
||||||
break;
|
break;
|
||||||
case ReconstructionIssues.VARYING_IMAGESDIMENSIONS:
|
case ReconstructionIssues.VARYING_IMAGESDIMENSIONS:
|
||||||
inconsistencyWarnings.push('The dataset frames have different dimensions (rows, columns).');
|
inconsistencyWarnings.push(
|
||||||
|
'The dataset frames have different dimensions (rows, columns).'
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case ReconstructionIssues.VARYING_IMAGESCOMPONENTS:
|
case ReconstructionIssues.VARYING_IMAGESCOMPONENTS:
|
||||||
inconsistencyWarnings.push('The dataset frames have different components (Sample per pixel).');
|
inconsistencyWarnings.push(
|
||||||
|
'The dataset frames have different components (Sample per pixel).'
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case ReconstructionIssues.VARYING_IMAGESORIENTATION:
|
case ReconstructionIssues.VARYING_IMAGESORIENTATION:
|
||||||
inconsistencyWarnings.push('The dataset frames have different orientation.');
|
inconsistencyWarnings.push(
|
||||||
|
'The dataset frames have different orientation.'
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case ReconstructionIssues.IRREGULAR_SPACING:
|
case ReconstructionIssues.IRREGULAR_SPACING:
|
||||||
inconsistencyWarnings.push('The dataset frames have different pixel spacing.');
|
inconsistencyWarnings.push(
|
||||||
|
'The dataset frames have different pixel spacing.'
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case ReconstructionIssues.MULTIFFRAMES:
|
case ReconstructionIssues.MULTIFFRAMES:
|
||||||
inconsistencyWarnings.push('The dataset is a multiframes.');
|
inconsistencyWarnings.push('The dataset is a multiframes.');
|
||||||
@ -443,13 +495,22 @@ const _checkForSeriesInconsistencesWarnings = async function (displaySet, studie
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
inconsistencyWarnings.push('The datasets is not a reconstructable 3D volume. MPR mode is not available.');
|
inconsistencyWarnings.push(
|
||||||
|
'The datasets is not a reconstructable 3D volume. MPR mode is not available.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (displaySet.missingFrames &&
|
if (
|
||||||
|
displaySet.missingFrames &&
|
||||||
(!displaySet.reconstructionIssues ||
|
(!displaySet.reconstructionIssues ||
|
||||||
(displaySet.reconstructionIssues && !displaySet.reconstructionIssues.find(warn => warn === ReconstructionIssues.DATASET_4D)))) {
|
(displaySet.reconstructionIssues &&
|
||||||
inconsistencyWarnings.push('The datasets is missing frames: ' + displaySet.missingFrames + '.');
|
!displaySet.reconstructionIssues.find(
|
||||||
|
warn => warn === ReconstructionIssues.DATASET_4D
|
||||||
|
)))
|
||||||
|
) {
|
||||||
|
inconsistencyWarnings.push(
|
||||||
|
'The datasets is missing frames: ' + displaySet.missingFrames + '.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const segMetadata = displaySet.metadata;
|
const segMetadata = displaySet.metadata;
|
||||||
@ -458,24 +519,31 @@ const _checkForSeriesInconsistencesWarnings = async function (displaySet, studie
|
|||||||
return inconsistencyWarnings;
|
return inconsistencyWarnings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { referencedDisplaySet } = displaySet.getSourceDisplaySet(studies, false);
|
const { referencedDisplaySet } = displaySet.getSourceDisplaySet(
|
||||||
|
studies,
|
||||||
|
false
|
||||||
|
);
|
||||||
if (!referencedDisplaySet) {
|
if (!referencedDisplaySet) {
|
||||||
displaySet.inconsistencyWarnings = inconsistencyWarnings;
|
displaySet.inconsistencyWarnings = inconsistencyWarnings;
|
||||||
return inconsistencyWarnings;
|
return inconsistencyWarnings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageIds = referencedDisplaySet.images.map(image => image.getImageId());
|
const imageIds = referencedDisplaySet.images.map(image =>
|
||||||
|
image.getImageId()
|
||||||
|
);
|
||||||
if (!imageIds || imageIds.length === 0) {
|
if (!imageIds || imageIds.length === 0) {
|
||||||
displaySet.inconsistencyWarnings = inconsistencyWarnings;
|
displaySet.inconsistencyWarnings = inconsistencyWarnings;
|
||||||
return inconsistencyWarnings;
|
return inconsistencyWarnings;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (
|
for (
|
||||||
let i = 0, groupsLen = segMetadata.PerFrameFunctionalGroupsSequence.length;
|
let i = 0,
|
||||||
|
groupsLen = segMetadata.PerFrameFunctionalGroupsSequence.length;
|
||||||
i < groupsLen;
|
i < groupsLen;
|
||||||
++i
|
++i
|
||||||
) {
|
) {
|
||||||
const PerFrameFunctionalGroups = segMetadata.PerFrameFunctionalGroupsSequence[i];
|
const PerFrameFunctionalGroups =
|
||||||
|
segMetadata.PerFrameFunctionalGroupsSequence[i];
|
||||||
if (!PerFrameFunctionalGroups) {
|
if (!PerFrameFunctionalGroups) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -485,29 +553,27 @@ const _checkForSeriesInconsistencesWarnings = async function (displaySet, studie
|
|||||||
SourceImageSequence = segMetadata.SourceImageSequence[i];
|
SourceImageSequence = segMetadata.SourceImageSequence[i];
|
||||||
} else if (PerFrameFunctionalGroups.DerivationImageSequence) {
|
} else if (PerFrameFunctionalGroups.DerivationImageSequence) {
|
||||||
SourceImageSequence =
|
SourceImageSequence =
|
||||||
PerFrameFunctionalGroups.DerivationImageSequence
|
PerFrameFunctionalGroups.DerivationImageSequence.SourceImageSequence;
|
||||||
.SourceImageSequence;
|
|
||||||
}
|
}
|
||||||
if (!SourceImageSequence) {
|
if (!SourceImageSequence) {
|
||||||
if (inconsistencyWarnings.length === 0) {
|
if (inconsistencyWarnings.length === 0) {
|
||||||
const warningMessage = 'The segmentation ' +
|
const warningMessage =
|
||||||
|
'The segmentation ' +
|
||||||
'has frames out of plane respect to the source images.';
|
'has frames out of plane respect to the source images.';
|
||||||
inconsistencyWarnings.push(warningMessage);
|
inconsistencyWarnings.push(warningMessage);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { ReferencedSOPInstanceUID } = SourceImageSequence;
|
||||||
ReferencedSOPInstanceUID,
|
|
||||||
} = SourceImageSequence;
|
|
||||||
|
|
||||||
const imageId = imageIds.find(imageId => {
|
const imageId = imageIds.find(imageId => {
|
||||||
const sopCommonModule = cornerstone.metaData.get(
|
const sopCommonModule = cornerstone.metaData.get(
|
||||||
"sopCommonModule",
|
'sopCommonModule',
|
||||||
imageId
|
imageId
|
||||||
);
|
);
|
||||||
if (!sopCommonModule) {
|
if (!sopCommonModule) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sopCommonModule.sopInstanceUID === ReferencedSOPInstanceUID;
|
return sopCommonModule.sopInstanceUID === ReferencedSOPInstanceUID;
|
||||||
@ -517,33 +583,32 @@ const _checkForSeriesInconsistencesWarnings = async function (displaySet, studie
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceImageMetadata = cornerstone.metaData.get(
|
const sourceImageMetadata = cornerstone.metaData.get('instance', imageId);
|
||||||
"instance",
|
|
||||||
imageId
|
|
||||||
);
|
|
||||||
if (
|
if (
|
||||||
segMetadata.Rows !== sourceImageMetadata.Rows ||
|
segMetadata.Rows !== sourceImageMetadata.Rows ||
|
||||||
segMetadata.Columns !== sourceImageMetadata.Columns
|
segMetadata.Columns !== sourceImageMetadata.Columns
|
||||||
) {
|
) {
|
||||||
const warningMessage = 'The segmentation ' +
|
const warningMessage =
|
||||||
|
'The segmentation ' +
|
||||||
'has frames with different geometry ' +
|
'has frames with different geometry ' +
|
||||||
'dimensions (Rows and Columns) respect to the source images.';
|
'dimensions (Rows and Columns) respect to the source images.';
|
||||||
inconsistencyWarnings.push(warningMessage);
|
inconsistencyWarnings.push(warningMessage);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inconsistencyWarnings.length !== 0) {
|
if (inconsistencyWarnings.length !== 0) {
|
||||||
const warningMessage = 'The segmentation format is not supported yet. ' +
|
const warningMessage =
|
||||||
|
'The segmentation format is not supported yet. ' +
|
||||||
'The segmentation data (segments) could not be loaded.';
|
'The segmentation data (segments) could not be loaded.';
|
||||||
inconsistencyWarnings.push(warningMessage);
|
inconsistencyWarnings.push(warningMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cache the warnings
|
// cache the warnings
|
||||||
displaySet.inconsistencyWarnings = inconsistencyWarnings;
|
displaySet.inconsistencyWarnings = inconsistencyWarnings;
|
||||||
return inconsistencyWarnings;
|
return inconsistencyWarnings;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if display set is active, i.e. if the series is currently shown
|
* Checks if display set is active, i.e. if the series is currently shown
|
||||||
@ -562,30 +627,42 @@ const _checkForSeriesInconsistencesWarnings = async function (displaySet, studie
|
|||||||
* @param {string} activeDisplaySetInstanceUID
|
* @param {string} activeDisplaySetInstanceUID
|
||||||
* @returns {boolean} is active.
|
* @returns {boolean} is active.
|
||||||
*/
|
*/
|
||||||
const _isDisplaySetActive = function(displaySet, studies, activeDisplaySetInstanceUID) {
|
const _isDisplaySetActive = function(
|
||||||
|
displaySet,
|
||||||
|
studies,
|
||||||
|
activeDisplaySetInstanceUID
|
||||||
|
) {
|
||||||
let active = false;
|
let active = false;
|
||||||
|
|
||||||
const {
|
const { displaySetInstanceUID } = displaySet;
|
||||||
displaySetInstanceUID,
|
|
||||||
} = displaySet;
|
|
||||||
|
|
||||||
// TO DO: in the future, we could possibly support new modalities
|
// TO DO: in the future, we could possibly support new modalities
|
||||||
// we should have a list of all modalities here, instead of having hard coded checks
|
// we should have a list of all modalities here, instead of having hard coded checks
|
||||||
if (displaySet.Modality !== 'SEG' &&
|
if (
|
||||||
|
displaySet.Modality !== 'SEG' &&
|
||||||
displaySet.Modality !== 'RTSTRUCT' &&
|
displaySet.Modality !== 'RTSTRUCT' &&
|
||||||
displaySet.Modality !== 'RTDOSE') {
|
displaySet.Modality !== 'RTDOSE'
|
||||||
|
) {
|
||||||
active = activeDisplaySetInstanceUID === displaySetInstanceUID;
|
active = activeDisplaySetInstanceUID === displaySetInstanceUID;
|
||||||
} else if (displaySet.getSourceDisplaySet){
|
} else if (displaySet.getSourceDisplaySet) {
|
||||||
if (displaySet.Modality === 'SEG') {
|
if (displaySet.Modality === 'SEG') {
|
||||||
const { referencedDisplaySet } = displaySet.getSourceDisplaySet(studies, false);
|
const { referencedDisplaySet } = displaySet.getSourceDisplaySet(
|
||||||
active = referencedDisplaySet ?
|
studies,
|
||||||
activeDisplaySetInstanceUID === referencedDisplaySet.displaySetInstanceUID :
|
false
|
||||||
false;
|
);
|
||||||
|
active = referencedDisplaySet
|
||||||
|
? activeDisplaySetInstanceUID ===
|
||||||
|
referencedDisplaySet.displaySetInstanceUID
|
||||||
|
: false;
|
||||||
} else {
|
} else {
|
||||||
const referencedDisplaySet = displaySet.getSourceDisplaySet(studies, false);
|
const referencedDisplaySet = displaySet.getSourceDisplaySet(
|
||||||
active = referencedDisplaySet ?
|
studies,
|
||||||
activeDisplaySetInstanceUID === referencedDisplaySet.displaySetInstanceUID :
|
false
|
||||||
false;
|
);
|
||||||
|
active = referencedDisplaySet
|
||||||
|
? activeDisplaySetInstanceUID ===
|
||||||
|
referencedDisplaySet.displaySetInstanceUID
|
||||||
|
: false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -629,11 +706,17 @@ const _mapStudiesToThumbnails = function(studies, activeDisplaySetInstanceUID) {
|
|||||||
altImageText = displaySet.Modality ? displaySet.Modality : 'UN';
|
altImageText = displaySet.Modality ? displaySet.Modality : 'UN';
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasWarnings = _checkForSeriesInconsistencesWarnings(displaySet, studies);
|
const hasWarnings = _checkForSeriesInconsistencesWarnings(
|
||||||
const active = _isDisplaySetActive(displaySet, studies, activeDisplaySetInstanceUID)
|
displaySet,
|
||||||
|
studies
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
active,
|
active: _isDisplaySetActive(
|
||||||
|
displaySet,
|
||||||
|
studies,
|
||||||
|
activeDisplaySetInstanceUID
|
||||||
|
),
|
||||||
imageId,
|
imageId,
|
||||||
altImageText,
|
altImageText,
|
||||||
displaySetInstanceUID,
|
displaySetInstanceUID,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user