diff --git a/.jshintrc b/.jshintrc
index 24dfc91f0..1067213b4 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -87,6 +87,7 @@
"wsh" : false, // Windows Scripting Host
"yui" : false, // Yahoo User Interface
"globals" : {
+ "require": true,
"Package" : true, // Meteor Package definition
"cornerstone": true, // This group: cornerstone globals
diff --git a/Packages/ohif-design/styles/common/global.styl b/Packages/ohif-design/styles/common/global.styl
index 61936d84f..d48b1a772 100644
--- a/Packages/ohif-design/styles/common/global.styl
+++ b/Packages/ohif-design/styles/common/global.styl
@@ -6,7 +6,7 @@ html body
html body.stretch
height: 100%
- minWidth: 0
+ min-width: 0
overflow: hidden
position: fixed
width: 100%
diff --git a/Packages/ohif-log/main.js b/Packages/ohif-log/main.js
index 984b6fada..214a6b5b0 100644
--- a/Packages/ohif-log/main.js
+++ b/Packages/ohif-log/main.js
@@ -7,3 +7,22 @@ const defaultLevel = Meteor.isProduction ? 'ERROR' : 'TRACE';
// Create package logger using loglevel
OHIF.log = loglevel.getLogger('OHIF');
OHIF.log.setLevel(defaultLevel);
+
+// Add time and timeEnd to OHIF.log namespace
+const times = new Map();
+
+// Register the time method
+OHIF.log.time = givenKey => {
+ const key = typeof givenKey === 'undefined' ? 'default' : givenKey;
+ times.set(key, new Date().getTime());
+};
+
+// Register the timeEnd method
+OHIF.log.timeEnd = givenKey => {
+ const key = typeof givenKey === 'undefined' ? 'default' : givenKey;
+ const now = new Date().getTime();
+ const last = times.get(key) || now;
+ times.delete(key);
+ const duration = now - last;
+ OHIF.log.info(`${key}: ${duration}ms`);
+};
diff --git a/Packages/ohif-studies/both/main.js b/Packages/ohif-studies/both/main.js
new file mode 100644
index 000000000..ce3f3bdec
--- /dev/null
+++ b/Packages/ohif-studies/both/main.js
@@ -0,0 +1,5 @@
+import { OHIF } from 'meteor/ohif:core';
+
+OHIF.studies = {};
+
+require('../imports/both');
diff --git a/Packages/ohif-studies/client/main.js b/Packages/ohif-studies/client/main.js
new file mode 100644
index 000000000..a6fe3859b
--- /dev/null
+++ b/Packages/ohif-studies/client/main.js
@@ -0,0 +1 @@
+require('../imports/client');
diff --git a/Packages/ohif-studies/imports/both/index.js b/Packages/ohif-studies/imports/both/index.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/Packages/ohif-studies/imports/client/components/browser/index.js b/Packages/ohif-studies/imports/client/components/browser/index.js
new file mode 100644
index 000000000..7dfd1b27e
--- /dev/null
+++ b/Packages/ohif-studies/imports/client/components/browser/index.js
@@ -0,0 +1,5 @@
+import './list.html';
+import './list.js';
+import './item.html';
+import './item.js';
+import './item.styl';
diff --git a/Packages/ohif-studies/imports/client/components/browser/item.html b/Packages/ohif-studies/imports/client/components/browser/item.html
new file mode 100644
index 000000000..ec171da77
--- /dev/null
+++ b/Packages/ohif-studies/imports/client/components/browser/item.html
@@ -0,0 +1,21 @@
+
+
+
+ {{#if isLoading}}
+ {{>loadingText}}
+ {{/if}}
+
+
+
{{instance.modalities}}
+
+
+
{{formatDA this.studyInformation.date 'D-MMM-YYYY'}}
+
{{this.studyInformation.description}}
+
+
+
+ {{#if this.settings.studyTemplate}}
+ {{>Template.dynamic template=this.settings.studyTemplate data=(clone this)}}
+ {{/if}}
+
+
diff --git a/Packages/ohif-studies/imports/client/components/browser/item.js b/Packages/ohif-studies/imports/client/components/browser/item.js
new file mode 100644
index 000000000..449e30d15
--- /dev/null
+++ b/Packages/ohif-studies/imports/client/components/browser/item.js
@@ -0,0 +1,43 @@
+import { Template } from 'meteor/templating';
+import { $ } from 'meteor/jquery';
+
+Template.studyBrowserItem.onCreated(() => {
+ const instance = Template.instance();
+
+ const modality = instance.data.studyInformation.modality || 'UN';
+ instance.modalities = modality.replace(/\\/g, ' ');
+});
+
+Template.studyBrowserItem.events({
+ 'click .study-browser-item'(event, instance) {
+ const element = event.currentTarget;
+ const $element = $(element);
+ $element.trigger('ohif.studies.study.click', instance.data.studyInformation);
+ const { settings, studyInformation } = instance.data;
+ if (settings && typeof settings.studyClickCallback) {
+ settings.studyClickCallback(studyInformation, element);
+ }
+ }
+});
+
+Template.studyBrowserItem.helpers({
+ modalityStyle() {
+ // Responsively styles the Modality Acronyms for studies
+ // with more than one modality
+ const instance = Template.instance();
+ const numModalities = instance.modalities.split(/\s/g).length;
+
+ if (numModalities === 1) {
+ // If we have only one modality, it should take up the whole div.
+ return 'font-size: 1em';
+ } else if (numModalities === 2) {
+ // If we have two, let them sit side-by-side
+ return 'font-size: 0.75em';
+ } else {
+ // If we have more than two modalities, change the line height to display multiple rows,
+ // depending on the number of modalities we need to display.
+ const lineHeight = Math.ceil(numModalities / 2) * 1.2;
+ return 'line-height: ' + lineHeight + 'em';
+ }
+ }
+});
diff --git a/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepointStudy.styl b/Packages/ohif-studies/imports/client/components/browser/item.styl
similarity index 92%
rename from Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepointStudy.styl
rename to Packages/ohif-studies/imports/client/components/browser/item.styl
index cc1c4a847..15079a1ab 100644
--- a/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepointStudy.styl
+++ b/Packages/ohif-studies/imports/client/components/browser/item.styl
@@ -1,4 +1,4 @@
-@import "{ohif:design}/app"
+@require '{ohif:design}/app'
$boxBorderColor = transparent
$boxHoverBackgroundColor = #14191E
@@ -11,7 +11,7 @@ $nestingMargin = 6px
$spacerX = 7px
$spacerY = 12px
-.studyTimepointStudy
+.study-browser-item
position: relative
// required transformation to make inner fixed elements relative to this one
transform(scale(1))
@@ -33,12 +33,12 @@ $spacerY = 12px
opacity: 0.75
right: 16px
- .studyModality
+ .study-item-box
opacity: 0.2
&.active
- .studyModality
- .studyModalityBox
+ .study-item-box
+ .study-modality
theme('color', '$primaryBackgroundColor')
&
&:before
@@ -53,7 +53,7 @@ $spacerY = 12px
overflow: hidden
transition($sidebarTransition)
-.studyModality
+.study-item-box
border: 1px solid $boxBorderColor
border-radius: 12px
cursor: pointer
@@ -78,28 +78,29 @@ $spacerY = 12px
padding: 0
text-align: center
- .studyText
+ .study-text
font-size: 13px
left: ($spacerX * 3) + $boxWidth + ($nestingMargin * 3)
line-height: 14px
position: absolute
right: $spacerX
top: $spacerY
- .studyDate
+ .study-date
margin-top: 8px
theme('color', '$textSecondaryColor')
- .studyDescription
+ .study-description
margin-top: 8px
theme('color', '$textPrimaryColor')
- .studyModalityBox
+ .study-modality
theme('color', '$textSecondaryColor')
+ font-size: 20px
line-height: $boxWidth
margin-left: $nestingMargin * 2
margin-top: $nestingMargin * 2
position: relative
- .studyModalityText
+ .study-modality-text
text-align: center
text-transform: uppercase
height: 100%
diff --git a/Packages/ohif-studies/imports/client/components/browser/list.html b/Packages/ohif-studies/imports/client/components/browser/list.html
new file mode 100644
index 000000000..3ea705448
--- /dev/null
+++ b/Packages/ohif-studies/imports/client/components/browser/list.html
@@ -0,0 +1,7 @@
+
+
+ {{#each studyInformation in studiesInformation}}
+ {{>studyBrowserItem (clone this studyInformation=studyInformation)}}
+ {{/each}}
+
+
diff --git a/Packages/ohif-studies/imports/client/components/browser/list.js b/Packages/ohif-studies/imports/client/components/browser/list.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/Packages/ohif-studies/imports/client/components/index.js b/Packages/ohif-studies/imports/client/components/index.js
new file mode 100644
index 000000000..edb87e45d
--- /dev/null
+++ b/Packages/ohif-studies/imports/client/components/index.js
@@ -0,0 +1 @@
+import './browser';
diff --git a/Packages/ohif-studies/imports/client/index.js b/Packages/ohif-studies/imports/client/index.js
new file mode 100644
index 000000000..a3b6ff5b9
--- /dev/null
+++ b/Packages/ohif-studies/imports/client/index.js
@@ -0,0 +1,2 @@
+import './components';
+import './lib';
diff --git a/Packages/ohif-studies/imports/client/lib/index.js b/Packages/ohif-studies/imports/client/lib/index.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/Packages/ohif-studies/imports/client/lib/retrieveStudyMetadata.js b/Packages/ohif-studies/imports/client/lib/retrieveStudyMetadata.js
new file mode 100644
index 000000000..9b484962a
--- /dev/null
+++ b/Packages/ohif-studies/imports/client/lib/retrieveStudyMetadata.js
@@ -0,0 +1,102 @@
+import { Meteor } from 'meteor/meteor';
+import { OHIF } from 'meteor/ohif:core';
+import 'meteor/ohif:viewerbase';
+
+// Define the StudyMetaDataPromises object. This is used as a cache to store study meta data
+// promises and prevent unnecessary subsequent calls to the server
+const StudyMetaDataPromises = new Map();
+
+/**
+ * Retrieves study metadata using a server call
+ *
+ * @param {String} studyInstanceUid The UID of the Study to be retrieved
+ * @returns {Promise} that will be resolved with the metadata or rejected with the error
+ */
+OHIF.studies.retrieveStudyMetadata = (studyInstanceUid, seriesInstanceUids) => {
+
+ // @TODO: Whenever a study metadata request has failed, its related promise will be rejected once and for all
+ // and further requests for that metadata will always fail. On failure, we probably need to remove the
+ // corresponding promise from the "StudyMetaDataPromises" map...
+
+ // If the StudyMetaDataPromises cache already has a pending or resolved promise related to the
+ // given studyInstanceUid, then that promise is returned
+ if (StudyMetaDataPromises.has(studyInstanceUid)) {
+ return StudyMetaDataPromises.get(studyInstanceUid);
+ }
+
+ const seriesKeys = Array.isArray(seriesInstanceUids) ? '|' + seriesInstanceUids.join('|') : '';
+ const timingKey = `retrieveStudyMetadata[${studyInstanceUid}${seriesKeys}]`;
+ OHIF.log.time(timingKey);
+
+ // Create a promise to handle the data retrieval
+ const promise = new Promise((resolve, reject) => {
+ // If no study metadata is in the cache variable, we need to retrieve it from
+ // the server with a call.
+ Meteor.call('GetStudyMetadata', studyInstanceUid, function(error, study) {
+ OHIF.log.timeEnd(timingKey);
+
+ if (error) {
+ const errorType = error.error;
+ let errorMessage = '';
+
+ if (errorType === 'server-connection-error') {
+ errorMessage = 'There was an error connecting to the DICOM server, please verify if it is up and running.';
+ } else if (errorType === 'server-internal-error') {
+ errorMessage = `There was an internal error with the DICOM server getting metadeta for ${studyInstanceUid}`;
+ } else {
+ errorMessage = `For some reason we could not retrieve the study\'s metadata for ${studyInstanceUid}.`;
+ }
+
+ OHIF.log.error(errorMessage);
+ OHIF.log.error(error.stack);
+ reject(`GetStudyMetadata: ${errorMessage}`);
+ return;
+ }
+
+ // Filter series if seriesInstanceUid exists
+ if (seriesInstanceUids && seriesInstanceUids.length) {
+ study.seriesList = study.seriesList.filter(series => seriesInstanceUids.indexOf(series.seriesInstanceUid) > -1);
+ }
+
+ if (!study) {
+ reject(`GetStudyMetadata: No study data returned from server: ${studyInstanceUid}`);
+ return;
+ }
+
+ if (window.HipaaLogger && Meteor.user && Meteor.user()) {
+ window.HipaaLogger.logEvent({
+ eventType: 'viewed',
+ userId: Meteor.userId(),
+ userName: Meteor.user().profile.fullName,
+ collectionName: 'Study',
+ recordId: studyInstanceUid,
+ patientId: study.patientId,
+ patientName: study.patientName
+ });
+ }
+
+ // Once the data was retrieved, the series are sorted by series and instance number
+ OHIF.viewerbase.sortStudy(study);
+
+ // Updates WADO-RS metaDataManager
+ OHIF.viewerbase.updateMetaDataManager(study);
+
+ // Transform the study in a StudyMetadata object
+ const studyMetadata = new OHIF.metadata.StudyMetadata(study);
+
+ // Add the display sets to the study
+ study.displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(studyMetadata);
+ study.displaySets.forEach(displaySet => {
+ OHIF.viewerbase.stackManager.makeAndAddStack(study, displaySet);
+ });
+
+ // Resolve the promise with the final study metadata object
+ resolve(study);
+ });
+ });
+
+ // Store the promise in cache
+ StudyMetaDataPromises.set(studyInstanceUid, promise);
+
+ return promise;
+};
diff --git a/Packages/ohif-studies/imports/server/index.js b/Packages/ohif-studies/imports/server/index.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/Packages/ohif-studies/package.js b/Packages/ohif-studies/package.js
new file mode 100644
index 000000000..dbf40fc2f
--- /dev/null
+++ b/Packages/ohif-studies/package.js
@@ -0,0 +1,35 @@
+Package.describe({
+ name: 'ohif:studies',
+ summary: 'OHIF Studies Library to deal with studies UI, retrieval and manipulation',
+ version: '0.0.1'
+});
+
+Package.onUse(function(api) {
+ api.versionsFrom('1.4');
+
+ api.use([
+ 'ecmascript',
+ 'templating',
+ 'stylus',
+ 'http'
+ ]);
+
+ // Our custom packages
+ api.use([
+ 'ohif:design',
+ 'ohif:core',
+ 'ohif:log',
+ 'ohif:dicom-services',
+ 'ohif:viewerbase',
+ 'ohif:wadoproxy'
+ ]);
+
+ // Client and server imports
+ api.addFiles('both/main.js', [ 'client', 'server' ]);
+
+ // Server imports
+ api.addFiles('server/main.js', 'server');
+
+ // Client imports
+ api.addFiles('client/main.js', 'client');
+});
diff --git a/Packages/ohif-studies/server/main.js b/Packages/ohif-studies/server/main.js
new file mode 100644
index 000000000..3baa1920b
--- /dev/null
+++ b/Packages/ohif-studies/server/main.js
@@ -0,0 +1 @@
+require('../imports/server');
diff --git a/Packages/ohif-viewerbase/client/components/viewer/studySeriesQuickSwitch/studySeriesQuickSwitch.js b/Packages/ohif-viewerbase/client/components/viewer/studySeriesQuickSwitch/studySeriesQuickSwitch.js
index 6dba132af..78ecdb537 100644
--- a/Packages/ohif-viewerbase/client/components/viewer/studySeriesQuickSwitch/studySeriesQuickSwitch.js
+++ b/Packages/ohif-viewerbase/client/components/viewer/studySeriesQuickSwitch/studySeriesQuickSwitch.js
@@ -53,7 +53,7 @@ Template.studySeriesQuickSwitch.onCreated(() => {
const checkScrollArea = element => {
const { scrollHeight, offsetHeight, scrollTop } = element;
-
+
const matrix = $(element).find('.thumbnailsWrapper').css('transform');
let translateY = 0;
@@ -92,7 +92,7 @@ Template.studySeriesQuickSwitch.events({
instance.$('.js-quick-switch, .switchSectionSeries').removeClass('hover');
instance.$('.quickSwitchWrapper').removeClass('overlay');
},
- 'click .studyTimepointStudy'(event, instance) {
+ 'click .study-browser-item'(event, instance) {
instance.$('.switchSectionSeries').addClass('hover');
},
'scroll .scrollArea'(event) {
diff --git a/Packages/ohif-viewerbase/client/components/viewer/studySeriesQuickSwitch/studySeriesQuickSwitch.styl b/Packages/ohif-viewerbase/client/components/viewer/studySeriesQuickSwitch/studySeriesQuickSwitch.styl
index da0743073..200bf460e 100644
--- a/Packages/ohif-viewerbase/client/components/viewer/studySeriesQuickSwitch/studySeriesQuickSwitch.styl
+++ b/Packages/ohif-viewerbase/client/components/viewer/studySeriesQuickSwitch/studySeriesQuickSwitch.styl
@@ -105,7 +105,7 @@ $seriesSpacing = 2px
.studySwitch
.studyTimepointBrowser
background-color: transparent
- .studyTimepointStudy.active .studyModality
+ .study-browser-item.active .study-item-box
theme('box-shadow', 'inset 0 0 0 3px $activeColor')
.studyBox
theme('background-color', '$uiGrayDark')
@@ -164,10 +164,10 @@ $seriesSpacing = 2px
overflow-x: hidden
overflow-y: scroll
width: calc(100% + 22px)
-
+
&.is-mac
padding-right: 22px
-
+
&.show-scroll-indicator-up:before
&.show-scroll-indicator-down:after
font-family: FontAwesome
@@ -180,11 +180,11 @@ $seriesSpacing = 2px
z-index: 1
text-align: center
left: 0
-
+
&.show-scroll-indicator-up:before
top: -10px
content: '\f102'
-
+
&.show-scroll-indicator-down:after
bottom: 18px
content: '\f103'
diff --git a/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepoint.js b/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepoint.js
index 9dca64c32..d4314e09b 100644
--- a/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepoint.js
+++ b/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepoint.js
@@ -53,7 +53,7 @@ Template.studyTimepoint.events({
}
// Removes selected state from all studies but the triggered study
- $studiesTarget.find('.studyTimepointStudy').not($selection).removeClass('active');
+ $studiesTarget.find('.study-browser-item').not($selection).removeClass('active');
if (changed.isQuickSwitch) {
// Reset active studies map to allow only one active study
diff --git a/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepointBrowser.html b/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepointBrowser.html
index 039702e35..ed989585c 100644
--- a/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepointBrowser.html
+++ b/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepointBrowser.html
@@ -26,7 +26,7 @@
{{/each}}
{{#if and this.currentStudy (not showAdditionalTimepoints) hasAdditionalTimepoints}}
-
+
Show additional timepoints
{{/if}}
diff --git a/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepointBrowser.js b/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepointBrowser.js
index b2f326614..367f7196f 100644
--- a/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepointBrowser.js
+++ b/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepointBrowser.js
@@ -28,22 +28,18 @@ Template.studyTimepointBrowser.onCreated(() => {
}
return timepoint.studyInstanceUids.map(studyInstanceUid => {
- const query = {
- patientId: timepoint.patientId,
- studyInstanceUid: studyInstanceUid
- };
+ const query = { studyInstanceUid };
const loadedStudy = OHIF.viewer.Studies.findBy(query);
- if (loadedStudy) {
- return loadedStudy;
- }
+ if (loadedStudy) return loadedStudy;
const notYetLoaded = OHIF.studylist.collections.Studies.findOne(query);
- if (!notYetLoaded) {
- throw new OHIFError(`No study data available for Study: ${studyInstanceUid}`);
- }
+ if (notYetLoaded) return notYetLoaded;
- return notYetLoaded;
+ // const studyData = _.findWhere(timepoint.studiesData, query);
+ // if (studyData) return studyData;
+
+ throw new OHIFError(`No study data available for Study: ${studyInstanceUid}`);
});
};
});
@@ -57,7 +53,7 @@ Template.studyTimepointBrowser.onRendered(() => {
const type = instance.timepointViewType.get();
// Removes all active classes to collapse the timepoints and studies
- instance.$('.timepointEntry, .studyTimepointStudy').removeClass('active');
+ instance.$('.timepointEntry, .study-browser-item').removeClass('active');
if (type === 'key' && !instance.data.currentStudy) {
// Show only first timepoint expanded for key timepoints
instance.$('.timepointEntry:first').addClass('active');
@@ -71,7 +67,7 @@ Template.studyTimepointBrowser.onRendered(() => {
// Wait for rerendering and set the timepoint as active
instance.refreshActiveStudies = () => Tracker.afterFlush(() => {
_.each(activeStudiesUids, studyInstanceUid => {
- instance.$(`.studyTimepointStudy[data-uid='${studyInstanceUid}']`).addClass('active');
+ instance.$(`.study-browser-item[data-uid='${studyInstanceUid}']`).addClass('active');
});
// Show only first timepoint expanded for key timepoints
instance.$('.timepointEntry:first').addClass('active');
@@ -108,7 +104,7 @@ Template.studyTimepointBrowser.events({
$timepoint.toggleClass('active');
},
- 'click .studyModality.additional'(event, instance) {
+ 'click .study-item-box.additional'(event, instance) {
// Show all key timepoints
instance.showAdditionalTimepoints.set(true);
}
@@ -199,12 +195,22 @@ Template.studyTimepointBrowser.helpers({
const studies = instance.getStudies(timepoint);
+ const includedUids = new Set();
const modalities = {};
studies.forEach(study => {
const modality = study.modalities || 'UN';
modalities[modality] = modalities[modality] + 1 || 1;
+ includedUids.add(study.studyInstanceUid);
});
+ if (_.isArray(timepoint.studiesData)) {
+ timepoint.studiesData.forEach(({ modality, studyInstanceUid }) => {
+ if (includedUids.has(studyInstanceUid)) return;
+ modalities[modality] = modalities[modality] + 1 || 1;
+ includedUids.add(studyInstanceUid);
+ });
+ }
+
const result = [];
_.each(modalities, (count, modality) => {
result.push(`${count} ${modality}`);
diff --git a/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepointStudy.html b/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepointStudy.html
index 56dfa4575..68f22b7b5 100644
--- a/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepointStudy.html
+++ b/Packages/ohif-viewerbase/client/components/viewer/studyTimepointBrowser/studyTimepointStudy.html
@@ -1,23 +1,19 @@
{{#let isSidebar=(isUndefined viewportIndex)}}
-