Lazy loading measurements from lesions table
This commit is contained in:
parent
99c1fff3da
commit
89a96f02a0
@ -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}}
|
||||||
|
|||||||
@ -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';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,44 +1,43 @@
|
|||||||
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) {
|
||||||
const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer;
|
const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer;
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user