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:
Igor Octaviano 2021-09-09 04:58:24 -03:00 committed by GitHub
parent ea0963d75f
commit 8f2604509d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 970 additions and 384 deletions

View File

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

View 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,
},
};
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,8 +11,6 @@ describe('actions', () => {
'setLayout',
'clearViewportSpecificData',
'setActiveViewportSpecificData',
'setStudyLoadingProgress',
'clearStudyLoadingProgress',
'setUserPreferences',
'setExtensionData',
'setTimepoints',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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