reorganize packages, start converting overall app to react (very broken at the moment)

This commit is contained in:
Erik Ziegler committed 2018-12-12 18:06:25 +01:00
1 parent 024fe1a448
commit 5f30f7bd35
479 files changed
+5364 -11611

No files matched your search

@@ -1,4 +1,4 @@
@import "{ohif:design}/app"
@import "{ohif:viewerbase}/app"
#aboutModal
.logo
@@ -0,0 +1,11 @@
import './item.html';
import './item.js';
import './list.html';
import './list.js';
import './list.styl';
import './quickSwitch.html';
import './quickSwitch.js';
import './series.html';
import './series.js';
import './sidebar.html';
import './sidebar.js';
@@ -0,0 +1,29 @@
<template name="studyBrowserItem">
<div class="study-browser-item {{this.studyItemClass}} {{#if isActive}}active{{/if}} {{#if isLoading}}loading{{/if}}" data-uid="{{this.studyInformation.studyInstanceUid}}">
<div class="study-item">
{{#if isLoading}}
{{>loadingText}}
{{/if}}
{{#let studyData=instance.studyData.get}}
<div class="study-item-box">
<div class="study-modality">
<div class="study-modality-text" style="{{modalityStyle studyData.modalities}}">{{studyData.modalities}}</div>
</div>
<div class="study-text">
{{#if (or isLoaded hasDescriptionAndDate)}}
<div class="study-date">{{formatDA studyData.studyDate 'D-MMM-YYYY'}}</div>
<div class="study-description">{{studyData.studyDescription}}</div>
{{else}}
<div class="study-availability">{{#if eq studyAvailable false}}N/A{{else}}Click to load{{/if}}</div>
{{/if}}
</div>
</div>
{{/let}}
</div>
{{#if this.studyChildTemplate}}
{{#if and (not isLoading) instance.studyData.get instance.loaded}}
{{>Template.dynamic template=this.studyChildTemplate data=(clone this)}}
{{/if}}
{{/if}}
</div>
</template>
@@ -0,0 +1,140 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import $ from 'jquery';
import _ from 'underscore';
import { OHIF } from 'meteor/ohif:core';
Template.studyBrowserItem.onCreated(() => {
const instance = Template.instance();
const { studyInformation } = instance.data;
const { studyInstanceUid } = studyInformation;
instance.loaded = new ReactiveVar(false);
instance.loading = new ReactiveVar(false);
instance.studyData = new ReactiveVar(studyInformation);
// Try to load the study data from an external source if available
if (OHIF.studies.getStudyBoxData) {
OHIF.studies.getStudyBoxData(studyInformation).then(studyData => {
if (!instance.loaded.get()) {
instance.studyData.set(studyData);
}
});
}
instance.studyMetadata = null;
instance.getStudyMetadata = () => {
instance.loading.dep.depend();
if (!instance.studyMetadata) {
let study = OHIF.viewer.Studies.findBy({ studyInstanceUid }) || null;
if (study && !(study instanceof OHIF.viewerbase.metadata.StudyMetadata)) {
study = new OHIF.metadata.StudyMetadata(study, study.studyInstanceUid);
}
instance.studyMetadata = study;
}
return instance.studyMetadata;
};
instance.autorun(() => {
const instance = Template.instance();
OHIF.studies.loadingDict.get(studyInstanceUid);
const studyMetadata = instance.getStudyMetadata();
if (studyMetadata) {
const firstInstance = studyMetadata.getFirstInstance();
instance.studyData.set({
studyDate: firstInstance.getRawValue('x00080020') || '',
studyDescription: firstInstance.getRawValue('x00081030') || '',
modalities: firstInstance.getRawValue('x00080060') || '',
});
instance.loaded.set(true);
instance.loading.set(false);
}
});
});
Template.studyBrowserItem.events({
'click .study-item'(event, instance) {
if (instance.loading.get()) return;
const { studyInformation } = instance.data;
// Skip if study is not available (set to false explicitly)
if (studyInformation.available === false) {
return;
}
const element = event.currentTarget.parentElement;
const $element = $(element);
const triggerClick = () => {
const cloneEvent = _.clone(event);
delete cloneEvent.type;
cloneEvent.currentTarget = cloneEvent.target = element;
const newEvent = $.Event('ohif.studies.study.click', cloneEvent);
$element.trigger(newEvent, studyInformation);
};
if (instance.loaded.get()) {
triggerClick();
} else {
instance.loading.set(true);
OHIF.studies.loadStudy(studyInformation.studyInstanceUid).then(() => {
instance.loaded.set(true);
instance.loading.set(false);
$element.trigger('ohif.studies.study.load', studyInformation);
Tracker.afterFlush(triggerClick);
});
}
}
});
Template.studyBrowserItem.helpers({
isLoaded() {
return Template.instance().loaded.get();
},
hasDescriptionAndDate() {
const studyData = Template.instance().studyData.get();
return studyData.studyDescription && studyData.studyDate;
},
isLoading() {
return Template.instance().loading.get();
},
modalitiesText(modalities) {
const text = modalities || 'UN';
return _.uniq(text.split(/[^A-Za-z]+/g)).join(' ');
},
modalityStyle(modalities) {
// Responsively styles the Modality Acronyms for studies with more than one modality
const numModalities = modalities ? modalities.split(/\s/g).length : 1;
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 modalities, let them sit side-by-side
return 'font-size:0.7em';
} else {
// If we have more than two modalities, change the line height to display multiple rows
return 'font-size:0.7em;line-height:1.4em;padding-top:0.5em';
}
},
studyAvailable() {
const { studyInformation } = Template.instance().data;
if (!studyInformation) {
return;
}
return studyInformation.available;
}
});
@@ -0,0 +1,7 @@
<template name="studyBrowserList">
<div class="study-browser-list {{this.class}}">
{{#each studyInformation in getStudiesInformation}}
{{>studyBrowserItem (clone this studyInformation=studyInformation)}}
{{/each}}
</div>
</template>
@@ -0,0 +1,26 @@
import { Template } from 'meteor/templating';
Template.studyBrowserList.onCreated(() => {
const instance = Template.instance();
if (instance.data.studiesData) {
instance.studiesData = instance.data.studiesData;
}
});
Template.studyBrowserList.helpers({
getStudiesInformation() {
const instance = Template.instance();
let studiesInformation;
if (instance.studiesData) {
studiesInformation = instance.studiesData.get() || [];
} else {
studiesInformation = instance.data.studiesInformation || [];
}
const result = new Map();
studiesInformation.forEach(item => result.set(item.studyInstanceUid, item));
return Array.from(result.values());
}
});
@@ -0,0 +1,149 @@
@require '{ohif:viewerbase}/app'
$boxBorderColor = transparent
$boxHoverBackgroundColor = #14191E
$boxHoverBorderColor = #2d4660
$boxActiveBorderColor = #131D29
$boxWidth = 50px
$borderThickness = 2px
$boxBorderRadius = 8px
$nestingMargin = 6px
$spacerX = 7px
$spacerY = 12px
.study-browser-item
position: relative
// required transformation to make inner fixed elements relative to this one
transform(scale(1))
.loadingTextDiv
theme('color', '$textSecondaryColor')
background-color: $boxHoverBackgroundColor
border-radius: 12px
font-size: 32px
opacity: 0
margin: 0
padding: 25px
position: absolute
pointer-events: none
z-index: 100
&.loading
.loadingTextDiv
height: 100%
left: 0
opacity: 0.75
top: 0
width: 100%
.study-item-box
opacity: 0.2
&.active
.study-item-box
.study-modality
theme('color', '$primaryBackgroundColor')
&
&:before
&:after
theme('background-color', '$activeColor')
border-color: $boxActiveBorderColor
&:not(.active) .study-browser-series
max-height: 0 !important
.study-series-container
opacity: 0
transform(translateY(-100%))
.study-browser-series
overflow: hidden
transition(max-height 0.3s ease)
.study-series-container
opacity: 1
transition(opacity 0.3s ease\, transform 0.3s ease)
transform(translateY(0))
transform-origin(50% 0%)
.study-item-box
border: 1px solid $boxBorderColor
border-radius: 12px
cursor: pointer
padding: $spacerY $spacerX ($spacerY - 1)
position: relative
transition($sidebarTransition)
z-index: 0
&:hover
background-color: $boxHoverBackgroundColor
border-color: $boxHoverBorderColor
&.additional
theme('color', '$textSecondaryColor')
&.additional
theme('color', '$textPrimaryColor')
font-size: 16px
font-weight: normal
height: 91px
line-height: 91px
padding: 0
text-align: center
.study-text
font-size: 13px
left: ($spacerX * 3) + $boxWidth + ($nestingMargin * 3)
line-height: 14px
position: absolute
right: $spacerX
top: $spacerY
.study-date
margin-top: 8px
theme('color', '$textSecondaryColor')
.study-description
margin-top: 8px
theme('color', '$textPrimaryColor')
.study-availability
margin-top: 24px
theme('color', '$textPrimaryColor')
.study-modality
theme('color', '$textSecondaryColor')
font-size: 20px
line-height: $boxWidth
margin-left: $nestingMargin * 2
margin-top: $nestingMargin * 2
position: relative
.study-modality-text
height: 100%
overflow: hidden
text-align: center
text-transform: uppercase
&
&:before
&:after
theme('border', '%s solid $primaryBackgroundColor' % $borderThickness)
theme('background-color', '$boxBackgroundColor')
border-radius: $boxBorderRadius
height: $boxWidth + ($borderThickness * 2)
transition($sidebarTransition)
width: $boxWidth + ($borderThickness * 2)
&:before
&:after
display: block
content: ''
position: absolute
&:before
top: -($borderThickness + $nestingMargin)
left: -($borderThickness + $nestingMargin)
z-index: -1
&:after
top: -($borderThickness + ($nestingMargin * 2))
left: -($borderThickness + ($nestingMargin * 2))
z-index: -2
@@ -0,0 +1,3 @@
<template name="studyBrowserQuickSwitch">
{{>studyBrowserList studyBrowserData}}
</template>
@@ -0,0 +1,43 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { OHIF } from 'meteor/ohif:core';
Template.studyBrowserQuickSwitch.onCreated(() => {
const instance = Template.instance();
instance.studiesInformation = new ReactiveVar();
const filter = { studyInstanceUid: OHIF.viewer.data.studyInstanceUids };
OHIF.studies.searchStudies(filter).then(studies => instance.studiesInformation.set(studies));
});
Template.studyBrowserQuickSwitch.onRendered(() => {
const instance = Template.instance();
instance.autorun(() => {
instance.studiesInformation.dep.depend();
const currentStudy = instance.data.currentStudy.get();
const studyInstanceUid = (currentStudy && currentStudy.studyInstanceUid) || '';
Tracker.afterFlush(() => {
const $studyBrowserItems = instance.$('.study-browser-item');
$studyBrowserItems.removeClass('active');
$studyBrowserItems.filter(`[data-uid="${studyInstanceUid}"]`).addClass('active');
});
});
});
Template.studyBrowserQuickSwitch.events({
'ohif.studies.study.click'(event, instance, studyInformation) {
const { studyInstanceUid } = studyInformation;
const study = OHIF.viewer.Studies.findBy({ studyInstanceUid });
instance.data.currentStudy.set(study);
const $studySwitch = $(event.currentTarget).closest('.study-switch');
$studySwitch.siblings('.series-switch').trigger('rescale');
}
});
Template.studyBrowserQuickSwitch.helpers({
studyBrowserData() {
const instance = Template.instance();
const studiesInformation = instance.studiesInformation.get();
return { studiesInformation };
}
});
@@ -0,0 +1,9 @@
<template name="studyBrowserSeries">
<div class="study-browser-series">
<div class="study-series-container">
{{#each thumbnail in instance.thumbnails.get}}
{{>thumbnailEntry (clone this thumbnail=thumbnail)}}
{{/each}}
</div>
</div>
</template>
@@ -0,0 +1,65 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { OHIF } from 'meteor/ohif:core';
Template.studyBrowserSeries.onCreated(() => {
const instance = Template.instance();
const { studyInformation, studyMetadata } = instance.data;
const studyInstanceUid = (studyMetadata && studyMetadata.studyInstanceUid) || (studyInformation && studyInformation.studyInstanceUid);
instance.thumbnails = new ReactiveVar([]);
// Get the study metadata and update the study thumbnails
instance.autorun(() => {
let metadata = studyMetadata;
// Check for reactivity
if (metadata instanceof ReactiveVar) {
metadata = metadata.get();
}
// Retrieve the study metadata
if (!metadata) {
metadata = OHIF.viewer.Studies.findBy({ studyInstanceUid });
}
// Stop here if there's no study metadata
if (!metadata) return;
// Get the study display sets
let displaySets = metadata.displaySets;
if (!displaySets.length) {
displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(metadata);
metadata.displaySets = displaySets;
metadata.setDisplaySets(displaySets);
metadata.forEachDisplaySet(displaySet => {
OHIF.viewerbase.stackManager.makeAndAddStack(metadata, displaySet);
});
}
// Defines the resulting thumbnails list
const thumbnails = [];
displaySets.forEach((stack, thumbnailIndex) => {
thumbnails.push({
thumbnailIndex,
stack
});
});
instance.thumbnails.set(thumbnails);
});
});
Template.studyBrowserSeries.onRendered(() => {
const instance = Template.instance();
// Run this computation every time the thumbnails are changed
instance.autorun(() => {
instance.thumbnails.get();
Tracker.afterFlush(() => {
instance.$('.study-browser-series').adjustMax('height');
});
});
});
@@ -0,0 +1,5 @@
<template name="studyBrowserSidebar">
{{#scrollArea class='fit noselect' scrollableClass='p-a-1' scrollStep=191}}
{{>studyBrowserList (clone this studyChildTemplate='studyBrowserSeries')}}
{{/scrollArea}}
</template>
@@ -0,0 +1,13 @@
import { Template } from 'meteor/templating';
Template.studyBrowserSidebar.events({
'ohif.studies.study.click'(event, instance) {
const $element = $(event.currentTarget);
// Remove active class from sibling studies
$element.siblings().removeClass('active');
// Toggle the active class on clicked study
$element.toggleClass('active');
}
});
@@ -0,0 +1,10 @@
<template name="dialogStudyAssociation">
{{#dialogForm (extend this
schema=instance.schema
dialogClass='modal-lg'
title='Study Association'
confirmLabel='Save'
)}}
{{>studyAssociationTable}}
{{/dialogForm}}
</template>
@@ -0,0 +1,147 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { Blaze } from 'meteor/blaze';
import moment from 'moment';
import _ from 'underscore';
import { OHIF } from 'meteor/ohif:core';
Template.dialogStudyAssociation.onCreated(() => {
const instance = Template.instance();
instance.data.confirmCallback = formData => {
OHIF.log.info('Saving associations');
const Timepoints = OHIF.studylist.timepointApi.timepoints;
// Find the rows of the study association table
const $tableRows = instance.$('#studyAssociationTable table tbody tr');
// Create an empty object to group studies into
const studies = {};
// Loop through each row to parse the data
$tableRows.each(function() {
// Get a selector for this row
const $row = $(this);
// Check the includeStudy checkbox to see if we should parse this row
const includeStudy = $row.find('input.includeStudy[type="checkbox"]').eq(0).prop('checked');
if (!includeStudy) {
return;
}
// Find the selected timepoint option for this study
const $timepointInput = $row.find('input.timepointOption[type="radio"]:checked');
// Find the related label and trim it down to actual label (TODO: do this another way)
const timepointType = $timepointInput.val();
// Get the study metaData by checking the row with the template engine Blaze
const data = Blaze.getData(this);
// Concatenate the study data to an array, depending on whether is was marked as baseline
// or follow-up
if (!studies.hasOwnProperty(timepointType)) {
studies[timepointType] = [];
}
studies[timepointType].push(data);
});
const studiesKeys = Object.keys(studies);
// TODO: REMOVE - Temporary for RSNA
const hasBaseline = _.contains(studiesKeys, 'baseline');
if (hasBaseline) {
const patientId = studies[studiesKeys[0]][0].patientId;
Timepoints.remove({ patientId });
}
studiesKeys.forEach(timepointType => {
// Get the studies associated with this timepoint
const relatedStudies = studies[timepointType];
// Create an array of all the studyInstanceUids for storage in the Timepoint
const studyInstanceUids = relatedStudies.map(function(study) {
return study.studyInstanceUid;
});
// Create an array of all the studyDates for storage in the Timepoint
let studyDates = relatedStudies.map(study => moment(study.studyDate).toDate());
// Sort the study dates, so we can get a range for these values
studyDates = studyDates.sort();
// HipaaEventType to log changes in collections
let hipaaEventType;
let hipaaEvent;
// Check if these studies are already associated with an existing Timepoint
let existingTimepoint;
if (timepointType === 'baseline') {
// If we're trying to associate them to the Baseline, we don't need to
// check if the studyInstanceUids are already associated with anything else
existingTimepoint = Timepoints.findOne({
patientId: relatedStudies[0].patientId,
timepointType: 'baseline'
});
} else {
// If we're trying to associate them to a Follow-up, we should check if any
// of them are already part of a Follow-up (e.g. Follow-up 1), so that
// the rest will also be associated with Follow-up 1.
existingTimepoint = Timepoints.findOne({
patientId: relatedStudies[0].patientId,
studyInstanceUids: {
$in: studyInstanceUids
}
});
}
let timepointId;
if (existingTimepoint) {
// If these studies are already associated with an existing Timepoint,
// and the desired timepoint type is the same (e.g. Follow-up), update
// this Timepoint instead of creating a new one
Timepoints.update(existingTimepoint._id, {
$set: {
studyInstanceUids
}
});
timepointId = existingTimepoint.timepointId;
hipaaEventType = 'modify';
} else {
// Create a new timepoint to represent the (baseline or follow-up) studies
let timepoint = {
timepointType: timepointType,
timepointId: OHIF.utils.guid(),
studyInstanceUids: studyInstanceUids,
patientId: relatedStudies[0].patientId,
earliestDate: studyDates[0],
latestDate: studyDates[studyDates.length - 1],
isLocked: false
};
// Insert this timepoint into the Timepoints Collection
Timepoints.insert(timepoint);
timepointId = timepoint.timepointId;
hipaaEventType = 'create';
}
// Log
hipaaEvent = {
eventType: hipaaEventType,
userId: OHIF.user.getUserId(),
userName: OHIF.user.getName(),
collectionName: 'Timepoints',
recordId: timepointId,
patientId: relatedStudies[0].patientId,
patientName: relatedStudies[0].patientName
};
});
OHIF.studylist.timepointApi.storeTimepoints();
return formData;
};
});
@@ -0,0 +1,67 @@
<template name="studyAssociationTable">
<div id="studyAssociationTable">
<div id="associationInstructions" class="row">
<div class="col-md-12">
<h5>Instructions</h5>
<p>Select the studies you would like to associate to a time point. Only one follow-up time point can be associated at the same time.</p>
<p>We have automatically retrieved all studies within 14 days ({{formatDA this.earliestDate}} to {{formatDA latestDate}}) of your selected studies, in case you forgot to select a study.</p>
</div>
</div>
<div class="tableHeaderBackground">
</div>
<table class="table table-responsive">
<thead>
<tr>
<th class="center">Include Study?</th>
<th class="center">Study Date</th>
<th class="center">Patient Name</th>
<th class="center">Study Description</th>
<th class="center">Timepoint Type</th>
</tr>
</thead>
<tbody>
{{#each relevantStudies}}
<tr class="{{#if autoselected}}disabled{{/if}}">
<td class="checkboxWrapper center">
<label>
<input type="checkbox" class="includeStudy" {{valueIf autoselected '' 'checked'}}/> <span></span>
</label>
</td>
<td class="center studyDataCell">
{{#if autoselected}}
<p class="studyDate autoselected"
data-toggle="tooltip"
title="This study was autoselected.">
{{formatDA studyDate}}
</p>
{{else}}
<p class="studyDate">{{formatDA studyDate}}</p>
{{/if}}
</td>
<td class="center studyDataCell">
<p>{{formatPN patientName}}</p>
</td>
<td class="studyDataCell">
<p>{{studyDescription}}</p>
</td>
<td class="timepointOptions center studyDataCell noselect">
{{#each timepointOptions}}
<label class="noselect">
<input type="radio"
name="{{../_id}}"
value="{{value}}"
class="timepointOption"
{{valueIf checked 'checked' ''}}
{{valueIf ../autoselected 'disabled' ''}}>
<span></span>{{name}}
</label>
{{/each}}
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
</template>
@@ -0,0 +1,142 @@
import { Template } from 'meteor/templating';
import moment from 'moment';
import { OHIF } from 'meteor/ohif:core';
/**
* Finds related studies within defined time window of =/- 14 days of selected studies
* @param selectedStudies
* @param range Object
*/
function getDateRange(selectedStudies, range) {
if (range === undefined) {
range = {
days: 14
};
}
if (!selectedStudies.length) {
return;
}
const earliestStudy = selectedStudies[0];
const latestStudy = selectedStudies[selectedStudies.length - 1];
const earliestDate = moment(earliestStudy.studyDate, 'YYYYMMDD');
earliestDate.subtract(range);
const latestDate = moment(latestStudy.studyDate, 'YYYYMMDD');
latestDate.add(range);
return {
earliestDate: earliestDate,
latestDate: latestDate
};
}
/**
* Selects all studies related to the currently input studies,
* based on various criteria. Returns the entire array of related studies.
*
* (at the moment, this is only the date range +/- 14 days, with a matching patientId)
*
* @param selectedStudies A user-selected list of studies
* @returns {*} The entire array of related studies
*/
function autoSelectStudies(selectedStudies) {
if (!selectedStudies.length) {
return;
}
const range = getDateRange(selectedStudies);
// Fetch autoselected studies based on the date range
// Note that we used MongoDB's fetch here so we have a mutable array,
// rather than a Cursor
const autoselected = OHIF.studylist.collections.Studies.find({
studyDate: {
$gte: range.earliestDate.format('YYYYMMDD'),
$lte: range.latestDate.format('YYYYMMDD')
}
}, {
sort: {
studyDate: 1
}
}).fetch();
// Make an array of studyInstanceUids in selectedStudies
const studyInstanceUids = selectedStudies.map(selectedStudy => selectedStudy.studyInstanceUid);
autoselected.forEach(study => {
const exists = studyInstanceUids.indexOf(study.studyInstanceUid);
if (exists > -1) {
study.autoselected = false;
return;
}
study.autoselected = true;
});
return autoselected;
}
Template.studyAssociationTable.helpers({
/**
* This helpers includes the user-selected and autoselected studies
* to be associated.
*
* @returns {Array.<T>}
*/
relevantStudies() {
const selectedStudies = OHIF.studylist.getSelectedStudies();
return autoSelectStudies(selectedStudies);
},
/**
* This helper returns the list of Timepoint types the user can set for this study
*
* @returns {Array.<T>}
*/
timepointOptions() {
return [{
value: 'baseline',
name: 'Baseline',
checked: true
}, {
value: 'followup',
name: 'Follow-up',
checked: false
}];
},
earliestDate() {
const selectedStudies = OHIF.studylist.getSelectedStudies();
const range = getDateRange(selectedStudies);
if (range) {
return range.earliestDate;
}
},
latestDate() {
const selectedStudies = OHIF.studylist.getSelectedStudies();
const range = getDateRange(selectedStudies);
if (range) {
return range.latestDate;
}
}
});
Template.studyAssociationTable.events({
'change input.includeStudy'(event, instance) {
const checkbox = event.currentTarget;
const studyRow = $(checkbox).closest('tr');
const studyDataCells = studyRow.find('td.studyDataCell');
if (checkbox.checked === true) {
studyRow.removeClass('disabled');
studyDataCells.find('input').attr('disabled', false);
} else {
studyRow.addClass('disabled');
studyDataCells.find('input').attr('disabled', true);
}
}
});
@@ -0,0 +1,188 @@
@import "{ohif:viewerbase}/app"
$tableHoverColor = #2c363f
$tableTextPrimaryColor = white
$tableHeaderHeight = 50px
$bodyCellHeight = 30px
#studyAssociationTable
color: #fff
.header
text-align: center
.center
text-align: center
#associationInstructions
theme('background', '$uiGrayDarker')
margin-bottom: 2px
padding: 0 15px
.tableHeaderBackground
theme('background-color', '$uiGrayDarker')
position: absolute
height: $tableHeaderHeight + 1px
left: 0
width: 100%
z-index: 1
border-bottom: solid 1px
theme('border-bottom-color', '$largeNumbersColor')
table
z-index: 2
position: relative
thead
tr
th
padding-bottom: 5px
border-bottom: solid 1px #6fbde2
height: $tableHeaderHeight
font-weight: normal
tbody
tr
padding: 5px
background-color: black
&:nth-child(even)
theme('background-color', '$uiGrayDarker')
td
position: relative
height: $bodyCellHeight
line-height: $bodyCellHeight
color: $tableTextPrimaryColor
border-top: 1px solid #436270
border-bottom: 1px solid #436270
vertical-align: middle
transition(all 0.1s ease)
p
margin: 0
&.timepointOptions
label
font-weight: 200
cursor: pointer
input
margin: 0 3px
&[type="radio"]
border: 0
clip: rect(0 0 0 0)
height: 1px
margin: -1px
overflow: hidden
padding: 0
position: absolute
width: 1px
& + span
display: block
position: absolute
border: 2px solid #c8c8c8
border-radius: 100%
height: 15px
width: 15px
top: 2px
left: 0
padding: 3px
z-index: 5
transition: border .25s linear
-webkit-transition: border .25s linear
& + span:before
display: block
content: ''
border-radius: 100%
height: 100%
width: 100%
transition: background 0.25s linear
-webkit-transition: background 0.25s linear
&:checked + span
theme('border-color', '$activeColor')
&:before
theme('background-color', '$activeColor')
&:disabled
& + span
border-color: #535960 !important
&:checked + span
border-color: #0D4256 !important
&:before
background-color: #0D4256 !important
label
padding: 0 5px
padding-left: 20px
display: inline-block
position: relative
line-height: initial
&.checkboxWrapper
label
display: block
margin: 0
input
margin: 0 3px
&[type="checkbox"]
border: 0
clip: rect(0 0 0 0)
height: 1px
margin: -1px
overflow: hidden
padding: 0
position: absolute
width: 1px
& + span
display: block
border: 1px solid #b6b6b6
background-color: white
border-radius: 2px
height: 16px
width: 16px
margin: 0 auto
z-index: 5
transition: border .25s linear
-webkit-transition: border .25s linear
& + span:before
display: block
position: relative
content: ''
font-size: 9px
top: -7px
color: white
background-color: transparent
transition: background 0.25s linear
-webkit-transition: background 0.25s linear
&:checked + span
theme('background-color', '$activeColor')
&:before
content: '\2713'
&.disabled
td
color: #535960
&:hover, &:active, &.active
background-color: $tableHoverColor
color: white
td
// This selector is necessary to override bootstrap's 'table' class
border-top: 1px solid #436270
border-bottom: 1px solid #436270
background-color: $tableHoverColor
@@ -0,0 +1,7 @@
// Study-Timepoint Association imports
import './associationModal/associationModal.html';
import './associationModal/associationModal.js';
import './associationModal/studyAssociationTable/studyAssociationTable.html';
import './associationModal/studyAssociationTable/studyAssociationTable.styl';
import './associationModal/studyAssociationTable/studyAssociationTable.js';
@@ -0,0 +1,12 @@
<template name="caseProgress">
{{#form (extend class='caseProgress' api=instance.api)}}
{{#if instance.isFollowUp.get}}
{{>radialProgressBar isLocked=isLocked progressPercent=progressPercent progressText=progressText}}
{{#if isLocked}}
<div class="caseProgressStatus">
<h5>Locked</h5>
</div>
{{/if}}
{{/if}}
{{/form}}
</template>
@@ -0,0 +1,143 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { OHIF } from 'meteor/ohif:core';
Template.caseProgress.onCreated(() => {
const instance = Template.instance();
instance.progressPercent = new ReactiveVar();
instance.progressText = new ReactiveVar();
instance.isLocked = new ReactiveVar(false);
instance.isFollowUp = new ReactiveVar(false);
});
Template.caseProgress.onRendered(() => {
const instance = Template.instance();
const { timepointApi, measurementApi, timepointId } = instance.data;
// Stop here if we have no current timepoint ID (and therefore no defined timepointAPI)
if (!timepointApi) {
instance.progressPercent.set(100);
return;
}
// Get the current and prior timepoints
const current = timepointApi.timepoints.findOne({ timepointId });
const priorFilter = {
latestDate: { $lt: current.latestDate },
patientId: current.patientId
};
const priorSorting = { sort: { latestDate: -1 } };
const prior = timepointApi.timepoints.findOne(priorFilter, priorSorting);
// Stop here if timepoint is locked
if (current && current.isLocked) {
return instance.isLocked.set(true);
} else {
instance.isLocked.set(false);
}
// Stop here if no current or prior timepoint was found
if (!current || !prior || !current.timepointId) {
return instance.progressPercent.set(100);
}
// Retrieve the initial number of targets left to measure at this
// follow-up. Note that this is done outside of the reactive function
// below so that new lesions don't change the initial target count.
const config = OHIF.measurements.MeasurementApi.getConfiguration();
const toolGroups = config.measurementTools;
const toolIds = [];
toolGroups.forEach(toolGroup => toolGroup.childTools.forEach(tool => {
const option = 'options.caseProgress.include';
if (OHIF.utils.ObjectPath.get(tool, option)) {
toolIds.push(tool.id);
}
}));
const getTimepointFilter = timepointId => ({
timepointId,
toolId: { $in: toolIds }
});
const getNumMeasurementsAtTimepoint = timepointId => {
OHIF.log.info('getNumMeasurementsAtTimepoint');
const filter = getTimepointFilter(timepointId);
let count = 0;
toolGroups.forEach(toolGroup => {
count += measurementApi.fetch(toolGroup.id, filter).length;
});
return count;
};
const getNumRemainingBetweenTimepoints = (currentTimepointId, priorTimepointId) => {
const currentFilter = getTimepointFilter(currentTimepointId);
const priorFilter = getTimepointFilter(priorTimepointId);
let totalRemaining = 0;
toolGroups.forEach(toolGroup => {
const toolGroupId = toolGroup.id;
const numCurrent = measurementApi.fetch(toolGroupId, currentFilter).length;
const numPrior = measurementApi.fetch(toolGroupId, priorFilter).length;
const remaining = Math.max(numPrior - numCurrent, 0);
totalRemaining += remaining;
});
return totalRemaining;
};
// If we're currently reviewing a Baseline timepoint, don't do any
// progress measurement.
if (current.timepointType === 'baseline') {
instance.progressPercent.set(100);
instance.isFollowUp.set(false);
} else {
instance.isFollowUp.set(true);
// Setup a reactive function to update the progress whenever
// a measurement is made
instance.autorun(() => {
measurementApi.changeObserver.depend();
// Obtain the number of Measurements for which the current Timepoint has
// no Measurement data
const totalMeasurements = getNumMeasurementsAtTimepoint(prior.timepointId);
const numRemainingMeasurements = getNumRemainingBetweenTimepoints(current.timepointId, prior.timepointId);
const numMeasurementsMade = totalMeasurements - numRemainingMeasurements;
// Update the Case Progress text with the remaining measurement count
instance.progressText.set(numRemainingMeasurements);
// Calculate the Case Progress as a percentage in order to update the
// radial progress bar
const progressPercent = Math.min(100, Math.round(100 * numMeasurementsMade / totalMeasurements));
instance.progressPercent.set(progressPercent);
});
}
});
Template.caseProgress.helpers({
progressPercent() {
return Template.instance().progressPercent.get();
},
progressText() {
return Template.instance().progressText.get();
},
isLocked() {
return Template.instance().isLocked.get();
},
progressComplete() {
const instance = Template.instance();
if (!instance.data.timepointApi) {
return true;
}
const progressPercent = instance.progressPercent.get();
return progressPercent === 100;
}
});
@@ -0,0 +1,20 @@
@require '{ohif:viewerbase}/app'
.caseProgress
transition(all 0.3 ease)
.radialProgress
display: inline-block
float:left
.caseProgressStatus
display: inline-block
float: left
margin-left: 22px
h5
theme('color', '$textPrimaryColor')
line-height: 40px
margin: 0
font-size: 15px
font-weight: bold
@@ -0,0 +1,8 @@
// Case Progress imports
import './caseProgress.html';
import './caseProgress.styl';
import './caseProgress.js';
import './radialProgressBar/radialProgressBar.html';
import './radialProgressBar/radialProgressBar.styl';
import './radialProgressBar/radialProgressBar.js';
@@ -0,0 +1,45 @@
<template name="radialProgressBar">
<div class="radialProgress">
<svg id="svg"
width="26"
height="26"
viewPort="0 0 26 26"
version="1.1"
xmlns="http://www.w3.org/2000/svg">
<!-- stroke-dasharray is 2 * PI * radius !-->
<circle r="11"
cx="13"
cy="13"
fill="transparent"
stroke-dasharray="69.11503837897544"
stroke-dashoffset="0">
</circle>
{{#unless isLocked}}
<circle id="bar"
r="11"
cx="13"
cy="13"
fill="transparent"
stroke-dasharray="69.11503837897544"
style="stroke-dashoffset: {{progressRadius}}px;">
</circle>
{{/unless}}
</svg>
<div class="progressArea {{#if isLocked}}locked{{else}}{{#if progressComplete}}complete{{/if}}{{/if}}">
{{#if isLocked}}
<svg>
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-status-lock"}}></use>
</svg>
{{else}}
{{#if progressComplete}}
<svg>
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-status-complete"}}></use>
</svg>
{{else}}
{{progressText}}
{{/if}}
{{/if}}
</div>
</div>
</template>
@@ -0,0 +1,15 @@
import { Template } from 'meteor/templating';
Template.radialProgressBar.helpers({
progressComplete() {
const instance = Template.instance();
return instance.data.progressPercent === 100;
},
progressRadius() {
const instance = Template.instance();
const radius = 11 * 2 * Math.PI;
const percentLeft = (100 - instance.data.progressPercent) / 100;
return percentLeft * radius;
}
});
@@ -0,0 +1,55 @@
@require '{ohif:viewerbase}/app'
$circleSize = 26px
.radialProgress
border-radius: 100%
height: $circleSize
margin-top: 3px
margin-right: 4px
position: relative
vendorize(box-shadow, 0 0 1em black)
width: $circleSize
#svg circle
stroke-dashoffset: 0
theme('stroke', '$uiBorderColorDark')
stroke-width: 3px
vendorize(transition, stroke-dashoffset 1s linear)
#svg #bar
theme('stroke', '$textSecondaryColor')
vendorize(transform, rotate(270deg))
vendorize(transform-origin, center center)
.progressArea
theme('color', '$textSecondaryColor')
display: table
font-weight: 700
font-size: 12px
height: $circleSize
line-height: $circleSize
left: 50%
position: absolute
text-align: center
top: 50%
transform(translate(-50%, -50%))
width: $circleSize
&.locked
line-height: 0
&, & svg
height: 12px
width: 10px
&.complete
line-height: 0
&, & svg
height: 16px
width: 16px
svg
theme('fill', '$textSecondaryColor')
theme('stroke', '$textSecondaryColor')
@@ -0,0 +1,5 @@
//import './association';
import './caseProgress';
import './measurementTable';
import './measurementLightTable';
import './measureFlow';
@@ -0,0 +1,3 @@
import './measureFlow.html';
import './measureFlow.styl';
import './measureFlow.js';
@@ -0,0 +1,32 @@
<template name="measureFlow">
<div class="measure-flow {{#if this.threeColumns}}tree-columns{{/if}} {{instance.state.get}}"
style="top:{{choose position.y 0}}px; left:{{choose position.x 0}}px"
tabindex="-1">
<button class="btn-add" style="opacity: 0">
{{#if this.measurement.location}}Edit{{else}}Add{{/if}} label
</button>
{{#if eq instance.state.get 'selected'}}
<div class="tree-leaf">
<i class="icon-check"></i>
<span>{{instance.value.label}}</span>
{{#if instance.descriptionEdit.get}}
<div class="description clearfix">
<textarea
class="pull-left"
rows="1"
style="{{#unless instance.description.get}}height:0{{/unless}}"
>{{instance.description.get}}</textarea>
</div>
{{else}}
{{#if instance.description.get}}
<div class="descriptionText">{{instance.description.get}}</div>
{{/if}}
{{/if}}
<div class="actions clearfix">
<button class="pull-left btn-rename">Relabel</button>
<button class="pull-right btn-description">{{valueIf instance.description.get 'Edit' 'Add'}} description</button>
</div>
</div>
{{/if}}
</div>
</template>
@@ -0,0 +1,326 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { Blaze } from 'meteor/blaze';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import _ from 'underscore';
import $ from 'jquery';
Template.measureFlow.onCreated(() => {
const instance = Template.instance();
instance.value = instance.data.currentValue || '';
instance.state = new ReactiveVar('closed');
instance.description = new ReactiveVar(instance.data.currentDescription || '');
instance.descriptionEdit = new ReactiveVar(false);
const items = [
'Abdomen/Chest Wall',
'Adrenal',
'Bladder',
'Bone',
'Brain',
'Breast',
'Colon',
'Esophagus',
'Extremities',
'Gallbladder',
'Kidney',
'Liver',
'Lung',
'Lymph Node',
'Mediastinum/Hilum',
'Muscle',
'Neck',
'Other Soft Tissue',
'Ovary',
'Pancreas',
'Pelvis',
'Peritoneum/Omentum',
'Prostate',
'Retroperitoneum',
'Small Bowel',
'Spleen',
'Stomach',
'Subcutaneous'
];
instance.items = [];
_.each(items, item => {
instance.items.push({
label: item,
value: item
});
});
const commonItems = [
'Abdomen/Chest Wall',
'Lung',
'Lymph Node',
'Liver',
'Mediastinum/Hilum',
'Pelvis',
'Peritoneum/Omentum',
'Retroperitoneum'
];
instance.commonItems = [];
_.each(commonItems, item => {
instance.commonItems.push({
label: item,
value: item
});
});
});
Template.measureFlow.onRendered(() => {
const instance = Template.instance();
const $measureFlow = instance.$('.measure-flow');
const $btnAdd = instance.$('.btn-add');
// Make the measure flow bounded by the window borders
$measureFlow.bounded();
$btnAdd.focus();
if (instance.data.autoClick) {
$btnAdd.trigger('click', {
clientX: instance.data.position.x,
clientY: instance.data.position.Y
});
} else {
if (instance.data.direction) {
const direction = instance.data.direction;
let { left, top } = $measureFlow.offset();
left = direction.x === -1 ? left -= $btnAdd.outerWidth() : left;
top = direction.y === -1 ? top -= $btnAdd.outerHeight() : top;
const distance = 5;
left += direction.x * distance;
top += direction.y * distance;
$measureFlow.css({
left,
top
});
}
// Display the button after reposition it
$btnAdd.css('opacity', 1);
}
});
Template.measureFlow.events({
'click, mousedown, mouseup'(event, instance) {
event.stopPropagation();
},
'click .measure-flow .btn-add, click .measure-flow .btn-rename'(event, instance) {
const $measureFlow = instance.$('.measure-flow');
// Set the open state for the component
instance.state.set('open');
// Wait template rerender before rendering the selectTree
Tracker.afterFlush(() => {
// Get the click or rendering position
let position;
if (_.isUndefined(event.clientX)) {
position = {
left: instance.data.position.x,
top: instance.data.position.y,
};
} else {
position = {
left: event.clientX,
top: event.clientY,
};
}
// Define the data for selectTreeComponent
const data = {
key: 'label',
items: instance.items,
commonItems: instance.commonItems,
hideCommon: instance.data.hideCommon,
label: 'Assign label',
searchPlaceholder: 'Search labels',
// storageKey: 'measureLabelCommon',
threeColumns: instance.data.threeColumns,
position
};
// Define in which element the selectTree will be rendered in
const parentElement = $measureFlow[0];
// Render the selectTree element
instance.selectTreeView = Blaze.renderWithData(Template.selectTree, data, parentElement);
// Focus the measure flow to handle closing
$measureFlow.focus();
});
},
'click .measure-flow .btn-description'(event, instance) {
// Fade out the action buttons
instance.$('.measure-flow .actions').addClass('fadeOut');
// Set the description edit mode
instance.descriptionEdit.set(true);
// Wait for DOM re-rendering, resize and focus the description textarea
Tracker.afterFlush(() => {
const $textarea = instance.$('textarea');
$textarea.trigger('input').focus().select();
});
},
'input textarea, change textarea'(event, instance) {
const element = event.currentTarget;
const $element = $(element);
// Resize the textarea based on its content length
$element.css('max-height', 0);
$element.height(element.scrollHeight);
$element.css('max-height', '');
// Reposition the measure flow if needed
const $measureFlow = instance.$('.measure-flow');
$element.one('transitionend', () => $measureFlow.trigger('spatialChanged'));
},
'keydown textarea'(event, instance) {
// Unset the description edit mode if ENTER or ESC was pressed
if (event.which === 13 || event.which === 27) {
instance.$('.measure-flow .actions').removeClass('fadeOut');
instance.descriptionEdit.set(false);
instance.$('.measure-flow').focus();
}
// Keep the current description if ENTER was pressed
if (event.which === 13) {
event.preventDefault();
instance.description.set($(event.currentTarget).val());
}
},
'blur textarea'(event, instance) {
instance.$('.measure-flow .actions').removeClass('fadeOut');
instance.descriptionEdit.set(false);
instance.description.set($(event.currentTarget).val());
},
'click .select-tree-common label'(event, instance) {
// Set the common section clicked flag
instance.commonClicked = event.currentTarget;
},
'change .select-tree-root'(event, instance) {
// Stop here if it's an inner input event
if (event.target !== event.currentTarget) {
return;
}
// Store the selectTree component value before it's removed from DOM
const $treeRoot = $(event.currentTarget);
instance.value = $treeRoot.data('component').value();
},
'click .tree-leaf input'(event, instance) {
const $target = $(event.currentTarget);
let $label = $target.closest('label');
const $treeRoot = $label.closest('.select-tree-root');
const $container = $treeRoot.find('.tree-options:first');
let labelOffset;
// Check if the clicked target was a label inside common section
if (instance.commonClicked) {
$label = $(instance.commonClicked);
labelOffset = $label.data('offset');
} else {
labelOffset = $label.offset();
}
// Change the measure flow state to selected
instance.state.set('selected');
// Wait for the DOM re-rendering
Tracker.afterFlush(() => {
// Get the measure flow div
const $measureFlow = instance.$('.measure-flow');
// Adjust the label position
if (instance.commonClicked) {
labelOffset.top -= 10;
labelOffset.left -= 12;
} else {
labelOffset.top -= 10;
}
// Reposition the measure flow based on the clicked label position
$measureFlow.css(labelOffset);
// Resize the copied label with same width of the clicked one
// $measureFlow.children('.tree-leaf').width($label.outerWidth());
$measureFlow.children('.tree-leaf').width(212);
// Reset the flag to avoid wrong positioning when clicking normal labels again
instance.commonClicked = false;
instance.data.updateCallback(instance.value.value, instance.description.get());
Meteor.defer(() => !$measureFlow.is(':hover') && $measureFlow.trigger('close'));
});
// Wait the fade-out transition and remove the selectTree component
$container.one('transitionend', event => Blaze.remove(instance.selectTreeView));
},
'blur .measure-flow'(event, instance) {
const $measureFlow = $(event.currentTarget);
const element = $measureFlow[0];
Meteor.defer(() => {
const focused = $(':focus')[0];
if (element !== focused && !$.contains(element, focused)) {
$measureFlow.trigger('close');
}
});
},
'mouseleave .measure-flow'(event, instance) {
const $measureFlow = $(event.currentTarget);
const canClose = instance.state.get() === 'selected' && !instance.descriptionEdit.get();
if (canClose && !$.contains($measureFlow[0], event.toElement)) {
$measureFlow.trigger('close');
}
},
'mouseenter .measure-flow'(event, instance) {
// Prevent from closing if user go out and in too fast
clearTimeout(instance.closingTimeout);
$(event.currentTarget).off('animationend').removeClass('fadeOut');
},
'close .measure-flow'(event, instance) {
const $measureFlow = $(event.currentTarget);
// Clear the timeout to prevent executing the close process twice
clearTimeout(instance.closingTimeout);
instance.closingTimeout = setTimeout(() => {
const animationEndHandler = event => {
// Prevent closing if the animation is coming from actions panel
if (event.target !== $measureFlow[0]) {
$measureFlow.one('animationend', animationEndHandler);
return;
}
instance.data.doneCallback();
};
$measureFlow.one('animationend', animationEndHandler).addClass('fadeOut');
}, 300);
}
});
@@ -0,0 +1,196 @@
@import "{ohif:viewerbase}/app"
.measure-flow
outline: none
position: fixed
z-index: 1
&.fadeOut
animateFadeOut()
&.open,
&.selected
.btn-add
opacity: 0 !important
outline: none !important
pointer-events: none !important
.btn-add
outline: none
theme('color', '$textColorActive')
theme('background-color', '$activeColor')
theme('border', '2px solid $uiBorderColor')
border-radius: 14px
cursor: pointer
font-weight: bold
height: 24px
left: 0
line-height: 24px
opacity: 1
position: absolute
padding: 0 14px
top: 0
transition(opacity 0.3s ease)
white-space: nowrap
// / {selector()}
// color: $themes['tide']['uiYellow'] !important
.select-tree-root>.tree-content
width: 140px
&>.tree-options
position: relative
z-index: 2
.tree-inputs
position: relative
&>.tree-leaf
position: relative
.icon-check
theme('background-color', '$activeColor')
animateZoomIn()
left: -46px
position: absolute
top: 3px
span
background: white
font-weight: normal
height: 46px
line-height: 46px
padding: 0 12px
input, span
display: none
.actions
padding-top: 16px
opacity: 0
&:not(.fadeOut)
animateFadeIn()
animation-delay: 0.3s
&.fadeOut
animateFadeOut()
button
theme('border', '1px solid $uiBorderColor')
theme('color', '$textPrimaryColor')
theme('background-color', '$primaryBackgroundColor')
border-radius: 16px
font-weight: normal
height: 31px
line-height: 31px
padding: 0 12px
text-align: center
.description
margin-top: -10px
margin-bottom: 10px
textarea
background-color: white
box-shadow: 0 10px white
border: 0
line-height: 20px
outline: none
overflow: hidden
padding: 0 12px 5px
resize: none
transition(height 0.3s ease)
width: 100%
.descriptionText
background-color: white
box-shadow: 0 10px white
line-height: 20px
margin-bottom: 10px
margin-top: -10px
padding: 0 12px
&.selected>.tree-leaf
span
display: block
&.tree-columns
.select-tree-root
&.navigated .select-tree-common .content
animation-name: none
&.selected .select-tree-common .content
animation-name: selectTreeCommonCloseLeft
.tree-search
input, i
display: none
.select-tree-common
padding-left: 0
padding-right: 6px
right: auto
left: 0
width: 180px
&>.tree-content
transform-origin(0% 50%)
width: 280px
.select-tree-common .content
right: -180px
&.started .select-tree-common .content
right: 46px
&.navigated .select-tree-common .content
animation-name: selectTreeCommonCloseRight
.tree-inputs>label
float: left
width: 50%
@keyframes selectTreeCommonCloseRight {
from {
height: 37px
display: table
right: 6px
opacity: 1
visibility: visible
}
to {
height: 100%
right: -100%
opacity: 0
visibility: hidden
width: calc(100% - 6px)
}
}
.icon-check
background-color: white
border-radius: 20px
display: block
height: 40px
overflow: hidden
position: relative
width: 40px
&:before
border: 2px solid black
border-left-width: 0
border-top-width: 0
content: ''
display: block
height: 18px
left: 15px
position: absolute
top: 9px
transform(rotate(45deg))
width: 9px
@@ -0,0 +1,22 @@
// Measurement Table Components imports
import './measurementEditDescription/measurementEditDescription.html';
import './measurementEditDescription/measurementEditDescription.js';
import './measurementLightTable.html';
import './measurementLightTable.styl';
import './measurementLightTableHeaderRow/measurementLightTableHeaderRow.html';
import './measurementLightTableHeaderRow/measurementLightTableHeaderRow.styl';
import './measurementLightTableRow/measurementLightTableRow.html';
import './measurementLightTableRow/measurementLightTableRow.styl';
import './measurementLightTableRow/measurementLightTableRow.js';
import './measurementLightTableView/measurementLightTableView.html';
import './measurementLightTableView/measurementLightTableView.styl';
import './measurementLightTableView/measurementLightTableView.js';
import './measurementRelabel/measurementRelabel.html';
import './measurementRelabel/measurementRelabel.styl';
import './measurementRelabel/measurementRelabel.js';
@@ -0,0 +1,13 @@
<template name="measurementEditDescription">
{{#dialogForm (extend this
dialogClass='modal-sm'
api=instance.api
)}}
{{>inputText
labelClass='form-group'
key='description'
hideSearch=true
options=(clone placeholder='Add a description')
}}
{{/dialogForm}}
</template>
@@ -0,0 +1,26 @@
import { Template } from 'meteor/templating';
import { OHIF } from 'meteor/ohif:core';
Template.measurementEditDescription.onRendered(() => {
const instance = Template.instance();
const form = instance.$('form').data('component');
const measurementData = instance.data.measurementData;
const collection = OHIF.viewer.measurementApi.tools[measurementData.toolType];
const currentMeasurement = collection.findOne({ _id: measurementData._id });
if (currentMeasurement.description) {
form.value({
description: currentMeasurement.description
});
}
// Update the description after confirming the dialog data
instance.data.promise.then(formData => {
collection.update({
_id: measurementData._id,
}, {
$set: { description: formData.description }
});
});
});
@@ -0,0 +1,17 @@
<template name="measurementLightTable">
<div id="measurementLightTableContainer" class="flex-v">
<div class="measurementLightTableHeaderContainer">
<div class="measurementLightTableHeader">
<div class="studyDateLabel">
Study date:
</div>
<div class="studyDate">
{{formatDA this.timepointApi.all.[0].latestDate "DD-MMM-YY"}}
</div>
</div>
</div>
{{#scrollArea class='measurementLightTableView flex-grow fit' scrollStep=63}}
{{>measurementLightTableView (clone this)}}
{{/scrollArea}}
</div>
</template>
@@ -0,0 +1,40 @@
@require '{ohif:viewerbase}/app'
#measurementLightTableContainer
theme('background-color', '$primaryBackgroundColor')
height: 100%
width: 100%
.measurementLightTableHeaderContainer
display: flex
padding: 0 2px 0 44px
position: relative
.measurementLightTableHeader
flex: 1
justify-content: space-around
padding-top: 2px
position: relative
margin: 0.4em 0
.studyDateLabel, .studyDate
theme('border-left', '%s solid $uiBorderColor' % $uiBorderThickness)
font-weight: 400
padding-left: 12px
text-align: left
.studyDateLabel
theme('color', '$textSecondaryColor')
font-size: 12px
line-height: 12px
.studyDate
theme('color', '$textPrimaryColor')
font-size: 14px
line-height: 20px
padding-bottom: 6px
.measurementLightTableLayoutChanger
margin: 0 auto
padding: 20px
text-align: center
@@ -0,0 +1,6 @@
<template name="measurementLightTableHeaderRow">
<div class="measurementLightTableHeaderRow">
<div class="type">{{toolGroup.name}}</div>
<div class="numberOfMeasurements">{{measurementRows.length}}</div>
</div>
</template>
@@ -0,0 +1,36 @@
@import "{ohif:viewerbase}/app"
$headerRowHeight = 63px
.measurementLightTableHeaderRow
theme('background-color', '$uiGrayDarker')
theme('color', '$textSecondaryColor')
display: flex
theme('fill', '$textSecondaryColor')
height: $headerRowHeight
line-height: $headerRowHeight
margin-top: 2px
width: 100%
div
align-items: stretch
flex: 1
justify-content: space-around
text-align: center
.type
theme('color', '$textSecondaryColor')
font-size: 22px
font-weight: 300
line-height: $headerRowHeight
padding: 0 10px
text-align: left
.numberOfMeasurements
theme('color', '$uiSkyBlue')
float: right
font-weight: 300
font-size: 40px
max-width: 54px
height: $headerRowHeight
line-height: 66px
@@ -0,0 +1,55 @@
<template name="measurementLightTableRow">
<div class="measurementLightTableRow" data-measurementid="{{_id}}">
<div class='measurementRowSidebar'>
<div class="measurementNumber">
{{rowItem.measurementNumber}}
</div>
{{#if rowItem.responseStatus}}
<div class="response-status-icon">
{{rowItem.responseStatus}}
</div>
{{/if}}
</div>
<div class="measurementRowContent">
<div class="measurementDetails">
<label class="location">
{{#if rowItem.location}}
{{rowItem.location}}
{{else}}
(No Location)
{{/if}}
{{#if rowItem.description}}
({{rowItem.description}})
{{/if}}
</label>
<div class="value">
{{#if displayData}}
{{{displayData}}}
{{else}}
...
{{/if}}
</div>
</div>
<div class="rowOptions">
<div class="rowAction js-edit-label pull-left m-r-2" tabindex="1">
<svg class="edit-icon">
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-measurements-additional"}}></use>
</svg>
Relabel
</div>
<div class="rowAction js-edit-description pull-left m-r-2" tabindex="1">
<svg class="edit-icon">
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-measurements-additional"}}></use>
</svg>
Description
</div>
<div class="rowAction js-delete pull-left" tabindex="1">
<svg class="close-icon">
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-ui-close"}}></use>
</svg>
Delete
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,103 @@
import { Template } from 'meteor/templating';
import _ from 'underscore';
import $ from 'jquery';
import { OHIF } from 'meteor/ohif:core';
import { cornerstone } from 'meteor/ohif:cornerstone';
const getPosition = event => {
return {
x: event.clientX,
y: event.clientY
};
};
Template.measurementLightTableRow.helpers({
displayData() {
const instance = Template.instance();
const { rowItem } = instance.data;
const data = rowItem.entries[0];
const config = OHIF.measurements.MeasurementApi.getConfiguration();
const measurementTools = config.measurementTools;
const toolGroup = measurementTools.find( toolGroup => toolGroup.id === rowItem.measurementTypeId);
const tool = toolGroup.childTools.find( childTool => childTool.id === data.toolType );
if (!tool) {
return 'No measurement value';
}
const { displayFunction } = tool.options.measurementTable;
return displayFunction(data);
}
});
Template.measurementLightTableRow.events({
'click .measurementLightTableRow'(event, instance) {
const $row = instance.$('.measurementLightTableRow');
const rowItem = instance.data.rowItem;
const timepoints = instance.data.timepointApi.all();
$row.closest('.measurementLightTableView').find('.measurementLightTableRow').not($row).removeClass('active');
$row.toggleClass('active');
const childToolKey = $(event.target).attr('data-child');
OHIF.measurements.jumpToRowItem(rowItem, timepoints, childToolKey);
},
'click .js-edit-label'(event, instance) {
event.stopPropagation();
const rowItem = instance.data.rowItem;
const entry = rowItem.entries[0];
// Show the measure flow for measurements
OHIF.measurements.openLocationModal({
measurement: entry,
element: document.body,
measurementApi: instance.data.measurementApi,
position: getPosition(event),
autoClick: true
});
},
'click .js-edit-description'(event, instance) {
const rowItem = instance.data.rowItem;
const entry = rowItem.entries[0];
OHIF.ui.showDialog('measurementEditDescription', {
event,
title: 'Edit Description',
element: event.element,
measurementData: entry
});
},
'click .js-delete'(event, instance) {
event.stopPropagation();
const dialogSettings = {
class: 'themed',
title: 'Delete measurements',
message: 'Are you sure you want to delete the measurement?',
position: getPosition(event)
};
OHIF.ui.showDialog('dialogConfirm', dialogSettings).then(formData => {
const measurementTypeId = instance.data.rowItem.measurementTypeId;
const measurement = instance.data.rowItem.entries[0];
const measurementNumber = measurement.measurementNumber;
const { timepointApi, measurementApi } = instance.data;
// Remove all the measurements with the given type and number
measurementApi.deleteMeasurements(measurementTypeId, { measurementNumber });
// Sync the new measurement data with cornerstone tools
const baseline = timepointApi.baseline();
measurementApi.sortMeasurements(baseline.timepointId);
// Repaint the images on all viewports without the removed measurements
_.each($('.imageViewerViewport'), element => cornerstone.updateImage(element));
// Notify that viewer suffered changes
OHIF.measurements.triggerTimepointUnsavedChanges('deleted');
});
}
});
@@ -0,0 +1,87 @@
@import "{ohif:viewerbase}/app"
.measurementLightTableRow
display: flex
margin-left: -6px
margin-top: 2px
padding-left: 6px
opacity: 0.7
transform(scale(1))
width: calc(100% + 6px)
&:hover
opacity 1
&.active
opacity 1
.measurementRowSidebar
theme('color', '$activeColor')
.rowOptions
height: 35px
visibility: visible
.rowOptions
theme('background-color', '$uiGrayDarker')
height: 0
overflow: hidden
transition(all 0.3s ease)
visibility: hidden
padding-left: 14px
.rowAction
theme('color', '$defaultColor')
cursor: pointer
line-height: 35px
transition(all 0.3s ease)
&:hover, &:active
theme('color', '$textPrimaryColor')
svg
theme('fill', '$textPrimaryColor')
theme('stroke', '$textPrimaryColor')
svg
theme('fill', '$defaultColor')
theme('stroke', '$defaultColor')
transition(all 0.3s ease)
&.edit-icon
width: 14px
height: 13px
&.close-icon
width: 11px
height: 11px
.measurementRowSidebar
theme('background', '$uiGray')
theme('color', '$textSecondaryColor')
cursor: pointer
flex: 1
max-width: 30px
transition(all 0.3s ease)
.measurementNumber
font-size: 14px
font-weight: 400
margin-right: 5px
padding-top: 10px
text-align: center
.measurementRowContent
flex: 1
.measurementDetails
padding: 5px 2px 0 14px
line-height: 30px
font-size: 14px
.location
theme('color', '$textSecondaryColor')
font-weight: 400
margin-left: -2px
.value
theme('color', '$textPrimaryColor')
@@ -0,0 +1,29 @@
<template name="measurementLightTableView">
{{#let config=measurementConfiguration groups=measurementGroups.get}}
{{#each measurementGroup in groups}}
{{>measurementLightTableHeaderRow (clone this
toolGroup=measurementGroup.toolGroup
measurementRows=measurementGroup.measurementRows)}}
{{#each rowItem in measurementGroup.measurementRows}}
{{>measurementLightTableRow (clone this rowItem=rowItem)}}
{{/each}}
{{/each}}
{{/let}}
{{#if hasAnyMeasurement}}
<div class="report-area">
{{#if saveEnabled}}
{{#if hasUnsavedChanges}}
<div class="unsaved-changes-alert">
You have unsaved changes
</div>
{{/if}}
{{/if}}
{{#form (extend api=instance.api)}}
{{#button class='btn p-x-2' action='exportCSV'}}Export CSV{{/button}}
{{#if saveEnabled}}
{{#button class='btn p-x-2' action='save' disabled=(not hasUnsavedChanges)}}Save to Archive{{/button}}
{{/if}}
{{/form}}
</div>
{{/if}}
</template>
@@ -0,0 +1,75 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { OHIF } from 'meteor/ohif:core';
Template.measurementLightTableView.onCreated(() => {
const instance = Template.instance();
const { measurementApi, timepointApi } = instance.data;
instance.data.measurementGroups = new ReactiveVar();
instance.path = 'viewer.studyViewer.measurements';
instance.saveObserver = new Tracker.Dependency();
instance.api = {
save() {
const successHandler = () => {
OHIF.ui.unsavedChanges.clear(`${instance.path}.*`);
instance.saveObserver.changed();
};
// Display the error messages
const errorHandler = data => {
OHIF.ui.showDialog('dialogInfo', Object.assign({ class: 'themed' }, data));
};
const promise = instance.data.measurementApi.storeMeasurements();
promise.then(successHandler).catch(errorHandler);
OHIF.ui.showDialog('dialogLoading', {
promise,
text: 'Measurements saved.'
});
return promise;
},
exportCSV() {
const { measurementApi, timepointApi } = instance.data;
OHIF.measurements.exportCSV(measurementApi, timepointApi);
}
};
instance.autorun(() => {
measurementApi.changeObserver.depend();
const data = OHIF.measurements.getMeasurementsGroupedByNumber(measurementApi, timepointApi);
instance.data.measurementGroups.set(data);
});
});
Template.measurementLightTableView.helpers({
hasUnsavedChanges() {
const instance = Template.instance();
// Run this computation on save or every time any measurement / timepoint suffer changes
OHIF.ui.unsavedChanges.depend();
instance.saveObserver.depend();
return OHIF.ui.unsavedChanges.probe('viewer.*') !== 0;
},
hasAnyMeasurement() {
const instance = Template.instance();
const groups = instance.data.measurementGroups.get();
if (!groups) {
return false;
}
const group = groups.find(item => item.measurementRows.length > 0);
return group;
},
saveEnabled() {
const server = OHIF.servers.getCurrentServer();
return (server && server.type === 'dicomWeb');
}
});
@@ -0,0 +1,19 @@
@import "{ohif:viewerbase}/app"
.measurementLightTableView
.report-area
theme('background-color', '$uiGrayDarker')
margin-top: 2px
padding: 10px 0
text-align: center
.btn
theme('background-color', '$activeColor')
theme('border', '1px solid $uiBorderColorActive')
color: #000
.unsaved-changes-alert
padding-bottom: 10px;
font-size: 11px;
color: #FF0;
font-weight: 100;
@@ -0,0 +1,4 @@
<template name="measurementRelabel">
<div class="measurement-relabel {{#if this.threeColumns}}tree-columns{{/if}} {{instance.state.get}}" tabindex="-1">
</div>
</template>
@@ -0,0 +1,107 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { Blaze } from 'meteor/blaze';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import $ from 'jquery';
import {
segmentedTerminologyList,
segmentedTerminologyCommonList } from '../../../lib/getLabelTerminologyList';
Template.measurementRelabel.onCreated(() => {
const instance = Template.instance();
instance.value = instance.data.currentValue || '';
instance.state = new ReactiveVar('closed');
instance.items = segmentedTerminologyList;
instance.commonItems = segmentedTerminologyCommonList;
});
Template.measurementRelabel.onRendered(() => {
const instance = Template.instance();
const $measurementRelabel = instance.$('.measurement-relabel');
// Make the measure flow bounded by the window borders
$measurementRelabel.bounded();
// Wait template rerender before rendering the selectTree
Tracker.afterFlush(() => {
// Get the click or rendering position
const position = {
left: event.clientX || instance.data.position.x,
top: event.clientY || instance.data.position.y,
};
// Define the data for selectTreeComponent
const data = {
key: 'label',
items: instance.items,
commonItems: instance.commonItems,
hideCommon: instance.data.hideCommon,
label: 'Assign label',
search: true,
searchPlaceholder: 'Search labels',
threeColumns: instance.data.threeColumns,
position
};
// Define in which element the selectTree will be rendered in
const parentElement = $measurementRelabel[0];
// Render the selectTree element
instance.selectTreeView = Blaze.renderWithData(Template.selectTree, data, parentElement);
// Focus the measure flow to handle closing
$measurementRelabel.focus();
});
});
Template.measurementRelabel.events({
'click, mousedown, mouseup'(event, instance) {
event.stopPropagation();
},
'click .tree-leaf input'(event, instance) {
const $target = $(event.currentTarget);
const $measureFlow = instance.$('.measurement-relabel');
instance.state.set('selected');
instance.value = $target.data('component').value();
instance.data.updateCallback(instance.value);
$measureFlow.trigger('close')
},
'blur .measurement-relabel'(event, instance) {
const $measurementRelabel = $(event.currentTarget);
const element = $measurementRelabel[0];
Meteor.defer(() => {
const focused = $(':focus')[0];
if (element !== focused && !$.contains(element, focused)) {
$measurementRelabel.trigger('close');
}
});
},
'mouseleave .measurement-relabel'(event, instance) {
const $measurementRelabel = $(event.currentTarget);
const canClose = instance.state.get() === 'selected';
if (canClose && !$.contains($measurementRelabel[0], event.toElement)) {
$measurementRelabel.trigger('close');
}
},
'mouseenter .measurement-relabel'(event, instance) {
// Prevent from closing if user go out and in too fast
clearTimeout(instance.closingTimeout);
$(event.currentTarget).off('animationend').removeClass('fadeOut');
},
'close .measurement-relabel'(event, instance) {
// Clear the timeout to prevent executing the close process twice
clearTimeout(instance.closingTimeout);
instance.data.doneCallback();
}
});
@@ -0,0 +1,106 @@
@import "{ohif:viewerbase}/app"
.measurement-relabel
outline: none
position: fixed
z-index: 1
&.fadeOut
animateFadeOut()
.select-tree-root>.tree-content
width: 140px
&>.tree-options
position: relative
z-index: 2
.tree-inputs
position: relative
&>.tree-leaf
position: relative
.icon-check
theme('background-color', '$activeColor')
animateZoomIn()
left: -46px
position: absolute
top: 3px
span
background: white
font-weight: normal
height: 46px
line-height: 46px
padding: 0 12px
input, span
display: none
.actions
padding-top: 16px
opacity: 0
&:not(.fadeOut)
animateFadeIn()
animation-delay: 0.3s
&.fadeOut
animateFadeOut()
&.selected>.tree-leaf
span
display: block
&.tree-columns
.select-tree-root
&.navigated .select-tree-common .content
animation-name: none
&.selected .select-tree-common .content
animation-name: selectTreeCommonCloseLeft
.select-tree-common
padding-left: 0
padding-right: 6px
right: auto
left: 0
width: 180px
&>.tree-content
transform-origin(0% 50%)
width: 280px
.select-tree-common .content
right: -180px
&.started .select-tree-common .content
right: 46px
&.navigated .select-tree-common .content
animation-name: selectTreeCommonCloseRight
.tree-inputs>label
float: left
width: 50%
@keyframes selectTreeCommonCloseRight {
from {
height: 37px
display: table
right: 6px
opacity: 1
visibility: visible
}
to {
height: 100%
right: -100%
opacity: 0
visibility: hidden
width: calc(100% - 6px)
}
}
@@ -0,0 +1,30 @@
// Measurement Table Components imports
import './measurementTable.html';
import './measurementTable.styl';
import './measurementTable.js';
import './measurementTableView/measurementTableView.html';
import './measurementTableView/measurementTableView.styl';
import './measurementTableView/measurementTableView.js';
import './measurementTableHUD/measurementTableHUD.html';
import './measurementTableHUD/measurementTableHUD.styl';
import './measurementTableHUD/measurementTableHUD.js';
import './measurementTableRow/measurementTableRow.html';
import './measurementTableRow/measurementTableRow.styl';
import './measurementTableRow/measurementTableRow.js';
import './measurementTableHeaderRow/measurementTableHeaderRow.html';
import './measurementTableHeaderRow/measurementTableHeaderRow.styl';
import './measurementTableHeaderRow/measurementTableHeaderRow.js';
import './measurementTableTimepointCell/measurementTableTimepointCell.html';
import './measurementTableTimepointCell/measurementTableTimepointCell.styl';
import './measurementTableTimepointCell/measurementTableTimepointCell.js';
import './measurementTableTimepointHeader/measurementTableTimepointHeader.html';
import './measurementTableTimepointHeader/measurementTableTimepointHeader.styl';
import './measurementTableTimepointHeader/measurementTableTimepointHeader.js';
import './measurementTableWarnings/measurementTableWarningsDialog.html';
@@ -0,0 +1,25 @@
<template name="measurementTable">
<div id="measurementTableContainer" class="flex-v">
<div class="measurementTableHeader">
{{#if this.timepoints.get.length}}
<div class="measurementTableLayoutChanger viewerRoundedButtonGroup">
{{>roundedButtonGroup buttonGroupData}}
</div>
{{/if}}
<div class="measurementTableTimepointHeaderRow">
<div class="warning-status {{#unless hasWarnings}}hidden{{/unless}}">
<svg>
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-ui-warning"}}></use>
</svg>
</div>
{{#each timepoint in this.timepoints.get}}
{{>measurementTableTimepointHeader (clone this timepoint)}}
{{/each}}
<!-- TODO: Add header for cross-sectional / non-longitudinal data -->
</div>
</div>
{{#scrollArea class='measurementTableView flex-grow fit' scrollStep=63}}
{{>measurementTableView (clone this)}}
{{/scrollArea}}
</div>
</template>
@@ -0,0 +1,87 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import _ from 'underscore';
import { OHIF } from 'meteor/ohif:core';
Template.measurementTable.onCreated(() => {
const instance = Template.instance();
instance.data.measurementTableLayout = new ReactiveVar('comparison');
instance.data.timepoints = new ReactiveVar([]);
// Run this computation every time table layout changes
instance.autorun(() => {
// Get the current table layout
const tableLayout = instance.data.measurementTableLayout.get();
const timepointApi = instance.data.timepointApi;
let timepoints;
if (!timepointApi) {
timepoints = [];
} else if (tableLayout === 'key') {
timepoints = timepointApi.key();
} else {
timepoints = timepointApi.comparison();
}
// Return key timepoints
instance.data.timepoints.set(timepoints);
});
});
Template.measurementTable.onRendered(() => {
const instance = Template.instance();
instance.autorun(() => {
// Run this computation every time the lesion table layout is changed
instance.data.measurementTableLayout.dep.depend();
if (instance.data.state.get('rightSidebar') !== 'measurements') {
// Remove the amount attribute from sidebar element tag
instance.$('#measurementTableContainer').closest('.sidebarMenu').removeAttr('data-timepoints');
return;
}
// Get the amount of timepoints being shown
const timepointAmount = instance.data.timepoints.get().length;
// Set the amount in an attribute on sidebar element tag
instance.$('#measurementTableContainer').closest('.sidebarMenu').attr('data-timepoints', timepointAmount);
});
});
Template.measurementTable.helpers({
hasWarnings() {
return Template.instance().data.conformanceCriteria.nonconformities.get();
},
buttonGroupData() {
const instance = Template.instance();
return {
value: instance.data.measurementTableLayout,
options: [{
value: 'comparison',
text: 'Comparison'
}, {
value: 'key',
text: 'Key Timepoints'
}]
};
}
});
Template.measurementTable.events({
'click .warning-status'(event, instance) {
const nonconformities = instance.data.conformanceCriteria.nonconformities.get();
const messages = [];
_.each(nonconformities, nonconformity => messages.push(nonconformity.message));
OHIF.ui.showDialog('measurementTableWarningsDialog', {
messages,
position: {
x: event.clientX,
y: event.clientY
}
});
}
});
@@ -0,0 +1,43 @@
@require '{ohif:viewerbase}/app'
#measurementTableContainer
theme('background-color', '$primaryBackgroundColor')
height: 100%
width: 100%
.measurementTableTimepointHeaderRow
display: flex
padding: 0 2px 0 44px
position: relative
.warning-status
theme('border', '2px solid $uiYellow')
border-radius: 16px
cursor: pointer
height: 32px
left: 0
margin: 4px 6px
padding-top: 1px
position: absolute
text-align: center
top: 0
transition(border-color 0.3s ease)
width: 32px
&:hover
theme('border-color', '$hoverColor')
svg
theme('fill', '$hoverColor')
svg
theme('fill', '$uiYellow')
display: inline
height: 20px
transition(all 0.3s ease)
width: 22px
.measurementTableLayoutChanger
margin: 0 auto
padding: 20px
text-align: center
@@ -0,0 +1,20 @@
<template name="measurementTableHUD">
<div id="measurementTableHUD" class="{{hudHidden}}">
<div class="flex-v full-height">
<div class="header">
<span>Measurements</span>
<svg class="buttonClose">
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-ui-close"}}></use>
</svg>
</div>
{{#scrollArea class='measurementTableView flex-grow fit' scrollStep=63}}
{{>measurementTableView (clone this isHud=true)}}
{{/scrollArea}}
<div class="footer">
{{#each toolbarButton in toolbarButtons}}
{{>toolbarSectionButton toolbarButton}}
{{/each}}
</div>
</div>
</div>
</template>
@@ -0,0 +1,72 @@
import { Template } from 'meteor/templating';
import { Session } from 'meteor/session';
import { ReactiveVar } from 'meteor/reactive-var';
Template.measurementTableHUD.onCreated(() => {
const instance = Template.instance();
const timepointApi = instance.data.timepointApi;
instance.isRemoved = true;
if (timepointApi) {
instance.data.timepoints = new ReactiveVar(timepointApi.currentAndPrior());
}
});
Template.measurementTableHUD.onDestroyed(() => {
const instance = Template.instance();
instance.isRemoved = true;
Session.set('measurementTableHudOpen', false);
});
Template.measurementTableHUD.onRendered(() => {
const instance = Template.instance();
instance.$('#measurementTableHUD').resizable().draggable().bounded();
});
Template.measurementTableHUD.events({
'click .buttonClose'(event, instance) {
Session.set('measurementTableHudOpen', false);
}
});
Template.measurementTableHUD.helpers({
hudHidden() {
let instance = Template.instance(),
isOpen = Session.get('measurementTableHudOpen');
if (isOpen) {
instance.isRemoved = false;
return 'dialog-animated dialog-open';
}
return instance.isRemoved !== true ? 'dialog-animated dialog-closed' : 'hidden';
},
toolbarButtons() {
let buttonData = [];
buttonData.push({
id: 'bidirectional',
title: 'Target',
classes: 'imageViewerTool toolbarSectionButton',
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-measure-target'
});
buttonData.push({
id: 'nonTarget',
title: 'Non-Target',
classes: 'imageViewerTool toolbarSectionButton',
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-measure-non-target'
});
buttonData.push({
id: 'length',
title: 'Temp',
classes: 'imageViewerTool toolbarSectionButton',
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-measure-temp'
});
return buttonData;
}
});
@@ -0,0 +1,95 @@
@require '{ohif:viewerbase}/app'
$borderColor = rgba(77, 99, 110, 0.81)
#measurementTableHUD
background: rgba(0, 0, 0, 0.95)
border-radius: 8px
border: solid 1px $borderColor;
bottom: 3px
height: 326px
left: auto
overflow: hidden
position: absolute
right: 3px
top: auto
width: 318px
z-index: 1
.measurementTableView .scrollable
margin-left: 0
padding-left: 0
.header
theme('background', '$uiGrayDarkest')
theme('color', '$textSecondaryColor')
border-bottom: $uiBorderThickness solid $borderColor
font-weight: 300
font-size: 20px
height: 55px
line-height: 55px
position: relative
text-align: center
width: 100%
svg.buttonClose
theme('color', '$defaultColor')
cursor: pointer
height: 14px
position: absolute
right: 14px
theme('stroke', '$defaultColor')
top: 20px
width: 14px
.measurementTableView
height: calc(100% - 55px - 70px)
padding-bottom: 20px
.footer
theme('background', '$uiGrayDarkest')
border-top: $uiBorderThickness solid $borderColor
height: 70px
padding: 10px
.toolbarSectionButton
display: inline-block
theme('color', '$defaultColor')
theme('fill', '$defaultColor')
theme('stroke', '$defaultColor')
padding: 0 10px
height: $toolbarHeight
min-width: 30px
cursor: pointer
text-align: center
&.disabled
opacity: 0.5
cursor: not-allowed
.buttonLabel
font-size: 12px
.svgContainer
margin: 0 auto
text-align: center
svg
background-color: transparent
margin: 2px
width: 21px
height: 21px
&:active, &.active
theme('color', '$activeColor')
svg
theme('fill', '$activeColor')
theme('stroke', '$activeColor')
&:hover
theme('color', '$hoverColor')
svg
theme('fill', '$hoverColor')
theme('stroke', '$hoverColor')
@@ -0,0 +1,11 @@
<template name="measurementTableHeaderRow">
<div class="measurementTableHeaderRow">
<div class="type">{{toolGroup.name}}</div>
<div class="max {{#if gt numberOfMeasurements maxNumMeasurements}}warning{{/if}}">
{{#if getMax toolGroup.id}}
<p class="maxNumMeasurements">Max {{getMax toolGroup.id}}</p>
{{/if}}
</div>
<div class="numberOfMeasurements">{{numberOfMeasurements toolGroup.id}}</div>
</div>
</template>
@@ -0,0 +1,33 @@
import { Template } from 'meteor/templating';
import { OHIF } from 'meteor/ohif:core';
import { Viewerbase } from 'meteor/ohif:viewerbase';
Template.measurementTableHeaderRow.helpers({
numberOfMeasurements(toolGroupId) {
const { toolGroup, measurementRows } = Template.instance().data;
if (toolGroup.id === 'newTargets') {
let result = 0;
measurementRows.forEach(measurementRow => {
const measurementData = measurementRow.entries[0];
if (measurementData.isSplitLesion) return;
result++;
});
return result;
}
return measurementRows.length ? measurementRows.length : null;
},
getMax(toolGroupId) {
const { conformanceCriteria } = Template.instance().data;
if (!conformanceCriteria) return;
if (toolGroupId === 'targets') {
return conformanceCriteria.maxTargets.get();
} else if (toolGroupId === 'newTargets') {
return conformanceCriteria.maxNewTargets.get();
}
}
});
@@ -0,0 +1,83 @@
@import "{ohif:viewerbase}/app"
$headerRowHeight = 63px
.measurementTableHeaderRow
theme('background-color', '$uiGrayDarker')
theme('color', '$textSecondaryColor')
display: flex
theme('fill', '$textSecondaryColor')
height: $headerRowHeight
line-height: $headerRowHeight
margin-top: 2px
width: 100%
&.inactive .add
cursor: not-allowed
svg
fill: #5A666D
div
align-items: stretch
flex: 1
justify-content: space-around
text-align: center
.add
cursor: pointer
max-width: 30px
padding-left: 2px
svg
fill: #C1D8E3
height: $headerRowHeight
max-width: 11px
&:hover svg
theme('fill', '$hoverColor')
&:active svg
theme('fill', '$activeColor')
.type
theme('color', '$textSecondaryColor')
font-size: 22px
font-weight: 300
line-height: $headerRowHeight
padding: 0 10px 0 20px
text-align: left
.numberOfMeasurements
theme('color', '$uiSkyBlue')
float: right
font-weight: 300
font-size: 40px
max-width: 54px
height: $headerRowHeight
line-height: 66px
.max
height: $headerRowHeight
max-width: 64px
text-align: right
.maxNumMeasurements
theme('background-color', '$textSecondaryColor')
border-radius: 3px
color: black
display: table
font-size: 12px
font-weight: 500
height: 19px
line-height: 17px
margin-left: auto
margin-top: 22px
padding: 2px 6px 0
text-transform: uppercase
transition($sidebarTransition)
&.warning
.maxNumMeasurements
theme('background-color', '$uiYellow')
theme('color', '$textPrimaryColor')
@@ -0,0 +1,52 @@
<template name="measurementTableRow">
<div class="measurementTableRow {{#if hasWarnings}}warning{{/if}}{{#if responseStatus}}response-status{{/if}}" data-measurementid="{{_id}}">
<div class='measurementRowSidebar'>
<div class="measurementNumber">
{{rowItem.measurementNumber}}
</div>
{{#if hasWarnings}}
<div class="warning-icon">
<svg>
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-ui-warning"}}></use>
</svg>
</div>
{{/if}}
{{#if rowItem.responseStatus}}
<div class="response-status-icon">
{{rowItem.responseStatus}}
</div>
{{/if}}
</div>
<div class="measurementDetails">
<div class='location'>
{{#if rowItem.location}}
{{getLocationLabel rowItem.location}}
{{else}}
(No description)
{{/if}}
</div>
<div class="timepointData">
<!--Each time point as a column-->
{{#each timepoint in timepoints.get}}
{{>measurementTableTimepointCell (clone this timepoint)}}
{{else}}
{{>measurementTableTimepointCell}}
{{/each}}
</div>
<div class="rowOptions">
<div class="rowAction js-rename pull-left m-r-2" tabindex="1">
<svg class="edit-icon">
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-measurements-additional"}}></use>
</svg>
Relabel
</div>
<div class="rowAction js-delete pull-left" tabindex="1">
<svg class="close-icon">
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-ui-close"}}></use>
</svg>
Delete
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,100 @@
import { Template } from 'meteor/templating';
import _ from 'underscore';
import $ from 'jquery';
import { OHIF } from 'meteor/ohif:core';
import { cornerstone } from 'meteor/ohif:cornerstone';
Template.measurementTableRow.onCreated(() => {
const instance = Template.instance();
instance.getWarningMessages = () => {
const measurementTypeId = instance.data.rowItem.measurementTypeId;
const measurementNumber = instance.data.rowItem.measurementNumber;
const groupedNonConformities = instance.data.conformanceCriteria.groupedNonConformities.get() || {};
const nonconformitiesByMeasurementTypeId = groupedNonConformities[measurementTypeId] || {};
const nonconformitiesByMeasurementNumbers = nonconformitiesByMeasurementTypeId.measurementNumbers || {};
const nonconformitiesByMeasurementNumber = nonconformitiesByMeasurementNumbers[measurementNumber] || {};
return _.uniq(nonconformitiesByMeasurementNumber.messages || []);
};
});
Template.measurementTableRow.helpers({
hasWarnings() {
return !!Template.instance().getWarningMessages().length;
}
});
Template.measurementTableRow.events({
'click .measurementRowSidebar .warning-icon'(event, instance) {
event.stopPropagation();
OHIF.ui.showDialog('measurementTableWarningsDialog', {
messages: instance.getWarningMessages(),
position: {
x: event.clientX,
y: event.clientY
}
});
},
'click .measurementRowSidebar'(event, instance) {
const $row = instance.$('.measurementTableRow');
const rowItem = instance.data.rowItem;
const timepoints = instance.data.timepoints.get();
$row.closest('.measurementTableView').find('.measurementTableRow').not($row).removeClass('active');
$row.toggleClass('active');
const childToolKey = $(event.target).attr('data-child');
OHIF.measurements.jumpToRowItem(rowItem, timepoints, childToolKey);
},
'click .js-rename'(event, instance) {
const rowItem = instance.data.rowItem;
const entry = rowItem.entries[0];
// Show the measure flow for targets
OHIF.measurements.toggleLabelButton({
measurement: entry,
element: document.body,
measurementApi: instance.data.measurementApi,
position: {
x: event.clientX,
y: event.clientY
},
autoClick: true
});
},
'click .js-delete'(event, instance) {
const dialogSettings = {
class: 'themed',
title: 'Delete measurements',
message: 'Are you sure you want to delete the measurement across all timepoints?',
position: {
x: event.clientX,
y: event.clientY
}
};
OHIF.ui.showDialog('dialogConfirm', dialogSettings).then(formData => {
const measurementTypeId = instance.data.rowItem.measurementTypeId;
const measurement = instance.data.rowItem.entries[0];
const measurementNumber = measurement.measurementNumber;
const { timepointApi, measurementApi } = instance.data;
// Remove all the measurements with the given type and number
measurementApi.deleteMeasurements(measurementTypeId, { measurementNumber });
// Sync the new measurement data with cornerstone tools
const baseline = timepointApi.baseline();
measurementApi.sortMeasurements(baseline.timepointId);
// Repaint the images on all viewports without the removed measurements
_.each($('.imageViewerViewport'), element => cornerstone.updateImage(element));
// Notify that viewer suffered changes
OHIF.measurements.triggerTimepointUnsavedChanges('deleted');
});
}
});
@@ -0,0 +1,123 @@
@import "{ohif:viewerbase}/app"
.measurementTableRow
display: flex
margin-left: -6px
margin-top: 2px
padding-left: 6px
// required transformation to make inner fixed elements relative to this one
transform(scale(1))
width: calc(100% + 6px)
&.active
.measurementRowSidebar
theme('color', '$activeColor')
.rowOptions
height: 35px
visibility: visible
&.response-status .measurementRowSidebar .response-status-icon
theme('background-color', '$defaultColor')
theme('color', '$uiGray')
border-radius: 11px
font-size: 12px
font-weight: 700
height: 21px
line-height: 22px
margin: 5px auto 0
text-align: center
width: 21px
&.warning .measurementRowSidebar
theme('background-color', '$uiYellow')
theme('color', '$textPrimaryColor')
.warning-icon, svg
width: 22px
height: 20px
pointer-events: inherit
.warning-icon
margin: 7px auto 0
svg
theme('fill', '$textPrimaryColor')
.rowOptions
theme('background-color', '$uiGrayDarker')
height: 0
overflow: hidden
transition(all 0.3s ease)
visibility: hidden
.rowAction
theme('color', '$defaultColor')
cursor: pointer
line-height: 35px
transition(all 0.3s ease)
&:hover, &:active
theme('color', '$textPrimaryColor')
svg
theme('fill', '$textPrimaryColor')
theme('stroke', '$textPrimaryColor')
svg
theme('fill', '$defaultColor')
theme('stroke', '$defaultColor')
transition(all 0.3s ease)
&.edit-icon
width: 14px
height: 13px
&.close-icon
width: 11px
height: 11px
.measurementRowSidebar
theme('background', '$uiGray')
theme('color', '$textSecondaryColor')
cursor: pointer
flex: 1
max-width: 30px
transition(all 0.3s ease)
.measurementNumber
font-size: 14px
font-weight: 400
margin-right: 5px
padding-top: 10px
text-align: center
.measurementDetails
flex: 1
padding: 5px 2px 0 0
&>*
padding-left: 14px
.location
font-weight: 400
font-size: 14px
theme('color', '$textSecondaryColor')
height: 30px
line-height: 30px
margin-left: -2px
width: 100%
.timepointData
display: flex
min-height: 27px
margin-top: 1px
div
flex: 1
font-size: 14px
font-weight: 400
justify-content: space-around
line-height: 18px
padding-bottom: 4px
@@ -0,0 +1,12 @@
<template name="measurementTableTimepointCell">
{{#if hasDataAtThisTimepoint}}
<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}}}
</div>
{{else}}
<div class="measurementTableTimepointCell noselect empty">
...
</div>
{{/if}}
</template>
@@ -0,0 +1,127 @@
import { Template } from 'meteor/templating';
import _ from 'underscore';
import $ from 'jquery';
import { OHIF } from 'meteor/ohif:core';
import { cornerstone } from 'meteor/ohif:cornerstone';
Template.measurementTableTimepointCell.helpers({
hasDataAtThisTimepoint() {
// This simple function just checks whether or not timepoint data
// exists for this Measurement at this Timepoint
const instance = Template.instance();
const { rowItem, timepointId } = instance.data;
if (timepointId) {
const dataAtThisTimepoint = _.where(rowItem.entries, { timepointId });
return dataAtThisTimepoint.length > 0;
} else {
return rowItem.entries.length > 0;
}
},
displayData() {
const instance = Template.instance();
const { rowItem, timepointId } = instance.data;
let data;
if (timepointId) {
const dataAtThisTimepoint = _.where(rowItem.entries, { timepointId });
if (dataAtThisTimepoint.length > 1) {
throw 'More than one measurement was found at the same timepoint with the same measurement number?';
}
data = dataAtThisTimepoint[0];
} else {
data = rowItem.entries[0];
}
const config = OHIF.measurements.MeasurementApi.getConfiguration();
const measurementTools = config.measurementTools;
const toolGroup = _.findWhere(measurementTools, { id: rowItem.measurementTypeId });
const tool = _.findWhere(toolGroup.childTools, { id: data.toolType });
if (!tool) {
// TODO: Figure out what is going on here?
OHIF.log.warn('Something went wrong?');
}
const { displayFunction } = tool.options.measurementTable;
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';
}
});
Template.measurementTableTimepointCell.events({
'dblclick .measurementTableTimepointCell'(event, instance) {
const { rowItem, timepointId } = instance.data;
if (!timepointId) return;
const measurementData = _.findWhere(rowItem.entries, { timepointId });
if (!measurementData || measurementData.toolType !== 'nonTarget') return;
const viewportIndex = rowItem.entries.indexOf(measurementData);
const $viewports = $('#viewer .imageViewerViewport');
let $element = $viewports.eq(viewportIndex);
$element = $element.length ? $element : $viewports.eq(0);
OHIF.ui.showDialog('dialogNonTargetMeasurement', {
event,
title: 'Change Lesion Location',
element: $element[0],
measurementData,
edit: true
});
},
'keydown .measurementTableTimepointCell'(event, instance) {
// Delete a lesion if Ctrl+D or DELETE is pressed while a lesion is selected
const keys = {
D: 68,
DELETE: 46
};
const keyCode = event.which;
if (keyCode === keys.DELETE || keyCode === keys.BACKSPACE || (keyCode === keys.D && event.ctrlKey === true)) {
const timepointId = instance.data.timepointId;
const offset = $(event.currentTarget).offset();
const dialogSettings = {
class: 'themed',
title: 'Delete measurements',
message: 'Are you sure you want to delete this measurement?',
position: {
x: offset.left,
y: offset.top
}
};
OHIF.ui.showDialog('dialogConfirm', dialogSettings).then(() => {
const measurementTypeId = instance.data.rowItem.measurementTypeId;
const measurement = instance.data.rowItem.entries[0];
const measurementNumber = measurement.measurementNumber;
const { timepointApi, measurementApi } = instance.data;
// Remove all the measurements with the given type and number
measurementApi.deleteMeasurements(measurementTypeId, {
measurementNumber,
timepointId
});
// Sync the new measurement data with cornerstone tools
const baseline = timepointApi.baseline();
measurementApi.sortMeasurements(baseline.timepointId);
// Repaint the images on all viewports without the removed measurements
_.each($('.imageViewerViewport'), element => cornerstone.updateImage(element));
});
}
}
});
@@ -0,0 +1,26 @@
@require '{ohif:viewerbase}/app'
.measurementTableTimepointCell
theme('border-left', '%s solid $uiBorderColor' % $uiBorderThickness)
cursor: pointer
padding: 0 10px
position: relative
.loading-spinner
display: none
font-size: 16px
position: absolute
right: 5px
top: 3px
span
display: block
&.loading .loading-spinner
display: block
&, span
theme('color', '$textPrimaryColor')
&:hover, span:hover
theme('color', '$activeColor')
@@ -0,0 +1,13 @@
<template name="measurementTableTimepointHeader">
<div class="measurementTableTimepointHeader">
<div class="timepointName">
{{timepointName this}}
</div>
<div class="timepointDate">
{{formatDA latestDate "DD-MMM-YY"}}
</div>
<div class="case-progress-container">
{{>caseProgress}}
</div>
</div>
</template>
@@ -0,0 +1,7 @@
import { Template } from 'meteor/templating';
Template.measurementTableTimepointHeader.helpers({
timepointName(timepoint) {
return Template.instance().data.timepointApi.name(timepoint);
}
});
@@ -0,0 +1,29 @@
@require '{ohif:viewerbase}/app'
.measurementTableTimepointHeader
flex: 1
justify-content: space-around
padding-top: 2px
position: relative
.timepointName, .timepointDate
theme('border-left', '%s solid $uiBorderColor' % $uiBorderThickness)
font-weight: 400
padding-left: 12px
text-align: left
.timepointName
theme('color', '$textSecondaryColor')
font-size: 12px
line-height: 12px
.timepointDate
theme('color', '$textPrimaryColor')
font-size: 14px
line-height: 20px
padding-bottom: 6px
.case-progress-container
position: absolute
right: 4px
top: 0
@@ -0,0 +1,39 @@
<template name="measurementTableView">
{{#let config=measurementConfiguration groups=measurementGroups.get}}
{{#each measurementGroup in groups}}
{{! #if hasMeasurements measurementGroup.toolGroup.id}}
{{>measurementTableHeaderRow (clone this
toolGroup=measurementGroup.toolGroup
measurementRows=measurementGroup.measurementRows)}}
{{! /if}}
{{#each rowItem in measurementGroup.measurementRows}}
{{>measurementTableRow (clone this rowItem=rowItem)}}
{{/each}}
{{/each}}
{{#each newLesionGroup in config.newLesions}}
{{#let toolGroup=(getNewLesionsToolGroup newLesionGroup)}}
{{#let newMeasurements=(newLesionsMeasurements toolGroup)}}
{{#if newMeasurements.length}}
{{#let collection=newMeasurements}}
{{>measurementTableHeaderRow (clone this
toolGroup=toolGroup
measurementRows=collection)}}
{{#each entry in collection}}
{{>measurementTableRow (clone this rowItem=entry)}}
{{/each}}
{{/let}}
{{/if}}
{{/let}}
{{/let}}
{{/each}}
{{/let}}
{{#unless this.isHud}}
{{#if or (hasMeasurements 'targets') (hasMeasurements 'nonTargets')}}
<div class="report-area">
<div class="btn btn-sm js-pdf">Generate Report</div>
</div>
{{/if}}
{{/unless}}
</template>
@@ -0,0 +1,94 @@
import { Template } from 'meteor/templating';
import { Tracker } from 'meteor/tracker';
import { ReactiveVar } from 'meteor/reactive-var';
import _ from 'underscore';
import { OHIF } from 'meteor/ohif:core';
Template.measurementTableView.onCreated(() => {
const instance = Template.instance();
const { measurementApi, timepointApi } = instance.data;
instance.data.measurementGroups = new ReactiveVar();
Tracker.autorun(() => {
measurementApi.changeObserver.depend();
const data = OHIF.measurements.getMeasurementsGroupedByNumber(measurementApi, timepointApi);
instance.data.measurementGroups.set(data);
});
});
Template.measurementTableView.events({
'click .js-pdf'(event, instance) {
const { measurementApi, timepointApi } = instance.data;
OHIF.measurements.exportPdf(measurementApi, timepointApi);
}
});
Template.measurementTableView.helpers({
hasMeasurements(toolGroupId) {
const instance = Template.instance();
const groups = instance.data.measurementGroups.get();
if (!groups) {
return false;
}
const group = _.find(groups, item => item.toolGroup.id === toolGroupId);
return group && !!group.measurementRows.length;
},
getNewLesionsToolGroup(newLesionGroup) {
const configuration = OHIF.measurements.MeasurementApi.getConfiguration();
const toolGroup = _.findWhere(configuration.measurementTools, { id: newLesionGroup.toolGroupId });
return {
id: newLesionGroup.id,
name: newLesionGroup.name,
childTools: toolGroup.childTools,
measurementTypeId: toolGroup.id
};
},
newLesionsMeasurements(toolGroup) {
const { measurementApi, timepointApi } = Template.instance().data;
const current = timepointApi.current();
const baseline = timepointApi.baseline();
if (!measurementApi || !timepointApi || !current || !baseline) return;
// If this is a baseline, stop here since there are no new measurements to display
if (!current || current.timepointType === 'baseline') {
OHIF.log.info('Skipping New Measurements section');
return;
}
// Retrieve all the data for this Measurement type (e.g. 'targets')
// which was recorded at baseline.
const measurementTypeId = toolGroup.measurementTypeId;
const atBaseline = measurementApi.fetch(measurementTypeId, {
timepointId: baseline.timepointId
});
// Obtain a list of the Measurement Numbers from the
// measurements which have baseline data
const numbers = atBaseline.map(m => m.measurementNumber);
// Retrieve all the data for this Measurement type which
// do NOT match the Measurement Numbers obtained above
const data = measurementApi.fetch(measurementTypeId, {
measurementNumber: { $nin: numbers }
});
// Group the Measurements by Measurement Number
const groupObject = _.groupBy(data, entry => entry.measurementNumber);
// Reformat the data for display in the table
return Object.keys(groupObject).map(key => ({
measurementTypeId: measurementTypeId,
measurementNumber: key,
location: OHIF.measurements.getLocation(groupObject[key]),
responseStatus: false, // TODO: Get the latest timepoint and determine the response status
entries: groupObject[key]
}));
}
});
@@ -0,0 +1,15 @@
@import "{ohif:viewerbase}/app"
.measurementTableView
.report-area
theme('background-color', '$uiGrayDarker')
margin-top: 2px
padding: 10px 0
.btn.js-pdf
theme('background-color', '$activeColor')
theme('border', '1px solid $uiBorderColorActive')
color: #000
display: table
margin: 0 auto
@@ -0,0 +1,9 @@
<template name="measurementTableWarningsDialog">
{{#dialogSimple (extend this class='themed' dialogClass='modal-sm' title='Criteria nonconformities')}}
<ol>
{{#each message in this.messages}}
<li>{{message}}</li>
{{/each}}
</ol>
{{/dialogSimple}}
</template>
@@ -0,0 +1,8 @@
// selectTree
import './selectTree/selectTree.html';
import './selectTree/selectTree.styl';
import './selectTree/selectTree.js';
// selectTreeCommon
import './selectTreeCommon/selectTreeCommon.html';
import './selectTreeCommon/selectTreeCommon.js';
@@ -0,0 +1,91 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import _ from 'underscore';
/*
* input: controls a tree selection component
*/
OHIF.mixins.selectTree = new OHIF.Mixin({
dependencies: 'group',
composition: {
onCreated() {
const instance = Template.instance();
const component = instance.component;
const rootComponent = instance.data.root || component;
const rootInstance = rootComponent.templateInstance;
// Store the component's current value
instance.currentValue = null;
// Create the component's value storage property
instance.data.currentNode = new ReactiveVar(null);
// Share the component in data property
instance.data.component = component;
// Override the default value method
component.value = value => {
const isGet = _.isUndefined(value);
// Return the current value
if (isGet) {
return component.parseData(rootInstance.currentValue);
}
// Change the current value
rootInstance.currentValue = value;
// Trigger the change event
component.$element.trigger('change');
};
// Method to manipulate nodes
component.node = node => {
const isGet = _.isUndefined(node);
// Return the current node
if (isGet) {
const currentNode = instance.data.currentNode.get();
return currentNode ? currentNode.value : null;
}
// get the items array
const items = component.templateInstance.data.items;
// Get the node for the selected value
const currentNode = _.findWhere(items, {
value: node
});
// Change the current node
instance.data.currentNode.set(currentNode);
};
// Return plain data for all the leaves inside current node
component.getLeaves = () => {
const recursiveSeek = (items, result=[]) => {
_.each(items, item => {
if (item.items) {
recursiveSeek(item.items, result);
} else {
result.push(item);
}
});
return result;
};
return recursiveSeek(instance.data.items);
};
},
onRendered() {
const instance = Template.instance();
const component = instance.component;
// Define the main element
component.$element = instance.$('.select-tree:first').first();
}
}
});
@@ -0,0 +1,89 @@
<template name="selectTree">
{{#group (extend this
class=(concat
'select-tree '
(valueIf this.root '' 'select-tree-root ')
this.class
)
mixins=(concat 'selectTree ' this.mixins)
)}}
{{>UI.contentBlock}}
<div class="tree-content">
{{#let currentNode=this.currentNode.get}}
{{#if (and (not this.root) (ne this.search false))}}
{{#inputText
label=this.label
labelClass='tree-search'
placeholder=(choose this.searchPlaceholder 'Search')
}}
<i class="fa fa-search"></i>
{{/inputText}}
{{/if}}
<div class="tree-options {{#if currentNode}}collapsed{{/if}}">
{{#if this.root}}
{{>selectTreeBreadcrumb component=this.component}}
{{/if}}
<div class="tree-inputs clearfix">
{{#each item in treeItems}}
{{>inputRadio
id=(concat this.storageKey '_' (encodeId item.value))
class=this.radioClass
name=this.key
itemData=item
value=item.value
label=item.label
labelClass=(valueIf (isArray item.items) 'tree-node' 'tree-leaf')
checked=(eq item.value (reactive this.value))
}}
{{/each}}
</div>
{{#if (and currentNode (isArray currentNode.items))}}
{{>selectTree (clone this
items=currentNode.items
root=(choose this.root this.component)
class='select-tree-node'
)}}
{{/if}}
</div>
{{/let}}
</div>
{{#if (and (not this.root) (or this.storageKey this.commonItems))}}
{{>selectTreeCommon (clone this component=this.component treeInstance=instance)}}
{{/if}}
{{/group}}
</template>
<template name="selectTreeBreadcrumb">
<div class="tree-breadcrumb clearfix">
{{>selectTreeBreadcrumbParent
component=this.component.parent
index=1
}}
{{#let currentNode=this.component.parent.templateInstance.data.currentNode.get}}
<span class="tree-current-node">{{currentNode.label}}</span>
{{/let}}
</div>
</template>
<template name="selectTreeBreadcrumbParent">
{{#let parent=this.component.parent}}
{{#let parentNode=(reactive parent.templateInstance.data.currentNode)}}
{{#if parentNode}}
{{>selectTreeBreadcrumbParent
component=parent
index=(sum this.index 1)
}}
{{/if}}
<span class="path-link">
<a href="#" class="tree-back" data-index="{{this.index}}">
{{#if parentNode}}
<span>{{parentNode.label}}</span>
{{else}}
<i class="fa fa-fast-backward"></i>
{{/if}}
</a>
{{#if parentNode}}<span>/</span>{{/if}}
</span>
{{/let}}
{{/let}}
</template>
@@ -0,0 +1,394 @@
import { OHIF } from 'meteor/ohif:core';
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { Tracker } from 'meteor/tracker';
import { ReactiveVar } from 'meteor/reactive-var';
import _ from 'underscore';
import $ from 'jquery';
// TODO: use npm dependency
import transition from 'meteor/ohif:core/client/lib/third-party/transition-to-from-auto';
Template.selectTree.onCreated(() => {
const instance = Template.instance();
// Return the common items
instance.getCommonItems = () => {
const instance = Template.instance();
// Return the common items if given
if (instance.data.commonItems) {
return instance.data.commonItems;
}
// Get all the tree leaves
const leaves = instance.data.component.getLeaves();
// Generate an object with encoded keys from the tree leaves
const leavesObject = {};
_.each(leaves, leaf => {
leavesObject[OHIF.string.encodeId(leaf.value)] = leaf;
});
// Get the current items ranking
const ranking = OHIF.user.getData(instance.data.storageKey);
// Sort the items based on how many times each one was used
const sorted = [];
_.each(ranking, (count, key) => sorted.push([key, count]));
sorted.sort((a, b) => b[1] - a[1]);
// Create the result and push every item respecting the ranking order
const result = [];
_.each(sorted, item => {
const current = leavesObject[item[0]];
if (current) {
result.push(current);
}
});
// Return the resulting array
return result;
};
// Create a reactive variable to control the search
instance.searchTerm = new ReactiveVar('');
});
Template.selectTree.onRendered(() => {
const instance = Template.instance();
const component = instance.$('.select-tree:first').data('component');
const rootComponent = instance.data.root || component;
const rootInstance = rootComponent.templateInstance;
const $treeRoot = rootInstance.$('.select-tree-root:first').first();
instance.component = component;
// Force to hardware acceleration to move element if browser supports translate property
instance.useTransform = OHIF.ui.styleProperty.check('transform', 'translate(1px, 1px)');
// Set the margin property and width
const isthreeColumns = instance.data.threeColumns;
if (!rootInstance.marginProperty) {
rootInstance.marginProperty = isthreeColumns ? 'margin-left' : 'margin-right';
rootInstance.marginWidth = isthreeColumns ? $treeRoot.width() / 2 : $treeRoot.width();
}
// Define a function to set the margin and toggle common section
instance.setMargin = isHidden => {
const marginWidth = isHidden ? '' : rootInstance.marginWidth;
$treeRoot.children('.tree-content').css(rootInstance.marginProperty, marginWidth);
};
// Set the margin to display the common section
if (!$treeRoot.hasClass('started')) {
instance.setMargin(false);
}
// Make the component respect the window boundaries
if (rootComponent === component) {
$treeRoot.bounded();
}
// Check if the component will be rendered on a specific position
const position = instance.data.position;
if (component === rootComponent && position) {
// Get the dimensions and move it with the given position as its center
const width = $treeRoot.outerWidth();
const height = $treeRoot.outerHeight();
position.left -= Math.round(width / 2);
position.top -= Math.round(height / 2);
// Change the component's position and trigger the bounded event
$treeRoot.css(_.extend({}, position, {
position: 'fixed'
})).trigger('spatialChanged');
}
Meteor.defer(() => {
// Start the component transitions
$treeRoot.addClass('started');
// Focus the search box
$treeRoot.find('.tree-search input').focus();
});
// Update the component's viewport height
instance.updateHeight = _.throttle(searchTerm => {
let height;
// Check if there's a search term
if (searchTerm) {
// Set the viewport height as same as listed options height
height = rootInstance.$('.tree-inputs').height();
} else {
// Set the viewport height as same as selected node's options height
height = rootInstance.$('.tree-options:last').data('height');
}
// Update the viewport's height
rootInstance.$('.tree-options:first').first().height(height);
}, 100, { trailing: false });
// Update the opened node
instance.updateOpen = () => {
rootInstance.$('.select-tree').removeClass('open');
rootInstance.$('.select-tree:last').addClass('open');
};
// Put the component in selected state
instance.setSelected = (flag) => {
if (flag) {
$treeRoot.addClass('selected');
} else {
$treeRoot.removeClass('selected');
}
};
// Store the element's initial height
const $treeOptions = instance.$('.tree-options:first').first();
const height = $treeOptions.height();
$treeOptions.data('height', height);
$treeOptions.off('transitionend').on('transitionend', event => {
if (event.target !== event.currentTarget) return;
$treeRoot.trigger('spatialChanged');
});
instance.autorun(() => {
// Run this computation everytime the current node is changed
instance.data.currentNode.dep.depend();
// Run this computation everytime the search term is changed
const searchTerm = instance.searchTerm.get();
// Clean the current node selection if the user started the search
if (searchTerm) {
rootInstance.data.currentNode.set(null);
}
// Update the viewport height
Tracker.afterFlush(() => {
instance.updateHeight(searchTerm);
instance.updateOpen();
});
});
const selector = '.select-tree:first>.tree-content>.tree-options>.tree-breadcrumb .path-link';
instance.$(selector).css('width', 0).each((index, element) => {
transition({
element: element,
prop: 'width',
style: 'width 0.3s ease',
val: 'auto'
});
});
});
Template.selectTree.events({
'input .tree-search input'(event, instance) {
const $target = $(event.currentTarget);
// Get the tree root
const $treeRoot = $target.closest('.select-tree-root');
// Get the search term
const searchTerm = $target.val();
// Change the search term to update the tree items
instance.searchTerm.set(searchTerm);
// Change the component state
const method = searchTerm ? 'addClass' : 'removeClass';
$treeRoot[method]('navigated');
instance.setMargin(!!searchTerm);
},
'keydown .tree-search input'(event, instance) {
// Get the search term
const searchTerm = $(event.currentTarget).val().trim();
const $labels = instance.$('.tree-inputs:first>label');
// Select the label if ENTER was pressed and there is only one result
if (searchTerm.length && $labels.length === 1 && event.which === 13) {
$labels.find('input').click();
}
},
'change .select-tree:first>.tree-content>.tree-options>.tree-inputs>label>input'(event, instance) {
const component = instance.component;
const $target = $(event.currentTarget);
const $treeRoot = $target.closest('.select-tree-root');
const $label = $target.closest('label');
const eventComponent = $target.data('component');
const rootComponent = instance.data.root || component;
const rootInstance = rootComponent.templateInstance;
const offsetTop = $target.closest('.wrapperLabel').offset().top;
// Change the component's node
component.node(eventComponent.value());
// Unset the active leaf
rootInstance.$('label').removeClass('active').css('width', '');
// Unset the selected state
instance.setSelected(false);
// Detect the first interaction with the component and do the animation
instance.setMargin(true);
$treeRoot.addClass('navigated');
// Check if the clicked element is a node or a leaf
const isLeaf = $label.hasClass('tree-leaf');
if (isLeaf) {
// Change the active item
$label.css('width', $label.outerWidth()).addClass('active');
// Ckeck if there's a storageKey defined
const storageKey = instance.data.storageKey;
if (storageKey) {
// Get the current stored data
const storedData = _.extend({}, OHIF.user.getData(storageKey));
// Increment or create a counter for the clicked leaf
const itemKey = OHIF.string.encodeId($target.val());
if (storedData[itemKey]) {
storedData[itemKey]++;
} else {
storedData[itemKey] = 1;
}
// Update the stored data with the new count
OHIF.user.setData(storageKey, storedData);
}
// Set the selected leaf value in the root component
const itemData = $target.data('component').templateInstance.data.itemData;
rootComponent.value(itemData);
// Mark the component as selected
instance.setSelected(true);
} else {
// Get the position of the clicked element
const position = $label.position();
const getPosition = $element => {
if (instance.useTransform) {
const matrixToArray = str => str.match(/(-?[0-9\.]+)/g);
const transformMatrix = matrixToArray($element.css('transform')) || [];
return {
x: parseFloat(transformMatrix[4]) || 0,
y: parseFloat(transformMatrix[5]) || 0
};
} else {
return {
x: parseFloat($element.css('left')),
y: parseFloat($element.css('top'))
};
}
};
const setPosition = ($element, position) => {
if (instance.useTransform) {
const translation = `translate(${position.x}px, ${position.y}px)`;
OHIF.ui.styleProperty.set($element[0], 'transform', translation);
} else {
$element.css('left', `${position.x}px`);
$element.css('top', `${position.y}px`);
}
};
Meteor.defer(() => {
const $treeNode = $label.parent().siblings('.select-tree');
// Do the transition from the clicked position to top
$treeNode.css(position);
setTimeout(() => $treeNode.css('top', 0));
const optionsTop = $target.closest('.tree-options').position().top;
const treeOffsetTop = $treeRoot.offset().top;
const treeRootPosition = getPosition($treeRoot);
treeRootPosition.y += offsetTop - treeOffsetTop - optionsTop;
setPosition($treeRoot, treeRootPosition);
});
}
},
'click .select-tree:first>.tree-content>.tree-options>.tree-breadcrumb .tree-back'(event, instance) {
const $target = $(event.currentTarget);
const rootComponent = instance.data.root || instance.component;
const rootInstance = rootComponent.templateInstance;
// Prevent the default element action
event.preventDefault();
// Reset the checked inputs to unchecked state
rootInstance.$('input').removeAttr('checked');
// Unset the selected state
instance.setSelected(false);
instance.setMargin(false);
$target.closest('.select-tree-root').removeClass('navigated');
// Get the index of the breadcrumb's clicked option
const index = parseInt($target.attr('data-index'));
// Set the current instance
let currentInstance = instance.component.parent.templateInstance;
// Iterate over all parents until gets into the clicked node
for (let i = 1; i <= index; i++) {
// Unset the selected node
currentInstance.data.currentNode.set(null);
// Check if it's not the root node
if (i < index) {
// Change the current instance
currentInstance = currentInstance.component.parent.templateInstance;
}
}
}
});
Template.selectTree.helpers({
treeItems() {
const instance = Template.instance();
// Run this computation everytime the search term is changed
const searchTerm = instance.searchTerm.get();
// Get all the items
let items = instance.data.items;
// Check if the search term was informed
if (searchTerm) {
// Search the items by the give search term
items = OHIF.string.search(items, searchTerm, 'label');
// Filter only the tree leaves
items = _.filter(items, item => !item.items);
} else if (instance.data.hideCommon) {
// Get the values of common items
const commonValues = _.pluck(instance.getCommonItems(), 'value');
// Filter only the items that are not common
items = _.filter(items, item => !_.contains(commonValues, item.value));
}
// Return the items sorted for tree columns
if (instance.data.threeColumns) {
const begin = items.splice(0, Math.ceil(items.length / 2));
const sortedItems = [];
_.each(begin, (item, index) => {
sortedItems.push(item);
if (items[index]) {
sortedItems.push(items[index]);
}
});
items = sortedItems;
}
// Return the tree items
return items;
}
});
@@ -0,0 +1,264 @@
@import "{ohif:viewerbase}/app"
// TODO: [design] can't we use colors that are already in common pallete?
$gray1 = #A3A3A3
$gray2 = #A0A0A0
$gray3 = #C4C4C4
$gray4 = #C0C0C0
$gray5 = #9B9B9B
$gray6 = #303030
.select-tree-root
.tree-content
background-color: white
position: relative
transition(transform 0.3s ease\, background-color 0.3s ease\, border-color 0.3s ease\, border-radius 0.3s ease\, margin-right 0.3s ease)
.tree-search
a.tree-back
.tree-breadcrumb span
.tree-node
.tree-leaf
transition(all 0.3s ease)
&.selected
label.tree-leaf.active
box-shadow: 0 0 0 10px white, inset 0 0 0 200px white
.tree-content
background-color: transparent
&>.tree-content
border-color: transparent
border-radius: 0
.tree-search
background-color: transparent
border-color: transparent
color: transparent
input,
i
opacity: 0
&, .select-tree
&:not(.open)>.tree-content
&>.tree-inputs
&>.tree-breadcrumb
display: none
&>.tree-content>.tree-options
&>.tree-inputs>label:not(.active)
&>.tree-breadcrumb a
&>.tree-breadcrumb span
color: transparent
&>.tree-inputs>label.tree-node:not(.active)
border-color: transparent
&.common-clicked .tree-leaf.active
background-color: transparent
box-shadow: none
color: transparent
.select-tree-root
display: inline-block
position: relative
z-index: 0
&>.tree-content
border: 1px solid $gray3
border-radius: 5px
overflow: hidden
&>.tree-content>.tree-options
transform(scale(1))
transition(height 0.3s ease)
.tree-search
background-color: $gray4
border-bottom: 1px solid $gray3
color: $gray6
display: flex
margin: 0
padding: 5px
position: relative
text-align: center
span
font-weight: bold
line-height: 24px
input
border-color: $gray5
font-weight: normal
padding-left: 34px
transition(opacity 0.3s ease)
& ~ i
color: $gray1
font-size: 18px
left: 16px
position: absolute
top: 37px
transition(opacity 0.3s ease)
.tree-breadcrumb
line-height: 32px
padding: 5px 12px
.path-link, .tree-current-node
display: inline-block
font-weight: normal
float: left
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
i
margin-right: 5px
.select-tree-node
left: 0
overflow: hidden
position: fixed
width: 100%
z-index: 2
&
.tree-content
.tree-inputs
height: 100%
.tree-options
overflow: hidden
position: relative
z-index: 2
.tree-node, .tree-leaf
cursor: pointer
display: block
font-weight: normal
margin-bottom: 0
padding: 0 12px
position: relative
z-index: 1
&>input
display: none
.tree-node
height: 41px
border-bottom: 1px solid $gray3
border-top: 1px solid $gray3
line-height: 41px
margin-top: -1px
&:last-child
border-bottom: 0
.tree-leaf
box-shadow: 0 0 0 200px transparent, inset 0 0 0 200px transparent
line-height: 26px
&.active
box-shadow: 0 0 0 200px white, inset 0 0 0 200px white
transition(box-shadow 0.3s ease)
z-index: 0
&:last-child:after
display: block
content: ''
height: 10px
&.collapsed
z-index: 1
.select-tree-common
height: 100%
padding-left: 6px
position: absolute
right: 0
top: 0
width: 50%
.content
background-color: $gray6
border: 1px solid $gray5
border-radius: 5px
color: white
opacity: 0.9
padding: 0 15px 12px
position: absolute
top: 50%
transform(translateY(-50%))
width: calc(100% - 6px)
h5.title
color: $gray5
font-weight: normal
line-height: 20px
margin: 12px 0 16px
div.labels
position: relative
label
cursor: pointer
display: block
font-weight: 300
line-height: 26px
margin: 0
padding-left: 3px
transition(padding 0.3s ease)
&:hover
padding-left: 0
.select-tree-root
&>.tree-content
transform(scale(0))
transform-origin(100% 50%)
.select-tree-common
z-index: -1
.content
margin-left: calc(-100% - 6px)
transform(scale(0))
transition(margin-left 0.3s ease 0.3s\, transform 0s linear 0.3s)
&.started
&>.tree-content
transform(scale(1))
.select-tree-common
.content
margin-left: 0
transform(scale(1) translateY(-50%))
&.navigated, &.selected
.select-tree-common
.content
opacity: 0
transform(translateX(-100%) scale(0))
transition(transform 0.3s ease\, opacity 0.3s ease)
@keyframes selectTreeCommonOverflow {
from {
overflow: hidden
}
to {
overflow: visible
}
}
@@ -0,0 +1,12 @@
<template name="selectTreeCommon">
<div class="select-tree-common">
<div class="content">
<h5 class="title">Common</h5>
<div class="labels">
{{#each item in items}}
<label for="{{this.storageKey}}_{{encodeId item.value}}">{{item.label}}</label>
{{/each}}
</div>
</div>
</div>
</template>
@@ -0,0 +1,44 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { Tracker } from 'meteor/tracker';
Template.selectTreeCommon.events({
'click .select-tree-common label'(event, instance) {
event.preventDefault();
// Get the clicked label
const $target = $(event.currentTarget);
// Store the offset position for further use
$target.data('offset', $target.offset());
// Build the input selector based on the label target
const inputSelector = '#' + $target.attr('for');
// Get the tree instance
const treeInstance = instance.data.treeInstance;
// Check if the input is not rendered
if (!$(inputSelector).length) {
// Change the search terms with the clicked label string
treeInstance.$('.tree-search input').val($target.text()).trigger('input');
// Wait for options rerendering
Tracker.afterFlush(() => {
const $treeRoot = treeInstance.$('.select-tree-root:first').first();
$treeRoot.addClass('common-clicked');
// Wait for components initialization
Meteor.defer(() => $(inputSelector).click());
});
}
}
});
Template.selectTreeCommon.helpers({
// Get the common items
items() {
return Template.instance().data.treeInstance.getCommonItems();
}
});
@@ -0,0 +1,15 @@
import './serverInformationDicomWeb/serverInformationDicomWeb.html';
import './serverInformationDicomWeb/serverInformationDicomWeb.js';
import './serverInformationDimse/serverInformationDimse.html';
import './serverInformationDimse/serverInformationDimse.js';
import './serverInformationForm/serverInformationForm.html';
import './serverInformationForm/serverInformationForm.js';
import './serverInformationList/serverInformationList.html';
import './serverInformationList/serverInformationList.js';
import './serverInformationModal/serverInformationModal.html';
import './serverInformationModal/serverInformationModal.styl';
import './serverInformationModal/serverInformationModal.js';
@@ -0,0 +1,57 @@
<template name="serverInformationDicomWeb">
<div class="row">
<div class="col-lg-6">
{{>inputText labelClass='form-group' key='wadoUriRoot'}}
</div>
<div class="col-lg-6">
{{>inputSelect labelClass='form-group' key='imageRendering' hideSearch=true
options=(clone placeholder='Select an option')}}
</div>
<div class="col-lg-6">
{{>inputText labelClass='form-group' key='wadoRoot'}}
</div>
<div class="col-lg-6">
{{>inputSelect labelClass='form-group' key='thumbnailRendering' hideSearch=true
options=(clone placeholder='Select an option')}}
</div>
<div class="col-lg-6">
{{>inputText labelClass='form-group' key='qidoRoot'}}
</div>
<div class="col-lg-6">
<strong>QIDO options</strong>
<div class="form-group">
<div class="form-control">
<div class="checkbox">
{{>inputCheckbox key='qidoSupportsIncludeField'}}
</div>
</div>
</div>
</div>
</div>
<hr>
<h4>Request options</h4>
{{#group class='row p-y-1' key='requestOptions'}}
<div class="col-lg-6">
{{>inputText labelClass='form-group' key='auth'}}
<div class="checkbox pull-left">
{{>inputCheckbox key='requestFromBrowser'}}
</div>
</div>
<div class="col-lg-6">
<strong>Logging options</strong>
<div class="form-group">
<div class="form-control clearfix">
<div class="checkbox pull-left">
{{>inputCheckbox key='logRequests'}}
</div>
<div class="checkbox pull-left">
{{>inputCheckbox key='logResponses'}}
</div>
<div class="checkbox pull-left">
{{>inputCheckbox key='logTiming'}}
</div>
</div>
</div>
</div>
{{/group}}
</template>
@@ -0,0 +1,12 @@
import { Template } from 'meteor/templating';
Template.serverInformationDicomWeb.onRendered(() => {
const instance = Template.instance();
instance.autorun(function() {
const mode = instance.data.mode.get();
if (mode === 'edit') {
const data = instance.data.currentItem.get();
instance.data.form.value(data);
}
});
});
@@ -0,0 +1,60 @@
<template name="serverInformationDimse">
<div class="row">
<div class="col-lg-6">
{{>inputText labelClass='form-group' key='wadoUriRoot'}}
</div>
</div>
<div class="panel panel-default panel-body">
<div class="clearfix">
<h4 class="pull-left">Peer List</h4>
{{#button class='btn btn-primary pull-right'
action='newPeer' params=peer
}}New peer{{/button}}
</div>
<hr>
{{#group class='row' key='peers' arrayValues=true}}
{{#each peer in instance.peers.get}}
{{#group class='col-lg-6'}}
<div class="panel panel-default panel-body">
<div class="row">
<div class="col-lg-6">
{{>inputText class='form-control' key='aeTitle'}}
</div>
<div class="col-lg-6">
{{>inputText class='form-control' key='hostAE'}}
</div>
<div class="col-lg-6">
{{>inputText class='form-control' key='host'}}
</div>
<div class="col-lg-6">
{{>inputText class='form-control' key='port'}}
</div>
<div class="col-lg-12">
<strong>Settings</strong>
<div class="form-group">
<div class="form-control height-auto clearfix">
<div class="checkbox pull-left">
{{>inputCheckbox key='default'}}
</div>
<div class="checkbox pull-left">
{{>inputCheckbox key='server'}}
</div>
<div class="checkbox pull-left">
{{>inputCheckbox key='supportsInstanceRetrievalByStudyUid'}}
</div>
</div>
</div>
</div>
<div class="col-lg-12 text-right">
{{#button class='btn btn-sm btn-danger'
action='removePeer' params=@index
}}Remove{{/button}}
</div>
</div>
</div>
{{/group}}
{{/each}}
{{/group}}
</div>
<hr>
</template>
@@ -0,0 +1,44 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import _ from 'underscore';
Template.serverInformationDimse.onCreated(() => {
const instance = Template.instance();
instance.peers = new ReactiveVar([]);
instance.autorun(() => {
const currentItem = instance.data.currentItem.get();
if (currentItem) {
instance.peers.set(currentItem.peers || []);
}
});
const api = instance.data.api;
_.extend(api, {
newPeer() {
const peers = instance.peers.get();
peers.push({});
instance.peers.set(peers);
},
removePeer(peerIndex) {
const peers = instance.peers.get();
peers.splice(peerIndex, 1);
instance.peers.set(peers);
const data = instance.data.currentItem.get();
data.peers = peers;
Tracker.afterFlush(() => instance.data.form.value(data));
}
});
});
Template.serverInformationDimse.onRendered(() => {
const instance = Template.instance();
instance.autorun(() => {
const mode = instance.data.mode.get();
if (mode === 'edit') {
const data = instance.data.currentItem.get();
instance.data.form.value(data);
}
});
});
@@ -0,0 +1,27 @@
<template name="serverInformationForm">
{{#form class='server-information-form' schema=instance.currentSchema api=this.api}}
{{>inputHidden key='_id'}}
<div class="row">
<div class="col-lg-6">
{{>inputText labelClass='form-group' key='name'}}
</div>
<div class="col-lg-6">
{{>inputSelect labelClass='form-group' key='type'
hideSearch=true disabled=(eq this.mode.get 'edit')
options=(clone placeholder='Choose a server type')}}
</div>
</div>
<hr>
{{#if eq this.serverType.get 'dicomWeb'}}
{{>serverInformationDicomWeb this}}
{{/if}}
{{#if eq this.serverType.get 'dimse'}}
{{>serverInformationDimse this}}
{{/if}}
{{#if this.serverType.get}}
<div>
{{#button class='btn btn-primary' action='save'}}Save{{/button}}
</div>
{{/if}}
{{/form}}
</template>
@@ -0,0 +1,74 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { OHIF } from 'meteor/ohif:core';
import { DICOMWebServer as dicomSchema } from 'meteor/ohif:servers/both/schema/servers.js';
import { DIMSEServer as dimseSchema } from 'meteor/ohif:servers/both/schema/servers.js';
Template.serverInformationForm.onCreated(() => {
const instance = Template.instance();
instance.data.api = {
save() {
// Stop here if the form validation fails
if (!instance.data.form.validate()) {
return;
}
// Get the current form data
const formData = instance.data.form.value();
// Call the save method
Meteor.call('serverSave', formData, function(error) {
if (error) {
// TODO: check for errors: not-authorized, data-write
OHIF.log.error(error);
}
instance.data.resetState();
});
}
};
instance.currentSchema = new ReactiveVar(dicomSchema);
});
Template.serverInformationForm.onRendered(() => {
const instance = Template.instance();
instance.data.$form = instance.$('form').first();
instance.data.form = instance.data.$form.data('component');
// Handle the server type
instance.autorun(() => {
// Get the server type component
const typeComponent = instance.$('[data-key=type] :input').data('component');
// Run this computation every time the user change the server type
typeComponent.depend();
// Get the current server type value
const type = typeComponent.value();
// Set the serverType reactive value
instance.data.serverType.set(type);
// Change the schema based on the selected server type
if (type === 'dimse') {
instance.currentSchema.set(dimseSchema);
} else {
instance.currentSchema.set(dicomSchema);
}
});
// Handle the form mode (edit or add)
instance.autorun(() => {
const mode = instance.data.mode.get();
// Check if it is on edit mode and load the saved data
if (mode === 'edit') {
const data = instance.data.currentItem.get();
instance.data.form.value(data);
}
});
});
@@ -0,0 +1,45 @@
<template name="serverInformationList">
{{#form api=instance.api}}
<table id="tblServer" class="table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
{{#each server in servers}}
<tr>
<td>{{server.name}}</td>
<td>{{server.type}}</td>
<td class="text-right">
<div class="btn-group" role="group" aria-label="Actions">
{{#button class='btn btn-sm btn-default' action='use' params=server title='Use this server' disabled=(isActive server)}}
{{#if isActive server}}
<i class="fa fa-check-square-o"></i>
{{else}}
<i class="fa fa-square-o"></i>
{{/if}}
{{/button}}
{{#button class='btn btn-sm btn-default' action='edit' params=server title='Edit' disabled=(eq server.origin 'json')}}
<i class="fa fa-pencil"></i>
{{/button}}
{{#button class='btn btn-sm btn-danger' action='delete' params=server title='Remove' disabled=(eq server.origin 'json')}}
<i class="fa fa-trash"></i>
{{/button}}
</div>
</td>
</tr>
{{else}}
<tr>
<td class="text-center" colspan="3">No servers found</td>
</tr>
{{/each}}
</tbody>
</table>
{{#button class='btn btn-sm btn-default' action='add'}}
<i class="fa fa-plus"></i> Add a new server
{{/button}}
{{/form}}
</template>
@@ -0,0 +1,54 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { Servers, CurrentServer } from 'meteor/ohif:servers/both/collections';
Template.serverInformationList.onCreated(() => {
const instance = Template.instance();
instance.api = {
add: () => instance.data.mode.set('create'),
edit(server) {
instance.data.currentItem.set(server);
instance.data.mode.set('edit');
},
delete(server) {
// TODO: Replace this for confirmation dialog after LT-refactor is merged back to master
if (!window.confirm('Are you sure you want to remove this peer?')) {
return;
}
Meteor.call('serverRemove', server._id, error => {
// TODO: check for errors: data-write
});
},
use(server) {
Meteor.call('serverSetActive', server._id, error => {
// TODO: check for errors: data-write
});
}
};
});
Template.serverInformationList.helpers({
isActive: function(server) {
const currentServer = CurrentServer.findOne();
if (!currentServer) {
return;
}
return server._id === currentServer.serverId;
},
servers: function() {
const options = {
sort: {
origin: -1,
type: 1,
name: 1
}
};
return Servers.find({}, options).fetch();
}
});
@@ -0,0 +1,12 @@
<template name="serverInformationModal">
{{#dialogSimple (extend this dialogClass='modal-lg' title='Server Information')}}
{{#if (eq instance.container.mode.get "list")}}
{{>serverInformationList instance.container}}
{{else}}
{{>serverInformationForm instance.container}}
{{/if}}
{{#section 'dialogFooter'}}{{#unless (eq instance.container.mode.get "list")}}{{!
}}<button type="button" class="btn btn-default js-back">Back</button>{{!
}}{{/unless}}{{/section}}
{{/dialogSimple}}
</template>
@@ -0,0 +1,23 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
Template.serverInformationModal.onCreated(function() {
const instance = Template.instance();
instance.container = {
mode: new ReactiveVar('list'),
serverType: new ReactiveVar(null),
currentItem: new ReactiveVar(null),
$form: null,
resetState: function() {
instance.container.mode.set('list');
instance.container.serverType.set(null);
instance.container.currentItem.set(null);
}
};
});
Template.serverInformationModal.events({
'click .js-back'(event, instance) {
instance.container.resetState();
}
});
@@ -0,0 +1,5 @@
.server-information-form
label.wrapper
display: block
.checkbox
margin: 0 8px 0 0
@@ -0,0 +1,3 @@
import './themeSelectorModal/themeSelectorModal.html';
import './themeSelectorModal/themeSelectorModal.styl';
import './themeSelectorModal/themeSelectorModal.js';
@@ -0,0 +1,17 @@
<template name="themeSelectorModal">
{{#dialogForm (extend this
id='themeSelectorModal' dialogClass='themed modal-lg' confirmLabel='Apply theme'
title='Themes' titleIcon='fa fa-sliders'
)}}
<ul class="theme-list text-center">
{{#each theme in themes}}
<li class="text-center preview-theme {{printClassIfSelected theme}}" data-theme="{{theme}}">
<div class="icon-wrapper">
<span class="theme-icon-{{theme}}"></span>
</div>
<h4 class="theme-title">{{ucFirst theme}}</h4>
</li>
{{/each}}
</ul>
{{/dialogForm}}
</template>
@@ -0,0 +1,80 @@
import { Template } from 'meteor/templating';
import { ReactiveDict } from 'meteor/reactive-dict';
const DEFAULT_THEME = 'tide';
const getCurrentTheme = () => sessionStorage.getItem('theme');
const setActualTheme = theme => sessionStorage.setItem('theme', theme);
const addThemeToBody = theme => document.body.classList.add('theme-' + theme);
const removeThemesFromBody = () => {
const classList = document.body.classList;
for (let i = classList.length - 1; i >= 0; i--) {
if (classList[i].search('theme-') !== -1) {
document.body.classList.remove(classList[i]);
}
}
};
Template.themeSelectorModal.onCreated(() => {
const instance = Template.instance();
let currentTheme = getCurrentTheme();
if (!currentTheme) {
currentTheme = DEFAULT_THEME;
setActualTheme(currentTheme);
}
instance.state = new ReactiveDict();
instance.state.set('selectedTheme', currentTheme);
addThemeToBody(currentTheme);
instance.container = {
currentTheme,
previewTheme(theme, state) {
state.set('selectedTheme', theme);
removeThemesFromBody();
addThemeToBody(theme);
},
applyTheme(state) {
setActualTheme(state.get('selectedTheme'));
},
resetState(state) {
const theme = getCurrentTheme();
state.set('selectedTheme', theme);
removeThemesFromBody();
addThemeToBody(theme);
}
};
const { promise } = instance.data;
promise.then(() => instance.container.applyTheme(instance.state));
promise.catch(() => instance.container.resetState(instance.state));
});
Template.themeSelectorModal.helpers({
themes: [ 'crickets', 'honeycomb', 'mint', 'overcast', 'quartz', 'tide', 'tigerlily' ],
ucFirst(text) {
return text.charAt(0).toUpperCase() + text.slice(1);
},
printClassIfSelected(theme) {
const instance = Template.instance();
return theme === instance.state.get('selectedTheme') ? 'selected' : '';
}
});
Template.themeSelectorModal.events({
'click .preview-theme'(event, instance) {
instance.container.previewTheme(event.currentTarget.dataset.theme, instance.state);
}
});
@@ -0,0 +1,36 @@
@import "{ohif:viewerbase}/app.styl"
#themeSelectorModal
.theme-list
list-style: none
padding: 0
li
display: inline-block
cursor: pointer
margin-right: 10px
margin-bottom: 10px
padding: 10px
width: 110px
theme('border', '1px solid $uiBorderColorDark')
&:hover
background-color: rgba(21, 25, 30, 0.7)
&:last-child
margin-right: 0
&.selected
theme('border', '3px solid $uiBorderColorActive')
padding: 7px;
.icon-wrapper
height: 64px
width: 64px
margin: 0 auto
line-height: 6.5
.theme-title
font-weight: normal
font-size: 1.1em
margin-bottom: 0
@@ -1,4 +1,4 @@
@import "{ohif:design}/app"
@import "{ohif:viewerbase}/app"
#imageViewerViewports
height: 100%
@@ -1,4 +1,4 @@
@import "{ohif:design}/app"
@import "{ohif:viewerbase}/app"
#viewer
height: "calc(100% - %s)" % $topBarHeight
@@ -1,4 +1,4 @@
@import "{ohif:design}/app"
@import "{ohif:viewerbase}/app"
.annotationDialog
theme('border', '1px solid $uiBorderColor', 0.95)
@@ -2,8 +2,8 @@ import { Template } from 'meteor/templating';
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';
import { _ } from 'meteor/underscore';
import { $ } from 'meteor/jquery';
import _ from 'underscore';
import $ from 'jquery';
import { OHIF } from 'meteor/ohif:core';
import { cornerstoneTools } from 'meteor/ohif:cornerstone';
import { viewportUtils } from '../../../lib/viewportUtils';
@@ -1,4 +1,4 @@
@require '{ohif:design}/app'
@require '{ohif:viewerbase}/app'
#cineDialog
theme('background', '$uiGrayDarkest', 0.85)
@@ -2,7 +2,7 @@ import { Template } from 'meteor/templating';
import { Tracker } from 'meteor/tracker';
import { ReactiveVar } from 'meteor/reactive-var';
import { Session } from 'meteor/session';
import { $ } from 'meteor/jquery';
import $ from 'jquery';
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
import { OHIF } from 'meteor/ohif:core';
import { cornerstone, cornerstoneTools } from 'meteor/ohif:cornerstone';
@@ -1,4 +1,4 @@
@require '{ohif:design}/app'
@require '{ohif:viewerbase}/app'
#imageDownloadDialog
Loaded 100 of 479 files, more files were not shown because too many files have changed in this diff. Show more