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: [],
|
||||
showStudyList: true,
|
||||
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: {
|
||||
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: [
|
||||
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
|
||||
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
|
||||
@ -97,14 +131,16 @@ window.config = {
|
||||
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.
|
||||
* Until we're able to decouple it, this key will serve as a workaround to
|
||||
* pass it configuration.
|
||||
*
|
||||
* @param {boolean} hideHandles Whether to show/hide annotation "handles"
|
||||
*/
|
||||
cornerstoneExtensionConfig: {
|
||||
/* Whether to show/hide annotation "handles" */
|
||||
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
|
||||
|
||||
*We currently support one property for annotation tools.*
|
||||
_We currently support one property for annotation tools._
|
||||
|
||||
### 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
|
||||
...
|
||||
@ -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]: https://github.com/cornerstonejs/cornerstone
|
||||
<!-- prettier-ignore-end -->
|
||||
```
|
||||
|
||||
@ -53,7 +53,11 @@ const mapStateToProps = (state, ownProps) => {
|
||||
// Currently justing using escape hatch + commands
|
||||
// activeTool: activeButton && activeButton.command,
|
||||
...dataFromStore,
|
||||
isStackPrefetchEnabled: isActive,
|
||||
isStackPrefetchEnabled: ownProps.hasOwnProperty('isStackPrefetchEnabled')
|
||||
? ownProps.isStackPrefetchEnabled
|
||||
: ownProps.stackPrefetch
|
||||
? ownProps.stackPrefetch.enabled
|
||||
: isActive,
|
||||
isPlaying,
|
||||
frameRate,
|
||||
//stack: viewportSpecificData.stack,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import OHIFCornerstoneViewportOverlay from './components/OHIFCornerstoneViewportOverlay'
|
||||
import OHIFCornerstoneViewportOverlay from './components/OHIFCornerstoneViewportOverlay';
|
||||
import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
|
||||
import OHIF from '@ohif/core';
|
||||
import PropTypes from 'prop-types';
|
||||
@ -15,6 +15,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
|
||||
static defaultProps = {
|
||||
customProps: {},
|
||||
isStackPrefetchEnabled: true,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
@ -23,6 +24,8 @@ class OHIFCornerstoneViewport extends Component {
|
||||
viewportIndex: PropTypes.number,
|
||||
children: PropTypes.node,
|
||||
customProps: PropTypes.object,
|
||||
stackPrefetch: PropTypes.object,
|
||||
isStackPrefetchEnabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
static id = 'OHIFCornerstoneViewport';
|
||||
@ -180,7 +183,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
|
||||
if (
|
||||
displaySet.displaySetInstanceUID !==
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||
) {
|
||||
@ -232,7 +235,12 @@ class OHIFCornerstoneViewport extends Component {
|
||||
};
|
||||
|
||||
const warningsOverlay = props => {
|
||||
return <OHIFCornerstoneViewportOverlay {...props} inconsistencyWarnings={inconsistencyWarnings} />
|
||||
return (
|
||||
<OHIFCornerstoneViewportOverlay
|
||||
{...props}
|
||||
inconsistencyWarnings={inconsistencyWarnings}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -244,10 +252,11 @@ class OHIFCornerstoneViewport extends Component {
|
||||
onNewImageDebounced={newImageHandler}
|
||||
onNewImageDebounceTime={300}
|
||||
viewportOverlayComponent={warningsOverlay}
|
||||
stackPrefetch={this.props.stackPrefetch}
|
||||
isStackPrefetchEnabled={this.props.isStackPrefetchEnabled}
|
||||
// ~~ Connected (From REDUX)
|
||||
// frameRate={frameRate}
|
||||
// isPlaying={false}
|
||||
// isStackPrefetchEnabled={true}
|
||||
// onElementEnabled={() => {}}
|
||||
// setViewportActive{() => {}}
|
||||
{...this.props.customProps}
|
||||
|
||||
@ -36,7 +36,7 @@ export default {
|
||||
preRegistration({ servicesManager, configuration = {} }) {
|
||||
init({ servicesManager, configuration });
|
||||
},
|
||||
getViewportModule({ commandsManager }) {
|
||||
getViewportModule({ commandsManager, appConfig }) {
|
||||
const ExtendedOHIFCornerstoneViewport = props => {
|
||||
/**
|
||||
* TODO: This appears to be used to set the redux parameters for
|
||||
@ -48,8 +48,17 @@ export default {
|
||||
jumpData.refreshViewports = false;
|
||||
commandsManager.runCommand('jumpToImage', jumpData);
|
||||
};
|
||||
|
||||
const { studyPrefetcher } = appConfig;
|
||||
const isStackPrefetchEnabled =
|
||||
studyPrefetcher && !studyPrefetcher.enabled;
|
||||
|
||||
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;
|
||||
|
||||
cornerstone.metaData.addProvider(
|
||||
@ -52,7 +59,7 @@ export default function init({ servicesManager, configuration }) {
|
||||
autoResizeViewports: false,
|
||||
};
|
||||
|
||||
initCornerstoneTools(defaultCsToolsConfig);
|
||||
initCornerstoneTools({ ...defaultCsToolsConfig, ...stackPrefetch });
|
||||
|
||||
const toolsGroupedByType = {
|
||||
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
|
||||
|
||||
@ -37,8 +37,8 @@ export default function(configuration = {}) {
|
||||
|
||||
// Configure stack prefetch
|
||||
cornerstoneTools.stackPrefetch.setConfiguration({
|
||||
maxImagesToPrefetch: Infinity,
|
||||
preserveExistingPool: false,
|
||||
maxSimultaneousRequests: 20,
|
||||
maxImagesToPrefetch: configuration.maxImagesToPrefetch,
|
||||
preserveExistingPool: configuration.preserveExistingPool,
|
||||
maxSimultaneousRequests: configuration.maxSimultaneousRequests,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||
import {
|
||||
clearStudyLoadingProgress,
|
||||
setStudyLoadingProgress,
|
||||
} from '../redux/actions';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
import StackManager from '../utils/StackManager';
|
||||
import { StudyPrefetcher } from './StudyPrefetcher';
|
||||
|
||||
class BaseLoadingListener {
|
||||
constructor(stack, options = {}) {
|
||||
this.id = BaseLoadingListener.getNewId();
|
||||
this.stack = stack;
|
||||
this.startListening();
|
||||
this.statsItemsLimit = options.statsItemsLimit || 2;
|
||||
this.stats = {
|
||||
items: [],
|
||||
@ -60,6 +58,9 @@ class BaseLoadingListener {
|
||||
}
|
||||
|
||||
_getProgressId() {
|
||||
/**
|
||||
* TODO: The id key should be configurable.
|
||||
*/
|
||||
const displaySetInstanceUID = this.stack.displaySetInstanceUID;
|
||||
return 'StackProgress:' + displaySetInstanceUID;
|
||||
}
|
||||
@ -96,11 +97,18 @@ class BaseLoadingListener {
|
||||
class DICOMFileLoadingListener extends BaseLoadingListener {
|
||||
constructor(stack, options) {
|
||||
super(stack, options);
|
||||
|
||||
this.imageLoadProgressEventHandler = this._imageLoadProgressEventHandle.bind(
|
||||
this
|
||||
);
|
||||
|
||||
this._dataSetUrl = this._getDataSetUrl(stack);
|
||||
this._lastLoaded = 0;
|
||||
|
||||
// Check how many instances has already been download (cached)
|
||||
this._checkCachedData();
|
||||
|
||||
this.startListening();
|
||||
}
|
||||
|
||||
_checkCachedData() {
|
||||
@ -127,10 +135,6 @@ class DICOMFileLoadingListener extends BaseLoadingListener {
|
||||
startListening() {
|
||||
const imageLoadProgressEventName = this._getImageLoadProgressEventName();
|
||||
|
||||
this.imageLoadProgressEventHandler = this._imageLoadProgressEventHandle.bind(
|
||||
this
|
||||
);
|
||||
|
||||
this.stopListening();
|
||||
|
||||
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 {
|
||||
constructor(stack, options = {}) {
|
||||
options.statsItemsLimit = 20;
|
||||
|
||||
super(stack, options);
|
||||
|
||||
this.imageLoadedEventHandler = this._imageLoadedEventHandler.bind(this);
|
||||
this.imageCachePromiseRemovedEventHandler = this._imageCachePromiseRemovedEventHandler.bind(
|
||||
this
|
||||
);
|
||||
|
||||
this.imageDataMap = this._convertImageIdsArrayToMap(stack.imageIds);
|
||||
this.framesStatus = this._createArray(stack.imageIds.length, false);
|
||||
this.loadedCount = 0;
|
||||
|
||||
// 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) {
|
||||
@ -238,18 +282,37 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
return array;
|
||||
}
|
||||
|
||||
_checkCachedData() {
|
||||
// const imageIds = this.stack.imageIds;
|
||||
// TODO: No way to check status of Promise.
|
||||
/*for(let i = 0; i < imageIds.length; i++) {
|
||||
const imageId = imageIds[i];
|
||||
/**
|
||||
* Check if image id is cached in cornerstone.
|
||||
*
|
||||
* @param {string} imageId
|
||||
* @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')) {
|
||||
this._updateFrameStatus(imageId, true);
|
||||
}
|
||||
}*/
|
||||
for (let i = 0; i < imageIds.length; i++) {
|
||||
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() {
|
||||
@ -272,11 +335,6 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
const imageLoadedEventName = this._getImageLoadedEventName();
|
||||
const imageCachePromiseRemovedEventName = this._getImageCachePromiseRemoveEventName();
|
||||
|
||||
this.imageLoadedEventHandler = this._imageLoadedEventHandler.bind(this);
|
||||
this.imageCachePromiseRemovedEventHandler = this._imageCachePromiseRemovedEventHandler.bind(
|
||||
this
|
||||
);
|
||||
|
||||
this.stopListening();
|
||||
|
||||
cornerstone.events.addEventListener(
|
||||
@ -303,7 +361,7 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
);
|
||||
}
|
||||
|
||||
_updateFrameStatus(imageId, loaded) {
|
||||
_updateFrameStatus(imageId, loaded, debounced) {
|
||||
const imageData = this.imageDataMap.get(imageId);
|
||||
|
||||
if (!imageData || imageData.loaded === loaded) {
|
||||
@ -318,7 +376,7 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
imageData.loaded = loaded;
|
||||
this.framesStatus[imageData.index] = loaded;
|
||||
this.loadedCount += loaded ? 1 : -1;
|
||||
this._updateProgress();
|
||||
this._updateProgress(debounced);
|
||||
}
|
||||
|
||||
_setProgressData(progressId, progressData) {
|
||||
@ -339,7 +397,7 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
);
|
||||
}
|
||||
|
||||
_updateProgress() {
|
||||
_updateProgress(debounced) {
|
||||
const totalFramesCount = this.stack.imageIds.length;
|
||||
const loadedFramesCount = this.loadedCount;
|
||||
const loadingFramesCount = totalFramesCount - loadedFramesCount;
|
||||
@ -357,6 +415,11 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
framesStatus: this.framesStatus,
|
||||
};
|
||||
|
||||
if (debounced) {
|
||||
this._debouncedSetProgressData(progressId, progressData);
|
||||
return;
|
||||
}
|
||||
|
||||
this._setProgressData(progressId, progressData);
|
||||
}
|
||||
|
||||
@ -371,11 +434,13 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
}
|
||||
|
||||
progressBar += ']';
|
||||
log.info(`${displaySetInstanceUID}: ${progressBar}`);
|
||||
console.info(`${displaySetInstanceUID}: ${progressBar}`);
|
||||
}
|
||||
}
|
||||
|
||||
class StudyLoadingListener {
|
||||
static events = StudyLoadingListenerEvents;
|
||||
|
||||
constructor(options) {
|
||||
this.listeners = {};
|
||||
this.options = options;
|
||||
@ -454,14 +519,36 @@ class StudyLoadingListener {
|
||||
|
||||
_getSchema(stack) {
|
||||
const imageId = stack.imageIds[0];
|
||||
if (!imageId) {
|
||||
return;
|
||||
}
|
||||
const colonIndex = imageId.indexOf(':');
|
||||
return imageId.substring(0, colonIndex);
|
||||
}
|
||||
|
||||
// Singleton
|
||||
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) {
|
||||
StudyLoadingListener._instance = new StudyLoadingListener(options);
|
||||
StudyLoadingListener._instance = new StudyLoadingListener(
|
||||
options || DEFAULT_OPTIONS
|
||||
);
|
||||
}
|
||||
|
||||
return StudyLoadingListener._instance;
|
||||
|
||||
@ -1,14 +1,28 @@
|
||||
import log from '../log.js';
|
||||
import OHIFError from './OHIFError';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
|
||||
import getImageId from '../utils/getImageId.js';
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
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.prefetchDisplaySetsTimeout = 300;
|
||||
this.lastActiveViewportElement = null;
|
||||
|
||||
if (options) {
|
||||
this.options = { ...this.options, ...options };
|
||||
this.options.requestType = 'prefetch';
|
||||
}
|
||||
|
||||
cornerstone.events.addEventListener(
|
||||
'cornerstoneimagecachefull.StudyPrefetcher',
|
||||
@ -16,6 +30,9 @@ export class StudyPrefetcher {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove previously set event listeners and stop prefetching.
|
||||
*/
|
||||
destroy() {
|
||||
this.stopPrefetching();
|
||||
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) {
|
||||
StudyPrefetcher.instance = new StudyPrefetcher([]);
|
||||
StudyPrefetcher.instance = new StudyPrefetcher(studies, options);
|
||||
}
|
||||
|
||||
if (options) {
|
||||
this.options = { ...this.options, ...options };
|
||||
this.options.requestType = 'prefetch';
|
||||
}
|
||||
|
||||
return StudyPrefetcher.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* OHIF study metadata instances.
|
||||
*
|
||||
* @param {array} studies
|
||||
*/
|
||||
setStudies(studies) {
|
||||
this.stopPrefetching();
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.element = element;
|
||||
this.enabledElement = cornerstone.getEnabledElement(element);
|
||||
|
||||
this.stopPrefetching();
|
||||
this.prefetchDisplaySets();
|
||||
this.prefetchDisplaySets(displaySetInstanceUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop prefetching images.
|
||||
*/
|
||||
stopPrefetching() {
|
||||
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);
|
||||
this.prefetchDisplaySetsHandler = setTimeout(() => {
|
||||
this.prefetchDisplaySets();
|
||||
this.prefetchDisplaySets(element);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
prefetchDisplaySets() {
|
||||
// TODO: Allow passing in config
|
||||
let config = {
|
||||
order: 'closest',
|
||||
displaySetCount: 1,
|
||||
};
|
||||
|
||||
const displaySetsToPrefetch = this.getDisplaySetsToPrefetch(config);
|
||||
/**
|
||||
* Extract all image ids from all display sets to be fetched and
|
||||
* call method to add images to request pool manager.
|
||||
*
|
||||
* @param {string} displaySetInstanceUID the display set instance uid
|
||||
*/
|
||||
prefetchDisplaySets(displaySetInstanceUID) {
|
||||
const displaySetsToPrefetch = this.getDisplaySetsToPrefetch(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
const imageIds = this.getImageIdsFromDisplaySets(displaySetsToPrefetch);
|
||||
|
||||
this.prefetchImageIds(imageIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add image ids to request pool manager.
|
||||
*
|
||||
* @param {array} imageIds
|
||||
*/
|
||||
prefetchImageIds(imageIds) {
|
||||
const nonCachedImageIds = this.filterCachedImageIds(imageIds);
|
||||
const requestPoolManager = cornerstoneTools.requestPoolManager;
|
||||
const requestType = 'prefetch';
|
||||
const preventCache = false;
|
||||
const noop = () => {};
|
||||
|
||||
nonCachedImageIds.forEach(imageId => {
|
||||
requestPoolManager.addRequest(
|
||||
{},
|
||||
imageId,
|
||||
requestType,
|
||||
preventCache,
|
||||
noop,
|
||||
this.options.requestType,
|
||||
this.options.preventCache,
|
||||
() => {},
|
||||
noop
|
||||
);
|
||||
});
|
||||
@ -93,26 +159,45 @@ export class StudyPrefetcher {
|
||||
requestPoolManager.startGrabbing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get study by cornerstone image instance.
|
||||
*
|
||||
* @param {object} image
|
||||
* @returns
|
||||
*/
|
||||
getStudy(image) {
|
||||
const StudyInstanceUID = cornerstone.metaData.get(
|
||||
'StudyInstanceUID',
|
||||
image.imageId
|
||||
);
|
||||
return OHIF.viewer.Studies.find(
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
const studies = this.studies;
|
||||
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) {
|
||||
const SeriesInstanceUID = cornerstone.metaData.get(
|
||||
'SeriesInstanceUID',
|
||||
image.imageId
|
||||
);
|
||||
const studyMetadata = OHIF.viewerbase.getStudyMetadata(study);
|
||||
|
||||
return studyMetadata.getSeriesByUID(SeriesInstanceUID);
|
||||
return study.getSeriesByUID(SeriesInstanceUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sop instance by cornerstone image instance.
|
||||
*
|
||||
* @param {array} series
|
||||
* @param {object} image
|
||||
* @returns
|
||||
*/
|
||||
getInstance(series, image) {
|
||||
const instanceMetadata = cornerstone.metaData.get(
|
||||
'instance',
|
||||
@ -121,7 +206,33 @@ export class StudyPrefetcher {
|
||||
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 displaySet.images.some(displaySetImage => {
|
||||
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();
|
||||
|
||||
if (!image || !config || !config.displaySetCount) {
|
||||
if (!image) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/*const study = this.getStudy(image);
|
||||
const study = this.getStudy(image);
|
||||
const series = this.getSeries(study, image);
|
||||
const instance = this.getInstance(series, image);*/
|
||||
const instance = this.getInstance(series, image);
|
||||
const displaySets = study.displaySets;
|
||||
const activeDisplaySet = null; //this.getActiveDisplaySet(displaySets, instance);
|
||||
const activeDisplaySet = displaySetInstanceUID
|
||||
? this.getDisplaySetByUID(displaySetInstanceUID)
|
||||
: this.getDisplaySetBySOPInstanceUID(displaySets, instance);
|
||||
|
||||
const prefetchMethodMap = {
|
||||
topdown: 'getFirstDisplaySets',
|
||||
downward: 'getNextDisplaySets',
|
||||
upward: 'getPreviousDisplaySets',
|
||||
closest: 'getClosestDisplaySets',
|
||||
all: 'getAllDisplaySets',
|
||||
};
|
||||
|
||||
const prefetchOrder = config.order;
|
||||
const prefetchOrder = this.options.order;
|
||||
const methodName = prefetchMethodMap[prefetchOrder];
|
||||
const getDisplaySets = this[methodName];
|
||||
|
||||
@ -163,18 +297,59 @@ export class StudyPrefetcher {
|
||||
this,
|
||||
displaySets,
|
||||
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 selectedDisplaySets = [];
|
||||
|
||||
for (let i = 0; i < length && displaySetCount; i++) {
|
||||
const displaySet = displaySets[i];
|
||||
|
||||
if (displaySet !== activeDisplaySet) {
|
||||
if (includeActiveDisplaySet || displaySet !== activeDisplaySet) {
|
||||
selectedDisplaySets.push(displaySet);
|
||||
displaySetCount--;
|
||||
}
|
||||
@ -183,21 +358,78 @@ export class StudyPrefetcher {
|
||||
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 begin = activeDisplaySetIndex + 1;
|
||||
const end = Math.min(begin + displaySetCount, displaySets.length);
|
||||
const end = includeActiveDisplaySet
|
||||
? 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);
|
||||
}
|
||||
|
||||
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 length = displaySets.length;
|
||||
const selectedDisplaySets = [];
|
||||
let left = activeDisplaySetIndex - 1;
|
||||
let right = activeDisplaySetIndex + 1;
|
||||
|
||||
if (includeActiveDisplaySet) {
|
||||
selectedDisplaySets.push(displaySets[activeDisplaySetIndex]);
|
||||
displaySetCount--;
|
||||
}
|
||||
|
||||
while ((left >= 0 || right < length) && displaySetCount) {
|
||||
if (left >= 0) {
|
||||
selectedDisplaySets.push(displaySets[left]);
|
||||
@ -215,6 +447,12 @@ export class StudyPrefetcher {
|
||||
return selectedDisplaySets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all image ids from display sets.
|
||||
*
|
||||
* @param {array} displaySets
|
||||
* @returns {array} image ids
|
||||
*/
|
||||
getImageIdsFromDisplaySets(displaySets) {
|
||||
let imageIds = [];
|
||||
|
||||
@ -225,9 +463,19 @@ export class StudyPrefetcher {
|
||||
return imageIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all image ids from a given display set.
|
||||
*
|
||||
* @param {array} displaySet
|
||||
* @returns
|
||||
*/
|
||||
getImageIdsFromDisplaySet(displaySet) {
|
||||
const imageIds = [];
|
||||
|
||||
if (!displaySet.images || displaySet.images.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// TODO: This duplicates work done by the stack manager
|
||||
displaySet.images.forEach(image => {
|
||||
const numFrames = image.numFrames;
|
||||
@ -245,15 +493,30 @@ export class StudyPrefetcher {
|
||||
return imageIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter cached image ids from a set of image ids.
|
||||
*
|
||||
* @param {array} imageIds
|
||||
* @returns {array} images not cached
|
||||
*/
|
||||
filterCachedImageIds(imageIds) {
|
||||
return imageIds.filter(imageId => !this.isImageCached(imageId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if image id is cached in cornerstone.
|
||||
*
|
||||
* @param {string} imageId
|
||||
* @returns
|
||||
*/
|
||||
isImageCached(imageId) {
|
||||
const image = cornerstone.imageCache.imageCache[imageId];
|
||||
return image && image.sizeInBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Warns that cache is full and stops prefetching.
|
||||
*/
|
||||
cacheFullHandler = () => {
|
||||
log.warn('Cache full');
|
||||
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 = this._displaySets;
|
||||
}
|
||||
|
||||
@ -78,17 +78,6 @@ export const setActiveViewportSpecificData = viewportSpecificData => ({
|
||||
/**
|
||||
* 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 => ({
|
||||
type: SET_USER_PREFERENCES,
|
||||
state,
|
||||
@ -134,8 +123,6 @@ const actions = {
|
||||
/**
|
||||
* NOT-VIEWPORT
|
||||
*/
|
||||
setStudyLoadingProgress,
|
||||
clearStudyLoadingProgress,
|
||||
setUserPreferences,
|
||||
setExtensionData,
|
||||
setTimepoints,
|
||||
|
||||
@ -11,8 +11,6 @@ describe('actions', () => {
|
||||
'setLayout',
|
||||
'clearViewportSpecificData',
|
||||
'setActiveViewportSpecificData',
|
||||
'setStudyLoadingProgress',
|
||||
'clearStudyLoadingProgress',
|
||||
'setUserPreferences',
|
||||
'setExtensionData',
|
||||
'setTimepoints',
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import extensions from './extensions';
|
||||
import loading from './loading';
|
||||
import preferences from './preferences';
|
||||
import servers from './servers';
|
||||
import studies from './studies';
|
||||
@ -8,7 +7,6 @@ import viewports from './viewports';
|
||||
|
||||
const reducers = {
|
||||
extensions,
|
||||
loading,
|
||||
preferences,
|
||||
servers,
|
||||
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 { utils } from '@ohif/core';
|
||||
import React, { useState, useEffect, createRef } from 'react';
|
||||
import React, { useState, useEffect, createRef, useCallback } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
@ -23,6 +23,7 @@ function ImageThumbnail(props) {
|
||||
imageId,
|
||||
stackPercentComplete,
|
||||
error: propsError,
|
||||
showProgressBar,
|
||||
} = props;
|
||||
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
@ -39,7 +40,8 @@ function ImageThumbnail(props) {
|
||||
loadingOrError = <ViewportLoadingIndicator />;
|
||||
}
|
||||
|
||||
const showStackLoadingProgressBar = stackPercentComplete !== undefined;
|
||||
const showStackLoadingProgressBar =
|
||||
showProgressBar && stackPercentComplete !== undefined;
|
||||
|
||||
const shouldRenderToCanvas = () => {
|
||||
return imageId && !imageSrc;
|
||||
@ -71,11 +73,11 @@ function ImageThumbnail(props) {
|
||||
}
|
||||
};
|
||||
|
||||
const purgeCancelablePromise = () => {
|
||||
const purgeCancelablePromise = useCallback(() => {
|
||||
if (cancelablePromise) {
|
||||
cancelablePromise.cancel();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@ -96,7 +98,13 @@ function ImageThumbnail(props) {
|
||||
setImagePromise();
|
||||
fetchImagePromise();
|
||||
}
|
||||
}, [fetchImagePromise, image.imageId, imageId, purgeCancelablePromise, setImagePromise]);
|
||||
}, [
|
||||
fetchImagePromise,
|
||||
image.imageId,
|
||||
imageId,
|
||||
purgeCancelablePromise,
|
||||
setImagePromise,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={classNames('ImageThumbnail', { active: active })}>
|
||||
@ -104,14 +112,14 @@ function ImageThumbnail(props) {
|
||||
{shouldRenderToCanvas() ? (
|
||||
<canvas ref={canvasRef} width={width} height={height} />
|
||||
) : (
|
||||
<img
|
||||
className="static-image"
|
||||
src={imageSrc}
|
||||
//width={this.props.width}
|
||||
height={height}
|
||||
alt={''}
|
||||
/>
|
||||
)}
|
||||
<img
|
||||
className="static-image"
|
||||
src={imageSrc}
|
||||
//width={this.props.width}
|
||||
height={height}
|
||||
alt={''}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{loadingOrError}
|
||||
{showStackLoadingProgressBar && (
|
||||
@ -135,6 +143,7 @@ ImageThumbnail.propTypes = {
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
stackPercentComplete: PropTypes.number.isRequired,
|
||||
showProgressBar: PropTypes.bool,
|
||||
};
|
||||
|
||||
ImageThumbnail.defaultProps = {
|
||||
@ -143,6 +152,7 @@ ImageThumbnail.defaultProps = {
|
||||
stackPercentComplete: 0,
|
||||
width: 217,
|
||||
height: 123,
|
||||
showProgressBar: true,
|
||||
};
|
||||
|
||||
export default ImageThumbnail;
|
||||
|
||||
@ -9,6 +9,7 @@ function StudyBrowser(props) {
|
||||
onThumbnailClick,
|
||||
onThumbnailDoubleClick,
|
||||
supportsDrag,
|
||||
showThumbnailProgressBar,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@ -28,7 +29,6 @@ function StudyBrowser(props) {
|
||||
numImageFrames,
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
stackPercentComplete,
|
||||
hasWarnings,
|
||||
} = thumb;
|
||||
|
||||
@ -54,13 +54,13 @@ function StudyBrowser(props) {
|
||||
SeriesDescription={SeriesDescription}
|
||||
SeriesNumber={SeriesNumber}
|
||||
hasWarnings={hasWarnings}
|
||||
stackPercentComplete={stackPercentComplete}
|
||||
// Events
|
||||
onClick={onThumbnailClick.bind(
|
||||
undefined,
|
||||
displaySetInstanceUID
|
||||
)}
|
||||
onDoubleClick={onThumbnailDoubleClick}
|
||||
showProgressBar={showThumbnailProgressBar}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -95,6 +95,7 @@ StudyBrowser.propTypes = {
|
||||
supportsDrag: PropTypes.bool,
|
||||
onThumbnailClick: PropTypes.func,
|
||||
onThumbnailDoubleClick: PropTypes.func,
|
||||
showThumbnailProgressBar: PropTypes.bool,
|
||||
};
|
||||
|
||||
StudyBrowser.defaultProps = {
|
||||
@ -102,6 +103,7 @@ StudyBrowser.defaultProps = {
|
||||
supportsDrag: true,
|
||||
onThumbnailClick: noop,
|
||||
onThumbnailDoubleClick: noop,
|
||||
showThumbnailProgressBar: true,
|
||||
};
|
||||
|
||||
export { StudyBrowser };
|
||||
|
||||
@ -1,34 +1,36 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDrag } from 'react-dnd';
|
||||
import { classes } from '@ohif/core';
|
||||
import ImageThumbnail from './ImageThumbnail';
|
||||
import classNames from 'classnames';
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import { Tooltip } from './../tooltip';
|
||||
import { OverlayTrigger } from './../overlayTrigger';
|
||||
|
||||
import './Thumbnail.styl';
|
||||
|
||||
const StudyLoadingListener = classes.StudyLoadingListener;
|
||||
|
||||
function ThumbnailFooter({
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
InstanceNumber,
|
||||
numImageFrames,
|
||||
hasWarnings
|
||||
hasWarnings,
|
||||
}) {
|
||||
const [inconsistencyWarnings, inconsistencyWarningsSet] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
let unmounted = false
|
||||
let unmounted = false;
|
||||
hasWarnings.then(response => {
|
||||
if (!unmounted) {
|
||||
inconsistencyWarningsSet(response)
|
||||
inconsistencyWarningsSet(response);
|
||||
}
|
||||
})
|
||||
});
|
||||
return () => {
|
||||
unmounted = true
|
||||
}
|
||||
}, [])
|
||||
unmounted = true;
|
||||
};
|
||||
}, [hasWarnings]);
|
||||
|
||||
const infoOnly = !SeriesDescription;
|
||||
|
||||
@ -41,7 +43,7 @@ function ThumbnailFooter({
|
||||
);
|
||||
};
|
||||
|
||||
const getWarningContent = (inconsistencyWarnings) => {
|
||||
const getWarningContent = inconsistencyWarnings => {
|
||||
if (Array.isArray(inconsistencyWarnings)) {
|
||||
const listedWarnings = inconsistencyWarnings.map((warn, index) => {
|
||||
return <li key={index}>{warn}</li>;
|
||||
@ -54,8 +56,8 @@ function ThumbnailFooter({
|
||||
};
|
||||
|
||||
const getWarningInfo = (SeriesNumber, inconsistencyWarnings) => {
|
||||
return(
|
||||
<React.Fragment>
|
||||
return (
|
||||
<React.Fragment>
|
||||
{inconsistencyWarnings && inconsistencyWarnings.length != 0 ? (
|
||||
<OverlayTrigger
|
||||
key={SeriesNumber}
|
||||
@ -67,7 +69,9 @@ function ThumbnailFooter({
|
||||
id="tooltip-left"
|
||||
>
|
||||
<div className="warningTitle">Series Inconsistencies</div>
|
||||
<div className="warningContent">{getWarningContent(inconsistencyWarnings)}</div>
|
||||
<div className="warningContent">
|
||||
{getWarningContent(inconsistencyWarnings)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
@ -79,9 +83,9 @@ function ThumbnailFooter({
|
||||
</OverlayTrigger>
|
||||
) : (
|
||||
<React.Fragment></React.Fragment>
|
||||
)}
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
);
|
||||
};
|
||||
const getSeriesInformation = (
|
||||
SeriesNumber,
|
||||
@ -92,21 +96,27 @@ function ThumbnailFooter({
|
||||
if (!SeriesNumber && !InstanceNumber && !numImageFrames) {
|
||||
return;
|
||||
}
|
||||
const seriesInformation =
|
||||
const seriesInformation = (
|
||||
<div className="series-information">
|
||||
{getInfo(SeriesNumber, 'S:')}
|
||||
{getInfo(InstanceNumber, 'I:')}
|
||||
{getInfo(numImageFrames, '', 'image-frames')}
|
||||
{getWarningInfo(SeriesNumber, inconsistencyWarnings)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (seriesInformation);
|
||||
return seriesInformation;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames('series-details', { 'info-only': infoOnly })}>
|
||||
<div className="series-description">{SeriesDescription}</div>
|
||||
{getSeriesInformation(SeriesNumber, InstanceNumber, numImageFrames, inconsistencyWarnings)}
|
||||
{getSeriesInformation(
|
||||
SeriesNumber,
|
||||
InstanceNumber,
|
||||
numImageFrames,
|
||||
inconsistencyWarnings
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -124,14 +134,37 @@ function Thumbnail(props) {
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
hasWarnings,
|
||||
stackPercentComplete,
|
||||
StudyInstanceUID,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
onMouseDown,
|
||||
supportsDrag,
|
||||
showProgressBar,
|
||||
} = 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({
|
||||
// `droppedItem` in `dropTarget`
|
||||
// The only data it will have access to
|
||||
@ -164,6 +197,7 @@ function Thumbnail(props) {
|
||||
imageId={imageId}
|
||||
error={error}
|
||||
stackPercentComplete={stackPercentComplete}
|
||||
showProgressBar={showProgressBar}
|
||||
/>
|
||||
)}
|
||||
{/* SHOW TEXT ALTERNATIVE */}
|
||||
@ -203,6 +237,7 @@ Thumbnail.propTypes = {
|
||||
onDoubleClick: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
onMouseDown: PropTypes.func,
|
||||
showProgressBar: PropTypes.bool,
|
||||
};
|
||||
|
||||
Thumbnail.defaultProps = {
|
||||
|
||||
@ -5,6 +5,15 @@ window.config = {
|
||||
showStudyList: true,
|
||||
filterQueryParam: false,
|
||||
disableServersCache: false,
|
||||
studyPrefetcher: {
|
||||
enabled: true,
|
||||
order: 'closest',
|
||||
displaySetCount: 3,
|
||||
preventCache: false,
|
||||
prefetchDisplaySetsTimeout: 300,
|
||||
displayProgress: true,
|
||||
includeActiveDisplaySet: true,
|
||||
},
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
window.config = {
|
||||
routerBasename: '/pwa',
|
||||
showStudyList: true,
|
||||
studyPrefetcher: {
|
||||
enabled: true,
|
||||
order: 'closest',
|
||||
displaySetCount: 3,
|
||||
preventCache: false,
|
||||
prefetchDisplaySetsTimeout: 300,
|
||||
displayProgress: true,
|
||||
includeActiveDisplaySet: true,
|
||||
},
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
|
||||
@ -1,42 +1,23 @@
|
||||
import { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import OHIF from "@ohif/core";
|
||||
import { useEffect } from 'react';
|
||||
import OHIF from '@ohif/core';
|
||||
|
||||
class StudyLoadingMonitor extends Component {
|
||||
static propTypes = {
|
||||
studies: PropTypes.array.isRequired,
|
||||
setStudyLoadingProgress: PropTypes.func.isRequired,
|
||||
clearStudyLoadingProgress: PropTypes.func.isRequired
|
||||
};
|
||||
const { StudyLoadingListener } = OHIF.classes;
|
||||
|
||||
componentDidMount() {
|
||||
// TODO: This is pretty ugly. The thing is that the StudyLoadingListener
|
||||
// needs to update the Redux store, but shouldn't know that it exists.
|
||||
// I am therefore passing in some functions to update the store instead,
|
||||
// but this should definitely be cleaned up somehow.
|
||||
const options = {
|
||||
_setProgressData: (progressId, progressData) => {
|
||||
this.props.setStudyLoadingProgress(progressId, progressData);
|
||||
},
|
||||
_clearProgressById: progressId => {
|
||||
this.props.clearStudyLoadingProgress(progressId);
|
||||
}
|
||||
const StudyLoadingMonitor = ({ studies }) => {
|
||||
useEffect(() => {
|
||||
const studyLoadingListener = StudyLoadingListener.getInstance();
|
||||
|
||||
if (studies && studies.length > 0) {
|
||||
studyLoadingListener.clear();
|
||||
studyLoadingListener.addStudies(studies);
|
||||
}
|
||||
|
||||
return () => {
|
||||
studyLoadingListener.clear();
|
||||
};
|
||||
}, [studies]);
|
||||
|
||||
const { StudyLoadingListener } = OHIF.classes;
|
||||
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();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
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 PropTypes from "prop-types";
|
||||
import OHIF from "@ohif/core";
|
||||
import React, { useEffect } from 'react';
|
||||
import { classes, utils } from '@ohif/core';
|
||||
import PropTypes from 'prop-types';
|
||||
import cs from 'cornerstone-core';
|
||||
|
||||
const { StudyPrefetcher } = OHIF.classes;
|
||||
import './StudyPrefetcher.css';
|
||||
|
||||
class StudyPrefetcherComponent extends Component {
|
||||
static propTypes = {
|
||||
studies: PropTypes.array
|
||||
};
|
||||
const StudyPrefetcher = ({ studies, options }) => {
|
||||
useEffect(() => {
|
||||
const studyPrefetcher = classes.StudyPrefetcher.getInstance(
|
||||
studies,
|
||||
options
|
||||
);
|
||||
const studiesMetadata = studies.map(s =>
|
||||
utils.studyMetadataManager.get(s.StudyInstanceUID)
|
||||
);
|
||||
studyPrefetcher.setStudies(studiesMetadata);
|
||||
|
||||
componentDidMount() {
|
||||
const { studies } = this.props;
|
||||
const onNewImage = ({ detail }) => {
|
||||
/**
|
||||
* 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();
|
||||
this.studyPrefetcher.setStudies(studies);
|
||||
}
|
||||
const study = studyPrefetcher.getStudy(detail.image);
|
||||
const series = studyPrefetcher.getSeries(study, detail.image);
|
||||
const instance = studyPrefetcher.getInstance(series, detail.image);
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
if (study.displaySets && study.displaySets.length > 0) {
|
||||
const {
|
||||
displaySetInstanceUID,
|
||||
} = studyPrefetcher.getDisplaySetBySOPInstanceUID(
|
||||
study.displaySets,
|
||||
instance
|
||||
);
|
||||
studyPrefetcher.prefetch(detail.element, displaySetInstanceUID);
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
// Stop prefetching when we close the viewer
|
||||
this.studyPrefetcher.destroy();
|
||||
}
|
||||
}
|
||||
const onElementEnabled = ({ detail }) => {
|
||||
detail.element.addEventListener(cs.EVENTS.NEW_IMAGE, onNewImage);
|
||||
};
|
||||
|
||||
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 { connect } from 'react-redux';
|
||||
import { StudyBrowser } from '@ohif/ui';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import findDisplaySetByUID from './findDisplaySetByUID';
|
||||
import { servicesManager } from './../App.js';
|
||||
|
||||
const { studyMetadataManager } = OHIF.utils;
|
||||
import { StudyBrowser } from '../../../ui/src/components/studyBrowser/StudyBrowser';
|
||||
|
||||
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) => {
|
||||
return {
|
||||
onThumbnailClick: displaySetInstanceUID => {
|
||||
@ -50,7 +17,10 @@ const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
if (displaySet.isDerived) {
|
||||
const { Modality } = displaySet;
|
||||
if (Modality === 'SEG' && servicesManager) {
|
||||
const {LoggerService, UINotificationService} = servicesManager.services;
|
||||
const {
|
||||
LoggerService,
|
||||
UINotificationService,
|
||||
} = servicesManager.services;
|
||||
const onDisplaySetLoadFailureHandler = error => {
|
||||
LoggerService.error({ error, message: error.message });
|
||||
UINotificationService.show({
|
||||
@ -61,20 +31,25 @@ const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
});
|
||||
};
|
||||
|
||||
const {referencedDisplaySet, activatedLabelmapPromise} = displaySet.getSourceDisplaySet(
|
||||
const {
|
||||
referencedDisplaySet,
|
||||
activatedLabelmapPromise,
|
||||
} = displaySet.getSourceDisplaySet(
|
||||
ownProps.studyMetadata,
|
||||
true,
|
||||
onDisplaySetLoadFailureHandler
|
||||
);
|
||||
displaySet = referencedDisplaySet;
|
||||
|
||||
activatedLabelmapPromise.then((activatedLabelmapIndex) => {
|
||||
const selectionFired = new CustomEvent("extensiondicomsegmentationsegselected", {
|
||||
"detail": {"activatedLabelmapIndex":activatedLabelmapIndex}
|
||||
});
|
||||
activatedLabelmapPromise.then(activatedLabelmapIndex => {
|
||||
const selectionFired = new CustomEvent(
|
||||
'extensiondicomsegmentationsegselected',
|
||||
{
|
||||
detail: { activatedLabelmapIndex: activatedLabelmapIndex },
|
||||
}
|
||||
);
|
||||
document.dispatchEvent(selectionFired);
|
||||
});
|
||||
|
||||
} else {
|
||||
displaySet = displaySet.getSourceDisplaySet(ownProps.studyMetadata);
|
||||
}
|
||||
@ -96,7 +71,7 @@ const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
};
|
||||
|
||||
const ConnectedStudyBrowser = connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(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 PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useLogger } from '@ohif/ui';
|
||||
|
||||
import OHIF, { MODULE_TYPES, DICOMSR } from '@ohif/core';
|
||||
import { withDialog } from '@ohif/ui';
|
||||
import moment from 'moment';
|
||||
|
||||
import ConnectedHeader from './ConnectedHeader.js';
|
||||
import ToolbarRow from './ToolbarRow.js';
|
||||
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
||||
@ -14,7 +13,6 @@ import SidePanel from './../components/SidePanel.js';
|
||||
import ErrorBoundaryDialog from './../components/ErrorBoundaryDialog';
|
||||
import { extensionManager } from './../App.js';
|
||||
import { ReconstructionIssues } from './../../../core/src/enums.js';
|
||||
import dcmjs from 'dcmjs';
|
||||
|
||||
// Contexts
|
||||
import WhiteLabelingContext from '../context/WhiteLabelingContext.js';
|
||||
@ -22,8 +20,8 @@ import UserManagerContext from '../context/UserManagerContext';
|
||||
import AppContext from '../context/AppContext';
|
||||
|
||||
import './Viewer.css';
|
||||
import { finished } from 'stream';
|
||||
import { cornerstoneWADOImageLoader } from 'cornerstone-wado-image-loader';
|
||||
import StudyPrefetcher from '../components/StudyPrefetcher.js';
|
||||
import StudyLoadingMonitor from '../components/StudyLoadingMonitor';
|
||||
|
||||
class Viewer extends Component {
|
||||
static propTypes = {
|
||||
@ -197,11 +195,17 @@ class Viewer extends Component {
|
||||
]);
|
||||
}
|
||||
|
||||
const activeViewport = this.props.viewports[this.props.activeViewportIndex];
|
||||
const activeDisplaySetInstanceUID =
|
||||
activeViewport ? activeViewport.displaySetInstanceUID : undefined;
|
||||
const activeViewport = this.props.viewports[
|
||||
this.props.activeViewportIndex
|
||||
];
|
||||
const activeDisplaySetInstanceUID = activeViewport
|
||||
? activeViewport.displaySetInstanceUID
|
||||
: undefined;
|
||||
this.setState({
|
||||
thumbnails: _mapStudiesToThumbnails(studies, activeDisplaySetInstanceUID),
|
||||
thumbnails: _mapStudiesToThumbnails(
|
||||
studies,
|
||||
activeDisplaySetInstanceUID
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -211,24 +215,31 @@ class Viewer extends Component {
|
||||
studies,
|
||||
isStudyLoaded,
|
||||
activeViewportIndex,
|
||||
viewports
|
||||
viewports,
|
||||
} = this.props;
|
||||
|
||||
const activeViewport = viewports[activeViewportIndex];
|
||||
const activeDisplaySetInstanceUID =
|
||||
activeViewport ? activeViewport.displaySetInstanceUID : undefined;
|
||||
const activeDisplaySetInstanceUID = activeViewport
|
||||
? activeViewport.displaySetInstanceUID
|
||||
: undefined;
|
||||
|
||||
const prevActiveViewport = prevProps.viewports[prevProps.activeViewportIndex];
|
||||
const prevActiveDisplaySetInstanceUID =
|
||||
prevActiveViewport ? prevActiveViewport.displaySetInstanceUID : undefined;
|
||||
const prevActiveViewport =
|
||||
prevProps.viewports[prevProps.activeViewportIndex];
|
||||
const prevActiveDisplaySetInstanceUID = prevActiveViewport
|
||||
? prevActiveViewport.displaySetInstanceUID
|
||||
: undefined;
|
||||
|
||||
if (studies !== prevProps.studies ||
|
||||
if (
|
||||
studies !== prevProps.studies ||
|
||||
activeViewportIndex !== prevProps.activeViewportIndex ||
|
||||
activeDisplaySetInstanceUID !== prevActiveDisplaySetInstanceUID
|
||||
) {
|
||||
|
||||
) {
|
||||
this.setState({
|
||||
thumbnails: _mapStudiesToThumbnails(studies, activeDisplaySetInstanceUID),
|
||||
thumbnails: _mapStudiesToThumbnails(
|
||||
studies,
|
||||
activeDisplaySetInstanceUID
|
||||
),
|
||||
activeDisplaySetInstanceUID,
|
||||
});
|
||||
}
|
||||
if (isStudyLoaded && isStudyLoaded !== prevProps.isStudyLoaded) {
|
||||
@ -288,14 +299,12 @@ class Viewer extends Component {
|
||||
</UserManagerContext.Consumer>
|
||||
)}
|
||||
</WhiteLabelingContext.Consumer>
|
||||
|
||||
{/* TOOLBAR */}
|
||||
<ErrorBoundaryDialog context="ToolbarRow">
|
||||
<ToolbarRow
|
||||
activeViewport={
|
||||
this.props.viewports[this.props.activeViewportIndex]
|
||||
}
|
||||
isDerivedDisplaySetsLoaded={this.props.isDerivedDisplaySetsLoaded}
|
||||
isLeftSidePanelOpen={this.state.isLeftSidePanelOpen}
|
||||
isRightSidePanelOpen={this.state.isRightSidePanelOpen}
|
||||
selectedLeftSidePanel={
|
||||
@ -332,10 +341,9 @@ class Viewer extends Component {
|
||||
studies={this.props.studies}
|
||||
/>
|
||||
</ErrorBoundaryDialog>
|
||||
|
||||
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
|
||||
{/*<StudyPrefetcher studies={this.props.studies} />*/}
|
||||
|
||||
<AppContext.Consumer>
|
||||
{appContext => <StudyLoadingMonitor studies={this.props.studies} />}
|
||||
</AppContext.Consumer>
|
||||
{/* VIEWPORTS + SIDEPANELS */}
|
||||
<div className="FlexboxLayout">
|
||||
{/* LEFT */}
|
||||
@ -348,10 +356,24 @@ class Viewer extends Component {
|
||||
activeIndex={this.props.activeViewportIndex}
|
||||
/>
|
||||
) : (
|
||||
<ConnectedStudyBrowser
|
||||
studies={this.state.thumbnails}
|
||||
studyMetadata={this.props.studies}
|
||||
/>
|
||||
<AppContext.Consumer>
|
||||
{appContext => {
|
||||
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>
|
||||
</ErrorBoundaryDialog>
|
||||
@ -359,6 +381,22 @@ class Viewer extends Component {
|
||||
{/* MAIN */}
|
||||
<div className={classNames('main-content')}>
|
||||
<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
|
||||
studies={this.props.studies}
|
||||
isStudyLoaded={this.props.isStudyLoaded}
|
||||
@ -410,7 +448,10 @@ export default withDialog(Viewer);
|
||||
* @param {*object} displaySet
|
||||
* @returns {[string]} an array of strings containing the warnings
|
||||
*/
|
||||
const _checkForSeriesInconsistencesWarnings = async function (displaySet, studies) {
|
||||
const _checkForSeriesInconsistencesWarnings = async function(
|
||||
displaySet,
|
||||
studies
|
||||
) {
|
||||
if (displaySet.inconsistencyWarnings) {
|
||||
// warnings already checked and cached in displaySet
|
||||
return displaySet.inconsistencyWarnings;
|
||||
@ -418,23 +459,34 @@ const _checkForSeriesInconsistencesWarnings = async function (displaySet, studie
|
||||
const inconsistencyWarnings = [];
|
||||
|
||||
if (displaySet.Modality !== 'SEG') {
|
||||
if (displaySet.reconstructionIssues && displaySet.reconstructionIssues.length !== 0) {
|
||||
if (
|
||||
displaySet.reconstructionIssues &&
|
||||
displaySet.reconstructionIssues.length !== 0
|
||||
) {
|
||||
displaySet.reconstructionIssues.forEach(warning => {
|
||||
switch (warning) {
|
||||
case ReconstructionIssues.DATASET_4D:
|
||||
inconsistencyWarnings.push('The dataset is 4D.');
|
||||
break;
|
||||
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;
|
||||
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;
|
||||
case ReconstructionIssues.VARYING_IMAGESORIENTATION:
|
||||
inconsistencyWarnings.push('The dataset frames have different orientation.');
|
||||
inconsistencyWarnings.push(
|
||||
'The dataset frames have different orientation.'
|
||||
);
|
||||
break;
|
||||
case ReconstructionIssues.IRREGULAR_SPACING:
|
||||
inconsistencyWarnings.push('The dataset frames have different pixel spacing.');
|
||||
inconsistencyWarnings.push(
|
||||
'The dataset frames have different pixel spacing.'
|
||||
);
|
||||
break;
|
||||
case ReconstructionIssues.MULTIFFRAMES:
|
||||
inconsistencyWarnings.push('The dataset is a multiframes.');
|
||||
@ -443,13 +495,22 @@ const _checkForSeriesInconsistencesWarnings = async function (displaySet, studie
|
||||
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.find(warn => warn === ReconstructionIssues.DATASET_4D)))) {
|
||||
inconsistencyWarnings.push('The datasets is missing frames: ' + displaySet.missingFrames + '.');
|
||||
(displaySet.reconstructionIssues &&
|
||||
!displaySet.reconstructionIssues.find(
|
||||
warn => warn === ReconstructionIssues.DATASET_4D
|
||||
)))
|
||||
) {
|
||||
inconsistencyWarnings.push(
|
||||
'The datasets is missing frames: ' + displaySet.missingFrames + '.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const segMetadata = displaySet.metadata;
|
||||
@ -458,24 +519,31 @@ const _checkForSeriesInconsistencesWarnings = async function (displaySet, studie
|
||||
return inconsistencyWarnings;
|
||||
}
|
||||
|
||||
const { referencedDisplaySet } = displaySet.getSourceDisplaySet(studies, false);
|
||||
const { referencedDisplaySet } = displaySet.getSourceDisplaySet(
|
||||
studies,
|
||||
false
|
||||
);
|
||||
if (!referencedDisplaySet) {
|
||||
displaySet.inconsistencyWarnings = inconsistencyWarnings;
|
||||
return inconsistencyWarnings;
|
||||
}
|
||||
|
||||
const imageIds = referencedDisplaySet.images.map(image => image.getImageId());
|
||||
const imageIds = referencedDisplaySet.images.map(image =>
|
||||
image.getImageId()
|
||||
);
|
||||
if (!imageIds || imageIds.length === 0) {
|
||||
displaySet.inconsistencyWarnings = inconsistencyWarnings;
|
||||
return inconsistencyWarnings;
|
||||
}
|
||||
|
||||
for (
|
||||
let i = 0, groupsLen = segMetadata.PerFrameFunctionalGroupsSequence.length;
|
||||
let i = 0,
|
||||
groupsLen = segMetadata.PerFrameFunctionalGroupsSequence.length;
|
||||
i < groupsLen;
|
||||
++i
|
||||
) {
|
||||
const PerFrameFunctionalGroups = segMetadata.PerFrameFunctionalGroupsSequence[i];
|
||||
const PerFrameFunctionalGroups =
|
||||
segMetadata.PerFrameFunctionalGroupsSequence[i];
|
||||
if (!PerFrameFunctionalGroups) {
|
||||
continue;
|
||||
}
|
||||
@ -485,29 +553,27 @@ const _checkForSeriesInconsistencesWarnings = async function (displaySet, studie
|
||||
SourceImageSequence = segMetadata.SourceImageSequence[i];
|
||||
} else if (PerFrameFunctionalGroups.DerivationImageSequence) {
|
||||
SourceImageSequence =
|
||||
PerFrameFunctionalGroups.DerivationImageSequence
|
||||
.SourceImageSequence;
|
||||
PerFrameFunctionalGroups.DerivationImageSequence.SourceImageSequence;
|
||||
}
|
||||
if (!SourceImageSequence) {
|
||||
if (inconsistencyWarnings.length === 0) {
|
||||
const warningMessage = 'The segmentation ' +
|
||||
const warningMessage =
|
||||
'The segmentation ' +
|
||||
'has frames out of plane respect to the source images.';
|
||||
inconsistencyWarnings.push(warningMessage);
|
||||
inconsistencyWarnings.push(warningMessage);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const {
|
||||
ReferencedSOPInstanceUID,
|
||||
} = SourceImageSequence;
|
||||
const { ReferencedSOPInstanceUID } = SourceImageSequence;
|
||||
|
||||
const imageId = imageIds.find(imageId => {
|
||||
const sopCommonModule = cornerstone.metaData.get(
|
||||
"sopCommonModule",
|
||||
imageId
|
||||
'sopCommonModule',
|
||||
imageId
|
||||
);
|
||||
if (!sopCommonModule) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
return sopCommonModule.sopInstanceUID === ReferencedSOPInstanceUID;
|
||||
@ -517,33 +583,32 @@ const _checkForSeriesInconsistencesWarnings = async function (displaySet, studie
|
||||
continue;
|
||||
}
|
||||
|
||||
const sourceImageMetadata = cornerstone.metaData.get(
|
||||
"instance",
|
||||
imageId
|
||||
);
|
||||
const sourceImageMetadata = cornerstone.metaData.get('instance', imageId);
|
||||
if (
|
||||
segMetadata.Rows !== sourceImageMetadata.Rows ||
|
||||
segMetadata.Columns !== sourceImageMetadata.Columns
|
||||
) {
|
||||
const warningMessage = 'The segmentation ' +
|
||||
const warningMessage =
|
||||
'The segmentation ' +
|
||||
'has frames with different geometry ' +
|
||||
'dimensions (Rows and Columns) respect to the source images.';
|
||||
inconsistencyWarnings.push(warningMessage);
|
||||
inconsistencyWarnings.push(warningMessage);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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.';
|
||||
inconsistencyWarnings.push(warningMessage);
|
||||
inconsistencyWarnings.push(warningMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// cache the warnings
|
||||
displaySet.inconsistencyWarnings = inconsistencyWarnings;
|
||||
return inconsistencyWarnings;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns {boolean} is active.
|
||||
*/
|
||||
const _isDisplaySetActive = function(displaySet, studies, activeDisplaySetInstanceUID) {
|
||||
const _isDisplaySetActive = function(
|
||||
displaySet,
|
||||
studies,
|
||||
activeDisplaySetInstanceUID
|
||||
) {
|
||||
let active = false;
|
||||
|
||||
const {
|
||||
displaySetInstanceUID,
|
||||
} = displaySet;
|
||||
const { displaySetInstanceUID } = displaySet;
|
||||
|
||||
// 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
|
||||
if (displaySet.Modality !== 'SEG' &&
|
||||
if (
|
||||
displaySet.Modality !== 'SEG' &&
|
||||
displaySet.Modality !== 'RTSTRUCT' &&
|
||||
displaySet.Modality !== 'RTDOSE') {
|
||||
displaySet.Modality !== 'RTDOSE'
|
||||
) {
|
||||
active = activeDisplaySetInstanceUID === displaySetInstanceUID;
|
||||
} else if (displaySet.getSourceDisplaySet){
|
||||
} else if (displaySet.getSourceDisplaySet) {
|
||||
if (displaySet.Modality === 'SEG') {
|
||||
const { referencedDisplaySet } = displaySet.getSourceDisplaySet(studies, false);
|
||||
active = referencedDisplaySet ?
|
||||
activeDisplaySetInstanceUID === referencedDisplaySet.displaySetInstanceUID :
|
||||
false;
|
||||
const { referencedDisplaySet } = displaySet.getSourceDisplaySet(
|
||||
studies,
|
||||
false
|
||||
);
|
||||
active = referencedDisplaySet
|
||||
? activeDisplaySetInstanceUID ===
|
||||
referencedDisplaySet.displaySetInstanceUID
|
||||
: false;
|
||||
} else {
|
||||
const referencedDisplaySet = displaySet.getSourceDisplaySet(studies, false);
|
||||
active = referencedDisplaySet ?
|
||||
activeDisplaySetInstanceUID === referencedDisplaySet.displaySetInstanceUID :
|
||||
false;
|
||||
const referencedDisplaySet = displaySet.getSourceDisplaySet(
|
||||
studies,
|
||||
false
|
||||
);
|
||||
active = referencedDisplaySet
|
||||
? activeDisplaySetInstanceUID ===
|
||||
referencedDisplaySet.displaySetInstanceUID
|
||||
: false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -629,11 +706,17 @@ const _mapStudiesToThumbnails = function(studies, activeDisplaySetInstanceUID) {
|
||||
altImageText = displaySet.Modality ? displaySet.Modality : 'UN';
|
||||
}
|
||||
|
||||
const hasWarnings = _checkForSeriesInconsistencesWarnings(displaySet, studies);
|
||||
const active = _isDisplaySetActive(displaySet, studies, activeDisplaySetInstanceUID)
|
||||
const hasWarnings = _checkForSeriesInconsistencesWarnings(
|
||||
displaySet,
|
||||
studies
|
||||
);
|
||||
|
||||
return {
|
||||
active,
|
||||
active: _isDisplaySetActive(
|
||||
displaySet,
|
||||
studies,
|
||||
activeDisplaySetInstanceUID
|
||||
),
|
||||
imageId,
|
||||
altImageText,
|
||||
displaySetInstanceUID,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user