From 1c509a9870c493f72b397375133c01efc9dc4390 Mon Sep 17 00:00:00 2001 From: Leonardo Campos Date: Wed, 25 Jan 2017 21:12:36 -0200 Subject: [PATCH] Adding StudyPrefetcher --- .../ohif-study-list/both/schema/servers.js | 19 ++ .../viewer/viewerMain/viewerMain.js | 3 + Packages/ohif-viewerbase/client/index.js | 4 + .../client/lib/classes/StudyPrefetcher.js | 309 ++++++++++++++++++ .../client/lib/enablePrefetchOnElement.js | 42 --- .../client/lib/setActiveViewport.js | 5 +- 6 files changed, 338 insertions(+), 44 deletions(-) create mode 100644 Packages/ohif-viewerbase/client/lib/classes/StudyPrefetcher.js delete mode 100644 Packages/ohif-viewerbase/client/lib/enablePrefetchOnElement.js diff --git a/Packages/ohif-study-list/both/schema/servers.js b/Packages/ohif-study-list/both/schema/servers.js index 1b884000b..312c31bdd 100644 --- a/Packages/ohif-study-list/both/schema/servers.js +++ b/Packages/ohif-study-list/both/schema/servers.js @@ -166,6 +166,21 @@ export const UISettings = new SimpleSchema({ } }); +export const PrefetchSchema = new SimpleSchema({ + order: { + type: String, + label: 'Prefetch Order', + allowedValues: ['topdown', 'downward', 'closest'], + optional: false + }, + displaySetCount: { + type: Number, + label: 'Display Set Count', + min: 1, + defaultValue: 1 + } +}); + export const PublicServerConfig = new SimpleSchema({ verifyEmail: { type: Boolean, @@ -180,6 +195,10 @@ export const PublicServerConfig = new SimpleSchema({ ui: { type: UISettings, label: 'UI Settings' + }, + prefetch: { + type: PrefetchSchema, + label: 'Prefetch settings' } }); diff --git a/Packages/ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js b/Packages/ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js index 4dda0b944..4f6db3409 100644 --- a/Packages/ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js +++ b/Packages/ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js @@ -8,6 +8,7 @@ import { unloadHandlers } from '../../../lib/unloadHandlers'; import { hotkeyUtils } from '../../../lib/hotkeyUtils'; import { ResizeViewportManager } from '../../../lib/classes/ResizeViewportManager'; import { LayoutManager } from '../../../lib/classes/LayoutManager'; +import { StudyPrefetcher } from '../../../lib/classes/StudyPrefetcher'; Meteor.startup(() => { window.ResizeViewportManager = window.ResizeViewportManager || new ResizeViewportManager(); @@ -34,8 +35,10 @@ Template.viewerMain.onRendered(() => { const instance = Template.instance(); const { studies } = instance.data; const parentElement = instance.$('#layoutManagerTarget').get(0); + const studyPrefetcher = StudyPrefetcher.getInstance(); OHIF.viewerbase.layoutManager = new LayoutManager(parentElement, studies); + studyPrefetcher.setStudies(studies); // Enable hotkeys hotkeyUtils.enableHotkeys(); diff --git a/Packages/ohif-viewerbase/client/index.js b/Packages/ohif-viewerbase/client/index.js index 35a0d2dcb..b2a04b1a3 100644 --- a/Packages/ohif-viewerbase/client/index.js +++ b/Packages/ohif-viewerbase/client/index.js @@ -180,6 +180,10 @@ Viewerbase.ImageSet = ImageSet; import { LayoutManager } from './lib/classes/LayoutManager'; Viewerbase.LayoutManager = LayoutManager; +// StudyPrefetcher +import { StudyPrefetcher } from './lib/classes/StudyPrefetcher'; +Viewerbase.StudyPrefetcher = StudyPrefetcher; + // ResizeViewportManager import { ResizeViewportManager } from './lib/classes/ResizeViewportManager'; Viewerbase.ResizeViewportManager = ResizeViewportManager; diff --git a/Packages/ohif-viewerbase/client/lib/classes/StudyPrefetcher.js b/Packages/ohif-viewerbase/client/lib/classes/StudyPrefetcher.js new file mode 100644 index 000000000..7d04593e7 --- /dev/null +++ b/Packages/ohif-viewerbase/client/lib/classes/StudyPrefetcher.js @@ -0,0 +1,309 @@ +import { OHIF } from 'meteor/ohif:core'; +import { Session } from 'meteor/session'; +import { StackManager } from '../StackManager.js'; +import { OHIFError } from './OHIFError'; +import { $ } from 'meteor/jquery'; +import { _ } from 'meteor/underscore'; + +export class StudyPrefetcher { + + constructor(studies) { + this.studies = []; + this.prefetchDisplaySetsTimeout = 300; + this.lastActiveViewportElement = null; + + $(cornerstone).on('CornerstoneImageCacheFull.StudyPrefetcher', _.bind(this.cacheFullHandler, this)); + } + + destroy() { + this.stopPrefetching(); + $(cornerstone).off('CornerstoneImageCacheFull.StudyPrefetcher'); + } + + static getInstance() { + if (!StudyPrefetcher.instance) { + StudyPrefetcher.instance = new StudyPrefetcher(); + } + + return StudyPrefetcher.instance; + } + + setStudies(studies) { + this.stopPrefetching(); + this.studies = studies; + } + + prefetch() { + if (!this.studies || !this.studies.length) { + return; + } + + this.stopPrefetching(); + this.prefetchActiveViewport(); + this.prefetchDisplaySets(); + } + + stopPrefetching() { + this.disableViewportPrefetch(); + cornerstoneTools.requestPoolManager.clearRequestStack('prefetch'); + } + + prefetchActiveViewport() { + const activeViewportElement = OHIF.viewerbase.viewportUtils.getActiveViewportElement(); + this.enablePrefetchOnElement(activeViewportElement); + this.attachActiveViewportListeners(activeViewportElement); + } + + disableViewportPrefetch() { + $('.imageViewerViewport').each(function() { + if (!$(this).find('canvas').length) { + return; + } + + cornerstoneTools.stackPrefetch.disable(this); + }); + } + + hasStack(element) { + const stack = cornerstoneTools.getToolState(element, 'stack'); + return stack && stack.data.length && (stack.data[0].imageIds.length > 1); + } + + /** + * This function enables stack prefetching for a specified element (viewport) + * It first disables any prefetching currently occurring on any other viewports. + * + * @param element {node} DOM Node representing the viewport element + */ + enablePrefetchOnElement(element) { + if (!$(element).find('canvas').length) { + return; + } + + // Make sure there is a stack to fetch + if (this.hasStack(element)) { + // Check if this is a clip or not + const activeViewportIndex = Session.get('activeViewport'); + const contentId = Session.get('activeContentId'); + const displaySetInstanceUid = ViewerData[contentId].loadedSeriesData[activeViewportIndex].displaySetInstanceUid; + + const stack = StackManager.findStack(displaySetInstanceUid); + + if (!stack) { + throw new OHIFError(`Requested stack ${displaySetInstanceUid} was not created`); + } + + cornerstoneTools.stackPrefetch.enable(element); + } + } + + attachActiveViewportListeners(activeViewportElement) { + const newImageHandler = () => { + // It needs to be called asynchronously because cornerstone does it at the same way. + // All instance urls to be prefetched will be removed again if we add them before + // Cornerstone callback (see stackPrefetch.onImageUpdated). + this.prefetchDisplaySetsAsync(); + }; + + $(this.lastActiveViewportElement).off('CornerstoneNewImage.StudyPrefetcher'); + $(activeViewportElement).off('CornerstoneNewImage.StudyPrefetcher'); + + // Cornerstone will not attach an event listener if the element doesn't have a stack + if (this.hasStack(activeViewportElement)) { + $(activeViewportElement).on('CornerstoneNewImage.StudyPrefetcher', newImageHandler); + } + + this.lastActiveViewportElement = activeViewportElement; + } + + prefetchDisplaySetsAsync(timeout) { + timeout = timeout || this.prefetchDisplaySetsTimeout; + + clearTimeout(this.prefetchDisplaySetsHandler); + this.prefetchDisplaySetsHandler = setTimeout(() => { + this.prefetchDisplaySets(); + }, timeout); + } + + prefetchDisplaySets() { + const config = Meteor.settings.public.prefetch; + const displaySetsToPrefetch = this.getDisplaySetsToPrefetch(config); + const imageIds = this.getImageIdsFromDisplaySets(displaySetsToPrefetch); + + this.prefetchImageIds(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, noop); + }); + + requestPoolManager.startGrabbing(); + } + + getActiveViewportImage() { + const element = OHIF.viewerbase.viewportUtils.getActiveViewportElement(); + + if (!element) { + return; + } + + const enabledElement = cornerstone.getEnabledElement(element); + const image = enabledElement.image; + + return image; + } + + getStudy(image) { + const studyMetadata = cornerstoneTools.metaData.get('study', image.imageId); + return _.find(this.studies, study => study.studyInstanceUid === studyMetadata.studyInstanceUid); + } + + getSeries(study, image) { + const seriesMetadata = cornerstoneTools.metaData.get('series', image.imageId); + return _.find(study.seriesList, series => series.seriesInstanceUid === seriesMetadata.seriesInstanceUid); + } + + getInstance(series, image) { + const instanceMetadata = cornerstoneTools.metaData.get('instance', image.imageId); + return _.find(series.instances, instance => instance.sopInstanceUid === instanceMetadata.sopInstanceUid); + } + + getActiveDisplaySet(displaySets, instance) { + return _.find(displaySets, displaySet => { + return _.some(displaySet.images, displaySetImage => { + return displaySetImage.sopInstanceUid == instance.sopInstanceUid; + }); + }); + } + + getDisplaySetsToPrefetch(config) { + const image = this.getActiveViewportImage(); + + if (!image || !config || !config.displaySetCount) { + return []; + } + + const study = this.getStudy(image); + const series = this.getSeries(study, image); + const instance = this.getInstance(series, image); + const displaySets = study.displaySets; + const activeDisplaySet = this.getActiveDisplaySet(displaySets, instance); + const prefetchMethodMap = { + topdown: 'getFirstDisplaySets', + downward: 'getNextDisplaySets', + closest: 'getClosestDisplaySets' + }; + + const prefetchOrder = config.order; + const methodName = prefetchMethodMap[prefetchOrder]; + const getDisplaySets = this[methodName]; + + if (!getDisplaySets) { + OHIF.log.warn(`Invalid prefetch order configuration (${prefetchOrder})`); + return []; + } + + return getDisplaySets.call(this, displaySets, activeDisplaySet, config.displaySetCount); + } + + getFirstDisplaySets(displaySets, activeDisplaySet, displaySetCount) { + const length = displaySets.length; + const selectedDisplaySets = []; + + for (let i = 0; (i < length) && displaySetCount; i++) { + const displaySet = displaySets[i]; + + if (displaySet !== activeDisplaySet) { + selectedDisplaySets.push(displaySet); + displaySetCount--; + } + } + + return selectedDisplaySets; + } + + getNextDisplaySets(displaySets, activeDisplaySet, displaySetCount) { + const activeDisplaySetIndex = displaySets.indexOf(activeDisplaySet); + const begin = activeDisplaySetIndex + 1; + const end = Math.min(begin + displaySetCount, displaySets.length); + + return displaySets.slice(begin, end); + } + + getClosestDisplaySets(displaySets, activeDisplaySet, displaySetCount) { + const activeDisplaySetIndex = displaySets.indexOf(activeDisplaySet); + const length = displaySets.length; + const selectedDisplaySets = []; + let left = activeDisplaySetIndex - 1; + let right = activeDisplaySetIndex + 1; + + while (((left >= 0) || (right < length)) && displaySetCount) { + if (left >= 0) { + selectedDisplaySets.push(displaySets[left]); + displaySetCount--; + left--; + } + + if ((right < length) && displaySetCount) { + selectedDisplaySets.push(displaySets[right]); + displaySetCount--; + right++; + } + } + + return selectedDisplaySets; + } + + getImageIdsFromDisplaySets(displaySets) { + let imageIds = []; + + displaySets.forEach(displaySet => { + imageIds = imageIds.concat(this.getImageIdsFromDisplaySet(displaySet)); + }); + + return imageIds; + } + + getImageIdsFromDisplaySet(displaySet) { + const imageIds = []; + + displaySet.images.forEach(image => { + const numFrames = image.numFrames; + if (numFrames > 1) { + for (let i = 0; i < numFrames; i++) { + let imageId = getImageId(image, i); + imageIds.push(imageId); + } + } else { + let imageId = getImageId(image); + imageIds.push(imageId); + } + }); + + return imageIds; + } + + filterCachedImageIds(imageIds) { + return _.filter(imageIds, imageId => { + return !this.isImageCached(imageId); + }); + } + + isImageCached(imageId) { + const imagePromise = cornerstone.imageCache.getImagePromise(imageId); + return imagePromise && (imagePromise.state() === 'resolved'); + } + + cacheFullHandler() { + OHIF.log.warn('Cache full'); + this.stopPrefetching(); + } + +}; diff --git a/Packages/ohif-viewerbase/client/lib/enablePrefetchOnElement.js b/Packages/ohif-viewerbase/client/lib/enablePrefetchOnElement.js deleted file mode 100644 index 39c4b1620..000000000 --- a/Packages/ohif-viewerbase/client/lib/enablePrefetchOnElement.js +++ /dev/null @@ -1,42 +0,0 @@ -import { Session } from 'meteor/session'; -import { $ } from 'meteor/jquery'; -import { OHIF } from 'meteor/ohif:core'; -import { StackManager } from './StackManager.js'; -import { OHIFError } from './classes/OHIFError'; -/** - * This function enables stack prefetching for a specified element (viewport) - * It first disables any prefetching currently occurring on any other viewports. - * - * @param element - */ -export function enablePrefetchOnElement(element) { - OHIF.log.info('enablePrefetchOnElement'); - - // Loop through all of the viewports and disable stackPrefetch - $('.viewportContainer .imageViewerViewport').each((index, viewportElement) => { - if ($(viewportElement).find('canvas').length) { - cornerstoneTools.stackPrefetch.disable(viewportElement); - } - }); - - if ($(element).find('canvas').length) { - // If the stack in the active viewport has more than one image, - // enable prefetching for the element - const cornerstoneStack = cornerstoneTools.getToolState(element, 'stack'); - if (cornerstoneStack && cornerstoneStack.data.length && cornerstoneStack.data[0].imageIds.length > 1) { - - // Check if this is a clip or not - const activeViewportIndex = Session.get('activeViewport'); - const contentId = Session.get('activeContentId'); - const displaySetInstanceUid = ViewerData[contentId].loadedSeriesData[activeViewportIndex].displaySetInstanceUid; - - const stack = StackManager.findStack(displaySetInstanceUid); - - if (!stack) { - throw new OHIFError(`Requested stack ${displaySetInstanceUid} was not created`); - } - - cornerstoneTools.stackPrefetch.enable(element); - } - } -} diff --git a/Packages/ohif-viewerbase/client/lib/setActiveViewport.js b/Packages/ohif-viewerbase/client/lib/setActiveViewport.js index 0f933c67b..1452d46fb 100644 --- a/Packages/ohif-viewerbase/client/lib/setActiveViewport.js +++ b/Packages/ohif-viewerbase/client/lib/setActiveViewport.js @@ -3,7 +3,7 @@ import { $ } from 'meteor/jquery'; import { Random } from 'meteor/random'; import { OHIF } from 'meteor/ohif:core'; -import { enablePrefetchOnElement } from './enablePrefetchOnElement'; +import { StudyPrefetcher } from './classes/StudyPrefetcher'; import { displayReferenceLines } from './displayReferenceLines'; /** @@ -51,8 +51,9 @@ export function setActiveViewport(element) { // Cornerstone Tools compare DOM elements (check getEnabledElement cornerstone function) // so we can't pass a jQuery object as an argument, otherwise it throws an excepetion const domElement = jQueryElement.get(0); - enablePrefetchOnElement(domElement); displayReferenceLines(domElement); + StudyPrefetcher.getInstance().prefetch(); + // @TODO Add this to OHIFAfterActivateViewport handler... if (OHIF.viewer.stackImagePositionOffsetSynchronizer) { OHIF.viewer.stackImagePositionOffsetSynchronizer.update();