chore(reorganization) Moving longitudinal feature to Lesiontracker package (#208)
This commit is contained in:
1 parent
1643e9f7d6
commit
9f73ae0153
11 files changed
+2
-1
No files matched your search
@@ -8,3 +8,5 @@ import './trialOptionsModal/recistDescription.html';
|
||||
|
||||
import './trialOptionsModal/trialOptionsModal.html';
|
||||
import './trialOptionsModal/trialOptionsModal.js';
|
||||
|
||||
import './longitudinal';
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
/**
|
||||
* Loads multiple unassociated studies in the Viewer
|
||||
*/
|
||||
const getAssociationAssessment = () => {
|
||||
// default result value
|
||||
const assessment = {
|
||||
selected: 0,
|
||||
associated: 0
|
||||
};
|
||||
|
||||
// check if timepointApi is available
|
||||
const timepointApi = OHIF.studylist.timepointApi;
|
||||
if (timepointApi) {
|
||||
// Get a Cursor pointing to the selected Studies from the StudyList
|
||||
const selectedStudies = OHIF.studylist.getSelectedStudies();
|
||||
if (selectedStudies.length > 0) {
|
||||
assessment.selected = selectedStudies.length;
|
||||
// Loop through the selected Studies and return true if at least one study has no association.
|
||||
for (let i = selectedStudies.length - 1; i >= 0; --i) {
|
||||
let study = selectedStudies[i],
|
||||
timepoints = timepointApi.study(study.studyInstanceUid);
|
||||
if (timepoints && timepoints.length > 0) {
|
||||
assessment.associated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return assessment;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes all present study / timepoint associations from the Clinical Trial
|
||||
*/
|
||||
const removeTimepointAssociations = event => {
|
||||
const dialogSettings = {
|
||||
title: 'Remove Association',
|
||||
message: 'Measurements related to this Study and Timepoint will be erased. Do you really want to delete this association?',
|
||||
confirmClass: 'btn-danger',
|
||||
position: {
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
}
|
||||
};
|
||||
|
||||
OHIF.ui.showDialog('dialogConfirm', dialogSettings).then(() => {
|
||||
// Get a Cursor pointing to the selected Studies from the StudyList
|
||||
const selectedStudies = OHIF.studylist.getSelectedStudies();
|
||||
|
||||
// Find the Timepoint that was previously referenced
|
||||
const timepointApi = OHIF.studylist.timepointApi;
|
||||
if (!timepointApi) {
|
||||
OHIF.log.error('Remove Study/Timepoint Association: No Timepoint API found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop through the Cursor of Selected Studies
|
||||
selectedStudies.forEach(study => {
|
||||
const studyInstanceUid = study.studyInstanceUid;
|
||||
const timepoints = timepointApi.study(studyInstanceUid);
|
||||
const timepointIds = timepoints.map(t => t.timepointId);
|
||||
timepointApi.disassociateStudy(timepointIds, studyInstanceUid);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Meteor.startup(() => {
|
||||
if (!OHIF.studylist) return;
|
||||
|
||||
OHIF.studylist.dropdown.setItems([{
|
||||
action: OHIF.studylist.viewStudies,
|
||||
text: 'View',
|
||||
separatorAfter: true
|
||||
}, {
|
||||
action: () => OHIF.ui.showDialog('dialogStudyAssociation'),
|
||||
text: 'Associate',
|
||||
disabled: () => {
|
||||
const assessment = getAssociationAssessment();
|
||||
return assessment.selected < 1;
|
||||
}
|
||||
}, {
|
||||
action: removeTimepointAssociations,
|
||||
text: 'Remove Association',
|
||||
separatorAfter: true,
|
||||
disabled: () => {
|
||||
const assessment = getAssociationAssessment();
|
||||
return assessment.selected < 1 || assessment.selected !== assessment.associated;
|
||||
}
|
||||
}, {
|
||||
action: OHIF.studylist.viewSeriesDetails,
|
||||
text: 'View Series Details'
|
||||
}, {
|
||||
text: 'Anonymize',
|
||||
disabled: true
|
||||
}, {
|
||||
text: 'Send',
|
||||
disabled: true,
|
||||
separatorAfter: true
|
||||
}, {
|
||||
action: OHIF.studylist.exportSelectedStudies,
|
||||
text: 'Export',
|
||||
title: 'Export Selected Studies'
|
||||
}, {
|
||||
text: 'Delete',
|
||||
disabled: true
|
||||
}]);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
// Longitudinal Components imports
|
||||
import './longitudinalStudyListStudy/longitudinalStudyListStudy.html';
|
||||
import './longitudinalStudyListStudy/longitudinalStudyListStudy.styl';
|
||||
import './longitudinalStudyListStudy/longitudinalStudyListStudy.js';
|
||||
|
||||
import './longitudinalViewportOverlay/imageViewportIcons.html';
|
||||
import './longitudinalViewportOverlay/longitudinalViewportOverlay.html';
|
||||
import './longitudinalViewportOverlay/longitudinalViewportOverlay.js';
|
||||
import './longitudinalViewportOverlay/longitudinalViewportOverlay.styl';
|
||||
|
||||
import './dropdown.js';
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<template name="longitudinalStudyListStudy">
|
||||
<tr class="studylistStudy noselect {{#if this.selected}}active{{/if}}">
|
||||
<td>
|
||||
{{#if reviewerTip}}
|
||||
<i class="fa fa-exclamation-circle reviewerTip" data-toggle="tooltip" data-placement="bottom" title="{{reviewerTip}}"></i>
|
||||
{{/if}}
|
||||
{{formatPN patientName}}
|
||||
</td>
|
||||
<td>
|
||||
{{patientId}}
|
||||
</td>
|
||||
<td>
|
||||
{{accessionNumber}}
|
||||
</td>
|
||||
<td>
|
||||
{{formatDA studyDate}} {{ #if timepointName }}({{timepointName}}){{ /if }}
|
||||
</td>
|
||||
<td>
|
||||
{{modalities}}
|
||||
</td>
|
||||
<td>
|
||||
{{studyDescription}}
|
||||
</td>
|
||||
<td>
|
||||
{{numberOfStudyRelatedInstances}}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
// Use Aldeed's meteor-template-extension package to replace the
|
||||
// default StudyListStudy template.
|
||||
// See https://github.com/aldeed/meteor-template-extension
|
||||
const defaultTemplate = 'studylistStudy';
|
||||
|
||||
if (OHIF.studylist) {
|
||||
Template.longitudinalStudyListStudy.replaces(defaultTemplate);
|
||||
|
||||
// Add the TimepointName helper to the default template. The
|
||||
// HTML of this template is replaced with that of longitudinalStudyListStudy
|
||||
Template[defaultTemplate].helpers({
|
||||
timepointName() {
|
||||
const instance = Template.instance();
|
||||
const timepointApi = OHIF.studylist.timepointApi;
|
||||
if (!timepointApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timepoint = timepointApi.study(instance.data.studyInstanceUid)[0];
|
||||
if (!timepoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
return timepointApi.name(timepoint);
|
||||
},
|
||||
|
||||
reviewerTip() {
|
||||
const instance = Template.instance();
|
||||
const timepointApi = OHIF.studylist.timepointApi;
|
||||
if (!timepointApi || !window.Reviewers) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timepoint = timepointApi.study(instance.data.studyInstanceUid);
|
||||
if (!timepoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timepointReviewers = window.Reviewers.findOne({ timepointId: timepoint.timepointId });
|
||||
if (!timepointReviewers) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getReviewerTipText(timepointReviewers.reviewers);
|
||||
}
|
||||
});
|
||||
|
||||
function getReviewerTipText(reviewers) {
|
||||
if (!reviewers || !reviewers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newReviewers = reviewers.filter(function(reviewer) {
|
||||
return reviewer.userId !== Meteor.userId();
|
||||
});
|
||||
|
||||
if (!newReviewers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tipText = 'The study is being reviewed by ';
|
||||
newReviewers.forEach(function(reviewer, index) {
|
||||
if (reviewer.userId === Meteor.userId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
tipText += ',';
|
||||
}
|
||||
|
||||
tipText += reviewer.userName;
|
||||
});
|
||||
|
||||
return tipText;
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
.studylistStudy
|
||||
cursor: pointer
|
||||
|
||||
.reviewerTip
|
||||
color: darkred
|
||||
+1
@@ -0,0 +1 @@
|
||||
<template name="imageViewportIcons"></template>
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
<template name="longitudinalViewportOverlay">
|
||||
<div class="imageViewerViewportOverlay noselect {{#if gt numImages 1}}controlsVisible{{/if}}">
|
||||
{{#unless tagDisplaySpecified}}
|
||||
<div class="topleft dicomTag">
|
||||
<div>{{patientId}}</div>
|
||||
<div>{{patientName}}</div>
|
||||
<div>{{getGenderAndAge}}</div>
|
||||
<div class="icons-section">
|
||||
{{#if linked}}
|
||||
<svg class="icon-link">
|
||||
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-viewport-link"}}></use>
|
||||
</svg>
|
||||
{{/if}}
|
||||
{{>imageViewportIcons this}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="topright dicomTag">
|
||||
<div>{{seriesDescription}}</div>
|
||||
<div>{{studyInfo 'AccessionNumber'}}</div>
|
||||
<div>{{formatDA (studyInfo 'AcquisitionDate')}} {{formatTM (studyInfo 'AcquisitionTime')}}</div>
|
||||
<div>{{#if seriesNumber}}S: {{seriesNumber}}{{#if gt numImages 1}},{{/if}} {{/if}}{{#if gt numImages 1}}I: {{imageIndex}}/{{numImages}}{{/if}}</div>
|
||||
<div class='timepointName'>{{timepointName}}</div>
|
||||
</div>
|
||||
<div class="bottomright dicomTag">
|
||||
<div class="stationName">{{studyInfo 'StationName'}}</div>
|
||||
<div class="manufacturerModel">{{studyInfo 'ManufacturersModelName'}}</div>
|
||||
<div class="institutionName">{{studyInfo 'InstitutionName'}}</div>
|
||||
</div>
|
||||
<div class="bottomleft dicomTag">
|
||||
<div>
|
||||
<span>{{#if location}}Loc: {{formatNumberPrecision location 2}} mm{{/if}}</span>
|
||||
<span>{{#if thickness}}Thick: {{formatNumberPrecision thickness 2}} mm{{/if}}</span>
|
||||
<span>{{#if spacingBetweenSlices}}Spacing: {{formatNumberPrecision spacingBetweenSlices 2}} mm{{/if}}</span>
|
||||
</div>
|
||||
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 2}}x{{/if}}</div>
|
||||
<div>{{wwwc}}</div>
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#if tagDisplayRightOnly}}
|
||||
<div class="topright dicomTag">
|
||||
<div>{{formatPN patientName}}</div>
|
||||
<div>{{patientId}}</div>
|
||||
<div class='timepointName'>{{timepointName}}</div>
|
||||
<div>{{studyDescription}}</div>
|
||||
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
|
||||
</div>
|
||||
<div class="bottomright dicomTag">
|
||||
<div>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
|
||||
<div>{{#if gt numImages 1}}Img: {{instanceNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
|
||||
<div>{{#if frameRate}}{{formatNumberPrecision frameRate 2}} FPS{{/if}}</div>
|
||||
<div>{{imageDimensions}}</div>
|
||||
<div>{{seriesDescription}}</div>
|
||||
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 0}}%{{/if}}</div>
|
||||
<div class="compressionIndicator">{{compression}}</div>
|
||||
<div>{{wwwc}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if tagDisplayLeftOnly}}
|
||||
<div class="topleft dicomTag">
|
||||
<div>{{formatPN patientName}}</div>
|
||||
<div>{{patientId}}</div>
|
||||
<div class='timepointName'>{{timepointName}}</div>
|
||||
<div>{{studyDescription}}</div>
|
||||
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
|
||||
</div>
|
||||
<div class="bottomleft dicomTag">
|
||||
<div>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
|
||||
<div>{{#if gt numImages 1}}Img: {{instanceNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
|
||||
<div>{{#if frameRate}}{{formatNumberPrecision frameRate 2}} FPS{{/if}}</div>
|
||||
<div>{{imageDimensions}}</div>
|
||||
<div>{{seriesDescription}}</div>
|
||||
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 0}}%{{/if}}</div>
|
||||
<div class="compressionIndicator">{{compression}}</div>
|
||||
<div>{{wwwc}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if gt numImages 1}}
|
||||
{{>imageControls imageIndex=imageIndex numImages=numImages}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Session } from 'meteor/session';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { moment } from 'meteor/momentjs:moment';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { cornerstone } from 'meteor/ohif:cornerstone';
|
||||
|
||||
// Use Aldeed's meteor-template-extension package to replace the
|
||||
// default viewportOverlay template.
|
||||
// See https://github.com/aldeed/meteor-template-extension
|
||||
const defaultTemplate = 'viewportOverlay';
|
||||
Template.longitudinalViewportOverlay.replaces(defaultTemplate);
|
||||
|
||||
Template[defaultTemplate].onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
instance.instanceMetadata = new ReactiveVar();
|
||||
|
||||
const { DICOMTagDescriptions } = OHIF.viewerbase;
|
||||
instance.getValueByTagKeyword = tagKeyword => {
|
||||
const instanceMetadata = instance.instanceMetadata.get();
|
||||
const tagObject = DICOMTagDescriptions.find(tagKeyword);
|
||||
if (!instanceMetadata || !tagObject) return;
|
||||
return instanceMetadata.getRawValue(tagObject.tag);
|
||||
};
|
||||
});
|
||||
|
||||
Template[defaultTemplate].onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
const { studyInstanceUid, seriesInstanceUid } = instance.data;
|
||||
|
||||
Tracker.autorun(computation => {
|
||||
Session.get('CornerstoneNewImage' + instance.data.viewportIndex);
|
||||
if (computation.firstRun) return;
|
||||
computation.stop();
|
||||
const imageIndex = instance.getImageIndex();
|
||||
OHIF.studies.loadStudy(studyInstanceUid).then(studyMetadata => {
|
||||
const seriesMetadata = studyMetadata.getSeriesByUID(seriesInstanceUid);
|
||||
const instanceMetadata = seriesMetadata.getInstanceByIndex(imageIndex);
|
||||
if (!instanceMetadata) return;
|
||||
instance.instanceMetadata.set(instanceMetadata);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Add the TimepointName helper to the default template. The
|
||||
// HTML of this template is replaced with that of longitudinalViewportOverlay
|
||||
Template[defaultTemplate].helpers({
|
||||
studyInfo(tagKeyword) {
|
||||
const instance = Template.instance();
|
||||
instance.instanceMetadata.dep.depend();
|
||||
return instance.getValueByTagKeyword(tagKeyword);
|
||||
},
|
||||
|
||||
getGenderAndAge() {
|
||||
const instance = Template.instance();
|
||||
const values = [];
|
||||
values.push(instance.getValueByTagKeyword('PatientSex'));
|
||||
|
||||
const patientAge = instance.getValueByTagKeyword('PatientAge');
|
||||
const patientBirthDate = instance.getValueByTagKeyword('PatientBirthDate');
|
||||
if (patientAge) {
|
||||
values.push(patientAge);
|
||||
} else if (patientBirthDate) {
|
||||
const date = moment(patientBirthDate, 'YYYYMMDD');
|
||||
const yearDiff = moment().diff(date, 'years');
|
||||
if (yearDiff) {
|
||||
values.push((yearDiff + 'Y').padStart(4, '0'));
|
||||
} else {
|
||||
const monthDiff = moment().diff(date, 'months');
|
||||
if (monthDiff) {
|
||||
values.push((monthDiff + 'M').padStart(4, '0'));
|
||||
} else {
|
||||
const dayDiff = moment().diff(date, 'days') || 0;
|
||||
values.push((dayDiff + 'D').padStart(4, '0'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values.filter(value => !!value).join(', ');
|
||||
},
|
||||
|
||||
thickness() {
|
||||
const instance = Template.instance();
|
||||
Session.get('CornerstoneNewImage' + instance.data.viewportIndex);
|
||||
|
||||
return instance.getValueByTagKeyword('SliceThickness');
|
||||
},
|
||||
|
||||
location() {
|
||||
const instance = Template.instance();
|
||||
Session.get('CornerstoneNewImage' + instance.data.viewportIndex);
|
||||
|
||||
const sliceLocation = instance.getValueByTagKeyword('SliceLocation');
|
||||
const tablePosition = instance.getValueByTagKeyword('TablePosition');
|
||||
const imagePositionPatient = instance.getValueByTagKeyword('ImagePositionPatient');
|
||||
return sliceLocation || tablePosition || imagePositionPatient;
|
||||
},
|
||||
|
||||
spacingBetweenSlices() {
|
||||
const instance = Template.instance();
|
||||
Session.get('CornerstoneNewImage' + instance.data.viewportIndex);
|
||||
|
||||
// TODO: Otherwise, displays a value derived from successive values
|
||||
// of Image Position (Patient) (0020,0032) perpendicular to
|
||||
// the Image Orientation (Patient) (0020,0037)
|
||||
|
||||
return instance.getValueByTagKeyword('SpacingBetweenSlices');
|
||||
},
|
||||
|
||||
zoom() {
|
||||
const instance = Template.instance();
|
||||
const { viewportIndex } = instance.data;
|
||||
const { getElementIfNotEmpty } = OHIF.viewerbase;
|
||||
Session.get('CornerstoneImageRendered' + viewportIndex);
|
||||
|
||||
const element = getElementIfNotEmpty(viewportIndex);
|
||||
if (!element) return;
|
||||
|
||||
const viewport = cornerstone.getViewport(element);
|
||||
if (!viewport) return;
|
||||
|
||||
return viewport.scale;
|
||||
},
|
||||
|
||||
wwwc() {
|
||||
const instance = Template.instance();
|
||||
const { viewportIndex } = instance.data;
|
||||
const { getElementIfNotEmpty, wlPresets } = OHIF.viewerbase;
|
||||
Session.get('CornerstoneImageRendered' + viewportIndex);
|
||||
wlPresets.changeObserver.depend();
|
||||
|
||||
const element = getElementIfNotEmpty(viewportIndex);
|
||||
if (!element) return;
|
||||
|
||||
const viewport = cornerstone.getViewport(element);
|
||||
if (!viewport) return;
|
||||
|
||||
const ww = viewport.voi.windowWidth.toFixed(0);
|
||||
const wc = viewport.voi.windowCenter.toFixed(0);
|
||||
const result = [`W: ${ww}, L: ${wc}`];
|
||||
|
||||
// Check if there's a preset with this W/L
|
||||
const preset = _.findWhere(OHIF.viewer.wlPresets, {
|
||||
ww: parseInt(ww),
|
||||
wc: parseInt(wc)
|
||||
});
|
||||
|
||||
// Append the preset name to the result if found
|
||||
if (preset) {
|
||||
result .push(`(${preset.id})`);
|
||||
}
|
||||
|
||||
return result.join(' ');
|
||||
},
|
||||
|
||||
timepointName() {
|
||||
const instance = Template.instance();
|
||||
const studyInstanceUid = instance.data.studyInstanceUid;
|
||||
|
||||
const timepointApi = OHIF.viewer.timepointApi;
|
||||
if (!timepointApi) return;
|
||||
|
||||
const timepoints = timepointApi.study(studyInstanceUid);
|
||||
if (!timepoints || !timepoints.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timepoint = timepoints[0];
|
||||
|
||||
return timepointApi.name(timepoint);
|
||||
},
|
||||
|
||||
linked() {
|
||||
const linkedViewports = Session.get('StackImagePositionOffsetSynchronizerLinkedViewports') || [];
|
||||
return (linkedViewports.indexOf(this.viewportIndex) !== -1);
|
||||
}
|
||||
});
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
@require '{ohif:design}/app'
|
||||
|
||||
.imageViewerViewportOverlay .icons-section .icon-link
|
||||
transform(translateY(3px))
|
||||
Reference in new issue
Block a user