Lazy loading measurements from lesions table

This commit is contained in:
Bruno Alves de Faria 2017-10-31 10:15:07 -02:00
parent 99c1fff3da
commit 89a96f02a0
7 changed files with 140 additions and 142 deletions

View File

@ -1,6 +1,7 @@
<template name="measurementTableTimepointCell"> <template name="measurementTableTimepointCell">
{{#if hasDataAtThisTimepoint}} {{#if hasDataAtThisTimepoint}}
<div class="measurementTableTimepointCell noselect" tabindex="1"> <div class="measurementTableTimepointCell noselect {{#if isLoading}}loading{{/if}}" tabindex="1">
<i class="fa fa-spin fa-circle-o-notch fa-fw loading-spinner"></i>
{{displayData}} {{displayData}}
</div> </div>
{{else}} {{else}}

View File

@ -7,8 +7,7 @@ Template.measurementTableTimepointCell.helpers({
// This simple function just checks whether or not timepoint data // This simple function just checks whether or not timepoint data
// exists for this Measurement at this Timepoint // exists for this Measurement at this Timepoint
const instance = Template.instance(); const instance = Template.instance();
const rowItem = instance.data.rowItem; const { rowItem, timepointId } = instance.data;
const timepointId = instance.data.timepointId;
if (timepointId) { if (timepointId) {
const dataAtThisTimepoint = _.where(rowItem.entries, { timepointId }); const dataAtThisTimepoint = _.where(rowItem.entries, { timepointId });
@ -20,8 +19,7 @@ Template.measurementTableTimepointCell.helpers({
displayData() { displayData() {
const instance = Template.instance(); const instance = Template.instance();
const rowItem = instance.data.rowItem; const { rowItem, timepointId } = instance.data;
const timepointId = instance.data.timepointId;
let data; let data;
if (timepointId) { if (timepointId) {
@ -45,8 +43,17 @@ Template.measurementTableTimepointCell.helpers({
OHIF.log.warn('Something went wrong?'); OHIF.log.warn('Something went wrong?');
} }
const displayFunction = tool.options.measurementTable.displayFunction; const { displayFunction } = tool.options.measurementTable;
return displayFunction(data); return displayFunction(data);
},
isLoading() {
const instance = Template.instance();
const { rowItem, timepointId } = instance.data;
const { entries } = rowItem;
const measurementData = timepointId ? _.findWhere(entries, { timepointId }) : entries[0];
const { studyInstanceUid } = measurementData;
return OHIF.studies.loadingDict.get(studyInstanceUid) === 'loading';
} }
}); });

View File

@ -1,10 +1,21 @@
@import "{ohif:design}/app" @require '{ohif:design}/app'
.measurementTableTimepointCell .measurementTableTimepointCell
theme('border-left', '%s solid $uiBorderColor' % $uiBorderThickness) theme('border-left', '%s solid $uiBorderColor' % $uiBorderThickness)
theme('color', '$textPrimaryColor') theme('color', '$textPrimaryColor')
padding: 0 10px
cursor: pointer cursor: pointer
padding: 0 10px
position: relative
.loading-spinner
display: none
font-size: 16px
position: absolute
right: 5px
top: 3px
&.loading .loading-spinner
display: block
&:hover &:hover
theme('color', '$activeColor') theme('color', '$activeColor')

View File

@ -77,31 +77,9 @@ OHIF.measurements.activateMeasurements = (element, measurementData) => {
cornerstoneTools.scrollToIndex(element, imageIdIndex); cornerstoneTools.scrollToIndex(element, imageIdIndex);
} }
// TODO: Find another way to do this?
// This might update one element twice, but at least it makes sure all viewports are
// updated and the highlight is removed from inactive tools in all visible viewports
const $viewports = $('.imageViewerViewport');
$viewports.each((index, element) => {
const $element = $(element); const $element = $(element);
if (!$element.find('canvas')) { if (!$element.find('canvas').length) return;
return;
}
// TODO: Implement isEnabledElement in Cornerstone
// or maybe just remove the 'error' this throws?
let ee;
try {
ee = cornerstone.getEnabledElement(element);
} catch(error) {
OHIF.log.warn(error);
return;
}
if (!ee.image) {
return;
}
$element.trigger('ViewerMeasurementsActivated'); $element.trigger('ViewerMeasurementsActivated');
cornerstone.updateImage(element); cornerstone.updateImage(element);
});
}; };

View File

@ -1,43 +1,42 @@
import { $ } from 'meteor/jquery'; import { $ } from 'meteor/jquery';
import { _ } from 'meteor/underscore'; import { _ } from 'meteor/underscore';
import { OHIF } from 'meteor/ohif:core'; import { OHIF } from 'meteor/ohif:core';
import { cornerstone } from 'meteor/ohif:cornerstone';
function renderIntoViewport(measurementData, enabledElement, viewportIndex) {
const { activateMeasurements, findAndRenderDisplaySet } = OHIF.measurements;
const { studyInstanceUid, seriesInstanceUid, sopInstanceUid } = measurementData;
const { element } = enabledElement;
return new Promise((resolve, reject) => {
const renderedCallback = element => {
activateMeasurements(element, measurementData);
$(element).one('CornerstoneImageRendered', () => resolve());
};
// Check if the study / series we need is already the one in the viewport
if (enabledElement && enabledElement.image) {
const imageId = enabledElement.image.imageId;
const series = cornerstone.metaData.get('series', imageId);
const study = cornerstone.metaData.get('study', imageId);
const isSameStudy = study.studyInstanceUid === measurementData.studyInstanceUid;
const isSameSeries = series.seriesInstanceUid === measurementData.seriesInstanceUid;
if (isSameStudy && isSameSeries) {
// If it is, activate the measurements in this viewport and stop here
renderedCallback(element);
}
}
// Otherwise, re-render the viewport with the required study/series, then add an rendered
// callback to activate the measurements
function renderIntoViewport(viewportIndex, studyInstanceUid, seriesInstanceUid, sopInstanceUid, renderedCallback) {
// @TypeSafeStudies // @TypeSafeStudies
// First, check if we already have this study loaded const study = OHIF.viewer.Studies.findBy({ studyInstanceUid });
const alreadyLoadedStudy = OHIF.viewer.Studies.findBy({ studyInstanceUid });
const { findAndRenderDisplaySet } = OHIF.measurements;
if (alreadyLoadedStudy) {
// If the Study is already loaded, find the display set and render it
findAndRenderDisplaySet(alreadyLoadedStudy.displaySets, viewportIndex, studyInstanceUid, seriesInstanceUid, sopInstanceUid, renderedCallback);
} else {
// If not, retrieve the study metadata and then find the relevant display set and
// render it.
const $viewports = $('.imageViewerViewport');
const element = $viewports.get(viewportIndex);
const startLoadingHandler = cornerstoneTools.loadHandlerManager.getStartLoadHandler();
startLoadingHandler(element);
OHIF.studies.retrieveStudyMetadata(studyInstanceUid).then(study => {
OHIF.log.warn('renderIntoViewport');
// Double check to make sure this study wasn't already inserted
// into OHIF.viewer.Studies, so we don't cause duplicate entry errors
const loaded = OHIF.viewer.Studies.findBy({
studyInstanceUid: study.studyInstanceUid
});
if (!loaded) {
OHIF.viewer.Studies.insert(study);
}
// TODO: Support frames? e.g. for measurements on multi-frame instances
findAndRenderDisplaySet(study.displaySets, viewportIndex, studyInstanceUid, seriesInstanceUid, sopInstanceUid, renderedCallback); findAndRenderDisplaySet(study.displaySets, viewportIndex, studyInstanceUid, seriesInstanceUid, sopInstanceUid, renderedCallback);
}).catch(error => {
OHIF.log.error(`There was an error trying to retrieve the study\'s metadata for studyInstanceUid: ${studyInstanceUid}`);
OHIF.log.error(error.stack);
OHIF.log.trace();
}); });
}
} }
function syncViewports(viewportsIndexes) { function syncViewports(viewportsIndexes) {
@ -52,54 +51,70 @@ function syncViewports(viewportsIndexes) {
} }
} }
// Store the lastActivatedRowItem to cancel jumping if another rowItem was triggered during loading
let lastActivatedRowItem;
/** /**
* Activates a set of lesions when lesion table row is clicked * Activates a set of lesions when lesion table row is clicked
* *
* @param measurementId The unique key for a specific Measurement * @param measurementId The unique key for a specific Measurement
*/ */
OHIF.measurements.jumpToRowItem = (rowItem, timepoints) => { OHIF.measurements.jumpToRowItem = (rowItem, timepoints) => {
OHIF.measurements.deactivateAllToolData(); lastActivatedRowItem = rowItem;
const activateMeasurements = OHIF.measurements.activateMeasurements;
const activatedViewportIndexes = [];
let syncViewportsCaller = syncViewports;
let renderCount = 0;
// Deactivate stack synchronizer because it will be re-activated later
OHIF.viewer.stackImagePositionOffsetSynchronizer.deactivate();
// Retrieve the timepoints that are currently being displayed in the
// Measurement Table
const numTimepoints = Math.max(timepoints.length, 1);
// Retrieve the list of available viewports // Retrieve the list of available viewports
const $viewports = $('.imageViewerViewport'); const $viewports = $('.imageViewerViewport');
const numViewports = Math.max($viewports.length, 0); const numViewports = Math.max($viewports.length, 0);
// Retrieve the timepoints that are currently being displayed in the Measurement Table
const numTimepoints = Math.max(timepoints.length, 1);
const numViewportsToUpdate = Math.min(numTimepoints, numViewports); const numViewportsToUpdate = Math.min(numTimepoints, numViewports);
// Retrieve the measurements data
const measurementsData = [];
const promises = new Set();
for (let i = 0; i < numViewportsToUpdate; i++) {
const { timepointId } = timepoints[i];
const dataAtThisTimepoint = _.where(rowItem.entries, { timepointId });
if (!dataAtThisTimepoint || !dataAtThisTimepoint.length) continue;
const measurementData = dataAtThisTimepoint[0];
measurementsData.push(measurementData);
const promise = OHIF.studies.loadStudy(measurementData.studyInstanceUid);
promise.then(() => OHIF.measurements.syncMeasurementAndToolData(measurementData));
promises.add(promise);
}
// Wait for studies metadata to be retrieved before jumpint to the given row item
Promise.all(promises).then(() => {
// Stop here if another rowItem was activated during loading process
if (rowItem !== lastActivatedRowItem) return;
OHIF.measurements.deactivateAllToolData();
const activatedViewportIndexes = [];
// Deactivate stack synchronizer because it will be re-activated later
OHIF.viewer.stackImagePositionOffsetSynchronizer.deactivate();
// Display timepoints in the order of viewports which is set by the hanging protocol // Display timepoints in the order of viewports which is set by the hanging protocol
// Reverse the timepoint array if the first timepoint does not match with the first viewport data // Reverse the array if the first timepoint does not match with the first viewport data
const layoutManager = OHIF.viewerbase.layoutManager; const layoutManager = OHIF.viewerbase.layoutManager;
if (timepoints[0].studyInstanceUids.indexOf(layoutManager.viewportData[0].studyInstanceUid) < 0) { const viewportData = layoutManager.viewportData[0];
if (timepoints[0].studyInstanceUids.indexOf(viewportData.studyInstanceUid) < 0) {
timepoints.reverse(); timepoints.reverse();
} }
for (let i = 0; i < numViewportsToUpdate; i++) { const renderPromises = [];
const timepoint = timepoints[i]; for (let viewportIndex = 0; viewportIndex < numViewportsToUpdate; viewportIndex++) {
const timepointId = timepoint.timepointId; const measurementData = measurementsData[viewportIndex];
if (!measurementData) continue;
const dataAtThisTimepoint = _.where(rowItem.entries, { timepointId }); activatedViewportIndexes.push(viewportIndex);
if (!dataAtThisTimepoint || !dataAtThisTimepoint.length) {
continue;
}
const measurementData = dataAtThisTimepoint[0]; const element = $viewports.get(viewportIndex);
// Check if the study / series we need is already the one in the viewport
const element = $viewports.get(i);
activatedViewportIndexes.push(i);
// TODO: Implement isEnabledElement in Cornerstone // TODO: Implement isEnabledElement in Cornerstone
// or maybe just remove the 'error' this throws? // or maybe just remove the 'error' this throws?
@ -110,40 +125,11 @@ OHIF.measurements.jumpToRowItem = (rowItem, timepoints) => {
continue; continue;
} }
if (enabledElement && enabledElement.image) { const promise = renderIntoViewport(measurementData, enabledElement, viewportIndex);
const imageId = enabledElement.image.imageId; renderPromises.push(promise);
const series = cornerstone.metaData.get('series', imageId);
const study = cornerstone.metaData.get('study', imageId);
if (series.seriesInstanceUid === measurementData.seriesInstanceUid &&
study.studyInstanceUid === measurementData.studyInstanceUid) {
// If it is, activate the measurements in this viewport and stop here
activateMeasurements(element, measurementData);
continue;
}
} }
// The sync will be called only after loading all series on viewports // Wait for all viewports to be rendered then sync them
syncViewportsCaller = _.after(++renderCount, syncViewports); Promise.all(renderPromises).then(() => syncViewports(activatedViewportIndexes));
});
// Otherwise, re-render the viewport with the required study/series, then
// add an onRendered callback to activate the measurements
const renderedCallback = element => {
activateMeasurements(element, measurementData);
syncViewportsCaller(activatedViewportIndexes);
};
// TODO: Support frames? e.g. for measurements on multi-frame instances
renderIntoViewport(i,
measurementData.studyInstanceUid,
measurementData.seriesInstanceUid,
measurementData.sopInstanceUid,
renderedCallback);
}
// If all viewports are already rendered then sync them
if (!renderCount) {
syncViewportsCaller(activatedViewportIndexes);
}
}; };

View File

@ -34,7 +34,7 @@ Template.studyBrowserItem.onCreated(() => {
instance.autorun(() => { instance.autorun(() => {
const instance = Template.instance(); const instance = Template.instance();
OHIF.studies.loadedDict.get(studyInstanceUid); OHIF.studies.loadingDict.get(studyInstanceUid);
const studyMetadata = instance.getStudyMetadata(); const studyMetadata = instance.getStudyMetadata();
if (studyMetadata) { if (studyMetadata) {
const firstInstance = studyMetadata.getFirstInstance(); const firstInstance = studyMetadata.getFirstInstance();

View File

@ -1,8 +1,9 @@
import { ReactiveDict } from 'meteor/reactive-dict'; import { ReactiveDict } from 'meteor/reactive-dict';
import { Tracker } from 'meteor/tracker';
import { OHIF } from 'meteor/ohif:core'; import { OHIF } from 'meteor/ohif:core';
// Create a studies loaded state dictionary to enable reactivity // Create a studies loaded state dictionary to enable reactivity. Values: loading|loaded|failed
OHIF.studies.loadedDict = new ReactiveDict(); OHIF.studies.loadingDict = new ReactiveDict();
/** /**
* Load the study metadata and store its information locally * Load the study metadata and store its information locally
@ -11,7 +12,18 @@ OHIF.studies.loadedDict = new ReactiveDict();
* @returns {Promise} that will be resolved with the study metadata or rejected with an error * @returns {Promise} that will be resolved with the study metadata or rejected with an error
*/ */
OHIF.studies.loadStudy = studyInstanceUid => new Promise((resolve, reject) => { OHIF.studies.loadStudy = studyInstanceUid => new Promise((resolve, reject) => {
OHIF.studies.retrieveStudyMetadata(studyInstanceUid).then(study => { // Disable reactivity to get the current loading state
let currentLoadingState;
Tracker.nonreactive(() => {
currentLoadingState = OHIF.studies.loadingDict.get(studyInstanceUid) || '';
});
// Set the loading state as the study is not yet loaded
if (currentLoadingState !== 'loading') {
OHIF.studies.loadingDict.set(studyInstanceUid, 'loading');
}
return OHIF.studies.retrieveStudyMetadata(studyInstanceUid).then(study => {
// Add the display sets to the study if not present // Add the display sets to the study if not present
if (!study.displaySets) { if (!study.displaySets) {
const displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(study); const displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(study);
@ -30,13 +42,16 @@ OHIF.studies.loadStudy = studyInstanceUid => new Promise((resolve, reject) => {
OHIF.viewer.StudyMetadataList.insert(study); OHIF.viewer.StudyMetadataList.insert(study);
} }
// Add the study to the loading listener to display loading progress on series thumbnails // Add the study to the loading listener to allow loading progress handling
const studyLoadingListener = OHIF.viewerbase.StudyLoadingListener.getInstance(); const studyLoadingListener = OHIF.viewerbase.StudyLoadingListener.getInstance();
studyLoadingListener.addStudy(study); studyLoadingListener.addStudy(study);
// Add the studyInstanceUid to the loaded state dictionary // Add the studyInstanceUid to the loaded state dictionary
OHIF.studies.loadedDict.set(study.studyInstanceUid, true); OHIF.studies.loadingDict.set(studyInstanceUid, 'loaded');
resolve(study); resolve(study);
}).catch(reject); }).catch((...args) => {
OHIF.studies.loadingDict.set(studyInstanceUid, 'failed');
reject(args);
});
}); });