This commit is contained in:
Rob Lewis 2016-08-17 16:31:15 -04:00
commit 4e164215ea
56 changed files with 955 additions and 262 deletions

View File

@ -12,6 +12,9 @@
<meta name="HandheldFriendly" content="True">
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- Make SVG work on IE -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link href='http://fonts.googleapis.com/css?family=Roboto:400,100,100italic,300,300italic,400italic,500,500italic,700,700italic,900,900italic&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Sanchez:400,700&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
</head>

View File

@ -12,7 +12,7 @@ Template.toolbarSection.helpers({
svgLink: '/packages/viewerbase/assets/icons.svg#icon-studies',
svgWidth: 15,
svgHeight: 13,
bottomLabel: 'Studies'
bottomLabel: 'Series'
}]
};
},

View File

@ -1,3 +1,4 @@
@import "{design}/styles/imports/animations"
@import "{design}/styles/imports/mixins"
@import "{design}/styles/imports/spacings"
@import "{design}/styles/imports/variables"

View File

@ -26,6 +26,7 @@ Package.onUse(function(api) {
// Common styles
api.addFiles([
'styles/common/webfonts.styl',
'styles/common/keyframes.styl',
'styles/common/global.styl',
'styles/common/spacings.styl'
], 'client');

View File

@ -0,0 +1,19 @@
@import "{design}/app.styl"
@keyframes zoomIn
from
transform(scale(0))
to
transform(scale(1))
@keyframes fadeIn
from
opacity: 0
to
opacity: 1
@keyframes fadeOut
from
opacity: 1
to
opacity: 0

View File

@ -13,8 +13,8 @@ $gray6 = #303030
.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)
&
.tree-search
a.tree-back
.tree-breadcrumb span
@ -146,6 +146,7 @@ $gray6 = #303030
&.active
box-shadow: 0 0 0 200px white, inset 0 0 0 200px white
position: fixed
transition(box-shadow 0.3s ease)
z-index: 0
@ -192,7 +193,6 @@ $gray6 = #303030
&>.tree-content
transform(scale(0))
transform-origin(100% 50%)
transition(all 0.3s ease)
.select-tree-common
overflow: hidden

View File

@ -0,0 +1,17 @@
animationDefaults()
animation-duration: 0.3s
animation-direction: alternate
animation-timing-function: ease-out
animation-fill-mode: forwards
animateZoomIn()
animationDefaults()
animation-name: zoomIn
animateFadeIn()
animationDefaults()
animation-name: fadeIn
animateFadeOut()
animationDefaults()
animation-name: fadeOut

View File

@ -12,15 +12,18 @@ var getInstanceRetrievalParams = function(studyInstanceUID, seriesInstanceUID) {
return {
0x0020000D: studyInstanceUID ? studyInstanceUID : '',
0x0020000E: (studyInstanceUID && seriesInstanceUID) ? seriesInstanceUID : '',
0x00080005: '',
0x00080020: '',
0x00080030: '',
0x00080090: '',
0x00100010: '',
0x00100020: '',
0x00200010: '',
0x0008103E: '',
0x00200011: '',
0x00080005: '', // specificCharacterSet
0x00080020: '', // studyDate
0x00080030: '', // studyDescription
0x00080090: '', // referringPhysicianName
0x00100010: '', // patientName
0x00100020: '', // patientId
0x00100030: '', // patientBirthDate
0x00100040: '', // patientSex
0x00200010: '', // studyId
0x0008103E: '', // seriesDescription
0x00200011: '', // seriesNumber
0x00080080: '', // institutionName
0x00080016: '', // sopClassUid
0x00080018: '', // sopInstanceUid
0x00080060: '', // modality
@ -37,7 +40,9 @@ var getInstanceRetrievalParams = function(studyInstanceUID, seriesInstanceUID) {
0x00281052: '', // rescaleIntercept
0x00281053: '', // rescaleSlope
0x00280002: '', // samplesPerPixel
0x00180050: '', // sliceThickness
0x00201041: '', // sliceLocation
0x00189327: '', // tablePosition
0x00281050: '', // windowCenter
0x00281051: '', // windowWidth
0x00280030: '', // pixelSpacing
@ -47,9 +52,14 @@ var getInstanceRetrievalParams = function(studyInstanceUID, seriesInstanceUID) {
0x00200032: '', // imagePositionPatient
0x00200037: '', // imageOrientationPatient
0x00200052: '', // frameOfReferenceUID
0x00282110: '', // lossyImageCompression
0x00282112: '', // lossyImageCompressionRatio
0x00282114: '', // lossyImageCompressionMethod,
0x00180088: '' // spacingBetweenSlices
// Orthanc has a bug here so we can't retrieve sequences at the moment
// https://groups.google.com/forum/#!topic/orthanc-users/ghKJfvtnK8Y
//0x00282111: '', // derivationDescription
//0x00082112: '' // sourceImageSequence
};
};

View File

@ -0,0 +1,102 @@
const hasValueAtTimepoint = timepointId => {
return measurement => {
if (measurement.timepoints[timepointId]) {
return true;
}
};
};
const hasNoValueAtTimepoint = timepointId => {
return measurement => {
if (measurement.timepoints[timepointId] === undefined) {
return true;
}
};
};
export const MeasurementApi = {
sortOptions: {
sort: {
lesionNumberAbsolute: 1
}
},
// Return all Measurements
all(withPriors=false) {
let data = Measurements.find({}, this.sortOptions).fetch();
// If we don't have a prior for this Timepoint,
// this filter, we should just return all of the
// available Non-Targets measurements
if (this.priorTimepointId && withPriors === true) {
return data.filter(hasValueAtTimepoint(this.priorTimepointId))
}
return data;
},
unmarked() {
const withPriors = true;
return this.all(withPriors).filter(hasNoValueAtTimepoint(this.currentTimepointId));;
},
unmarkedTargets() {
const withPriors = true;
return this.targets(withPriors).filter(hasNoValueAtTimepoint(this.currentTimepointId));;
},
unmarkedNonTargets() {
const withPriors = true;
return this.nonTargets(withPriors).filter(hasNoValueAtTimepoint(this.currentTimepointId));;
},
// Return only Target Measurements
targets(withPriors=false) {
let data = Measurements.find({
isTarget: true
}, this.sortOptions).fetch();
// If we don't have a prior for this Timepoint,
// this filter, we should just return all of the
// available Non-Targets measurements
if (this.priorTimepointId && withPriors === true) {
return data.filter(hasValueAtTimepoint(this.priorTimepointId))
}
return data;
},
// Return only Non-Target Measurements
nonTargets(withPriors=false) {
let data = Measurements.find({
isTarget: false
}, this.sortOptions).fetch();
// If we don't have a prior for this Timepoint,
// this filter, we should just return all of the
// available Non-Targets measurements
if (this.priorTimepointId && withPriors === true) {
return data.filter(hasValueAtTimepoint(this.priorTimepointId))
}
return data;
},
// Return only New Lesions
newLesions() {
// If we are current editing a Baseline we won't have any priors, so newLesions
// should return an empty array.
if (!this.priorTimepointId) {
return [];
}
// Find only lesions that have no value at the previous timepoint
return this.all().filter(hasNoValueAtTimepoint(this.priorTimepointId));
},
firstLesion() {
return Measurements.findOne({
target: true
}, this.sortOptions);
}
};

View File

@ -19,12 +19,42 @@ class TimepointApi {
return this.timepoints.find().fetch();
}
// Return only the current and prior timepoints
latest() {
const options = {
limit: 2
};
return this.timepoints.find({}, options).fetch();
// Return only the current timepoint
current() {
return this.timepoints.findOne({
timepointId: this.currentTimepointId
});
}
prior() {
const latestDate = this.current().latestDate;
return this.timepoints.findOne({
latestDate: {
$lt: latestDate
}
}, {
sort: {
latestDate: -1
},
});
}
// Return only the current and prior Timepoints
currentAndPrior() {
let timepoints = [this.current()];
const prior = this.prior();
if (prior) {
timepoints.push(prior);
}
return timepoints;
}
// Return only the baseline timepoint
baseline() {
return this.timepoints.findOne({
timepointType: 'baseline'
});
}
// Return only the key timepoints (current, prior, nadir and baseline)

View File

@ -1,45 +1,37 @@
import { MeasurementApi } from 'meteor/lesiontracker/client/api/measurement';
Template.caseProgress.onCreated(() => {
const instance = Template.instance();
instance.progressPercent = new ReactiveVar();
instance.progressText = new ReactiveVar();
instance.isLocked = new ReactiveVar();
if (!instance.data.currentTimepointId) {
const current = instance.data.timepointApi.current();
if (!current.timepointId) {
console.warn('Case has no timepointId');
return;
}
const currentTimepointId = instance.data.currentTimepointId;
const timepoint = Timepoints.findOne({
timepointId: currentTimepointId
});
instance.isLocked.set(current.isLocked);
const timepointType = timepoint.timepointType;
// 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 withPriors = true;
const totalTargets = MeasurementApi.targets(withPriors).length;
instance.isLocked.set(timepoint.isLocked);
if (timepointType === 'baseline') {
// If we're currently reviewing a Baseline timepoint, don't do any
// progress measurement.
if (current.timepointType === 'baseline') {
instance.progressPercent.set(100);
} else {
// 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 totalTargets = Measurements.find({
isTarget: true
}).count();
// Setup a reactive function to update the progress whenever
// a measurement is made
instance.autorun(() => {
// Obtain the number of Measurements for which the current Timepoint has
// no Measurement data
let numRemainingMeasurements = 0;
Measurements.find().forEach(measurement => {
if (!measurement.timepoints[currentTimepointId]) {
numRemainingMeasurements++;
}
});
const numRemainingMeasurements = MeasurementApi.unmarked().length;
// Update the Case Progress text with the remaining measurement count
instance.progressText.set(numRemainingMeasurements);
@ -67,7 +59,7 @@ Template.caseProgress.helpers({
},
progressComplete() {
let progressPercent = Template.instance().progressPercent.get();
const progressPercent = Template.instance().progressPercent.get();
return progressPercent === 100;
}
});

View File

@ -1,3 +1,5 @@
import { MeasurementApi } from 'meteor/lesiontracker/client/api/measurement';
Template.lesionTable.onCreated(() => {
const instance = Template.instance();
@ -13,7 +15,7 @@ Template.lesionTable.onCreated(() => {
if (tableLayout === 'key') {
timepoints = instance.data.timepointApi.key();
} else {
timepoints = instance.data.timepointApi.latest();
timepoints = instance.data.timepointApi.currentAndPrior();
}
// Return key timepoints
@ -47,14 +49,10 @@ Session.setDefault('NewSeriesLoaded', false);
Template.lesionTable.onRendered(() => {
// Find the first measurement by Lesion Number
var firstLesion = Measurements.findOne({}, {
sort: {
lesionNumber: 1
}
});
const firstLesion = MeasurementApi.firstLesion();
// Create an object to store the ContentId inside
var templateData = {
const templateData = {
contentId: Session.get('activeContentId')
};

View File

@ -1,7 +1,7 @@
Template.lesionTableHUD.onCreated(() => {
const instance = Template.instance();
instance.data.timepoints = new ReactiveVar(instance.data.timepointApi.latest());
instance.data.timepoints = new ReactiveVar(instance.data.timepointApi.currentAndPrior());
});
Template.lesionTableHUD.onRendered(() => {

View File

@ -1,14 +1,16 @@
<template name="lesionTableHeaderRow">
<div class="lesionTableHeaderRow">
{{ #if anyUnmarkedLesionsLeft}}
<div class="add js-setTool">
<svg>
<use xlink:href=/packages/viewerbase/assets/icons.svg#icon-ui-add></use>
</svg>
</div>
{{ /if }}
<div class="type">
{{type}}
</div>
<div class="max">
<div class="max {{ # if gt numberOfLesions maxNumLesions }}warning{{/if}}">
{{ #if maxNumLesions }}
<p class="maxNumLesions">
Max {{maxNumLesions}}

View File

@ -1,3 +1,5 @@
import { MeasurementApi } from 'meteor/lesiontracker/client/api/measurement';
const toolTypesById = {
target: 'bidirectional',
nonTarget: 'nonTarget',
@ -14,21 +16,14 @@ Template.lesionTableHeaderRow.onCreated(() => {
const instance = Template.instance();
instance.maxNumLesions = new ReactiveVar();
const current = instance.data.timepointApi.current();
const timepointType = current.timepointType;
if (!instance.data.currentTimepointId) {
console.warn('Case has no timepointId');
return;
}
// Give the header a proper name
instance.data.type = namesById[instance.data.id];
const currentTimepointId = instance.data.currentTimepointId;
const timepoint = Timepoints.findOne({
timepointId: currentTimepointId
});
const timepointType = timepoint.timepointType;
// TODO: Check if we have criteria where maximum limits are applied to
// Non-Targets and/or New Lesions
if (timepointType === 'baseline' && instance.data.id === 'target') {
@ -57,17 +52,35 @@ Template.lesionTableHeaderRow.onCreated(() => {
});
Template.lesionTableHeaderRow.helpers({
numberOfLesions: function() {
const instance = Template.instance();
return instance.data.measurements.count();
type() {
// Give the header a proper name
const id = Template.instance().data.id;
return namesById[id];
},
maxNumLesions: function() {
numberOfLesions() {
return Template.instance().data.measurements.length;
},
maxNumLesions() {
return Template.instance().maxNumLesions.get();
},
anyUnmarkedLesionsLeft() {
const id = Template.instance().data.id;
if (id === 'target') {
return MeasurementApi.unmarkedTargets().length;
} else if (id === 'nonTarget') {
return MeasurementApi.unmarkedNonTargets().length;
}
// Keep the 'Add' button for the New Lesions header row
return true;
}
});
Template.lesionTableHeaderRow.events({
'click .js-setTool': function(event, instance) {
'click .js-setTool'(event, instance) {
const id = instance.data.id;
const toolType = toolTypesById[id];
toolManager.setActiveTool(toolType);

View File

@ -63,7 +63,7 @@ $headerRowHeight = 63px
text-align: right
.maxNumLesions
background-color: $textSecondaryColor;
background-color: $textSecondaryColor
border-radius: 3px
color: black
display: table
@ -75,3 +75,9 @@ $headerRowHeight = 63px
margin-top: 22px
padding: 2px 6px 0
text-transform: uppercase
transition($sidebarTransition)
&.warning
.maxNumLesions
color: $textPrimaryColor
background-color: $uiYellow

View File

@ -1,17 +1,26 @@
<template name="lesionTableView">
<div class="lesionTableView scrollArea">
{{>lesionTableHeaderRow id="target" measurements=targets currentTimepointId=currentTimepointId}}
{{>lesionTableHeaderRow id="target"
measurements=targets
currentTimepointId=currentTimepointId
timepointApi=timepointApi}}
{{#each target in targets}}
{{>lesionTableRow (clone this rowItem=target)}}
{{/each}}
{{>lesionTableHeaderRow id="nonTarget" measurements=nonTargets currentTimepointId=currentTimepointId}}
{{>lesionTableHeaderRow id="nonTarget"
measurements=nonTargets
currentTimepointId=currentTimepointId
timepointApi=timepointApi}}
{{#each nonTarget in nonTargets}}
{{>lesionTableRow (clone this rowItem=nonTarget)}}
{{/each}}
{{#if gt this.timepoints.get.length 1}}
{{>lesionTableHeaderRow id="newLesion" measurements=newLesions currentTimepointId=currentTimepointId}}
{{>lesionTableHeaderRow id="newLesion"
measurements=newLesions
currentTimepointId=currentTimepointId
timepointApi=timepointApi}}
{{#each newLesion in newLesions}}
{{>lesionTableRow (clone this rowItem=newLesion)}}
{{/each}}

View File

@ -1,32 +1,23 @@
import { MeasurementApi } from 'meteor/lesiontracker/client/api/measurement';
Template.lesionTableView.helpers({
targets() {
// All Targets shall be listed first followed by Non-Targets
return Measurements.find({
isTarget: true
}, {
sort: {
lesionNumberAbsolute: 1
}
});
const withPriors = true;
return MeasurementApi.targets(withPriors);
},
nonTargets() {
return Measurements.find({
isTarget: false
}, {
sort: {
lesionNumberAbsolute: 1
}
});
const withPriors = true;
return MeasurementApi.nonTargets(withPriors);
},
newLesions() {
return Measurements.find({
saved: false
}, {
sort: {
lesionNumberAbsolute: 1
}
});
return MeasurementApi.newLesions();
},
isFollowup() {
const instance = Template.instance();
const current = instance.data.timepointApi.current();
return (current && current.timepointType === 'followup');
}
});

View File

@ -1,5 +1,5 @@
<template name="lesionTrackerViewportOverlay">
<div class="imageViewerViewportOverlay noselect">
<div class="imageViewerViewportOverlay noselect {{#if gt numImages 1}}controlsVisible{{/if}}">
<div class="topleft dicomTag">
<div>{{formatPN patientName}}</div>
<div>{{patientId}}</div>
@ -16,11 +16,13 @@
</div>
<div class="bottomleft dicomTag">
<div>Ser: {{seriesNumber}}</div>
<div>Img: {{imageNumber}} ({{imageIndex}}/{{numImages}})</div>
<div>{{#if gt numImages 1}}Img: {{instanceNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
<div>{{frameRate}}</div>
<div>{{imageDimensions}}</div>
<div>{{seriesDescription}}</div>
</div>
{{>imageControls}}
{{#if gt numImages 1}}
{{> imageControls imageIndex=imageIndex numImages=numImages}}
{{ /if }}
</div>
</template>

View File

@ -1,12 +1,27 @@
<template name="measureFlow">
<div class="measure-flow">
<button>Add label</button>
{{>selectTree
key='label'
items=instance.items
label='Assign label'
searchPlaceholder='Search labels'
storageKey='measureLabelCommon'
}}
<div class="measure-flow {{instance.state.get}}" style="position:fixed; top:200px; left:400px">
<button class="btn-add">Add label</button>
{{#if eq instance.state.get 'selected'}}
<div class="tree-leaf">
<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">Rename</button>
<button class="pull-right btn-description">{{valueIf instance.description.get 'Edit' 'Add'}} description</button>
</div>
</div>
{{/if}}
</div>
</template>

View File

@ -1,6 +1,59 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
import { Blaze } from 'meteor/blaze';
import { ReactiveVar } from 'meteor/reactive-var';
import { _ } from 'meteor/underscore';
import { $ } from 'meteor/jquery';
Template.measureFlow.onCreated(() => {
const instance = Template.instance();
instance.state = new ReactiveVar('closed');
instance.description = new ReactiveVar('');
instance.descriptionEdit = new ReactiveVar(false);
instance.items = [{
label: 'Category 1',
value: 'Category 1',
items: [{
label: 'Subcategory 1.1',
value: 'Subcategory 1.1'
}, {
label: 'Subcategory 1.2',
value: 'Subcategory 1.2',
items: [{
label: 'Subcategory 1.2.1',
value: 'Subcategory 1.2.1'
}, {
label: 'Subcategory 1.2.2',
value: 'Subcategory 1.2.2',
items: [{
label: 'Subcategory 1.2.2.1',
value: 'Subcategory 1.2.2.1'
}, {
label: 'Subcategory 1.2.2.2',
value: 'Subcategory 1.2.2.2'
}]
}]
}]
}, {
label: 'Category 2',
value: 'Category 2',
items: [{
label: 'Subcategory 2.1',
value: 'Subcategory 2.1'
}, {
label: 'Subcategory 2.2',
value: 'Subcategory 2.2'
}, {
label: 'Subcategory 2.3',
value: 'Subcategory 2.3'
}]
}, {
label: 'Category 3',
value: 'Category 3'
}];
const items = [
'Adrenal',
'Bladder',
@ -39,12 +92,139 @@ Template.measureFlow.onCreated(() => {
});
});
Template.measureFlow.onRendered(() => {
const instance = Template.instance();
// Make the measure flow bounded by the window borders
instance.$('.measure-flow').bounded();
});
Template.measureFlow.events({
'click .measure-flow .btn-add, click .measure-flow .btn-rename'(event, instance) {
// Set the open state for the component
instance.state.set('open');
// Wait template rerender before rendering the selectTree
Tracker.afterFlush(() => {
// Get the click position
const position = {
left: event.clientX,
top: event.clientY,
};
// Define the data for selectTreeComponent
const data = {
key: 'label',
items: instance.items,
label: 'Assign label',
searchPlaceholder: 'Search labels',
storageKey: 'measureLabelCommon',
position
};
// Define in which element the selectTree will be rendered in
const parentElement = instance.$('.measure-flow')[0];
// Render the selectTree element
instance.selectTreeView = Blaze.renderWithData(Template.selectTree, data, parentElement);
});
},
'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();
});
},
'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);
}
// Keep the current description if ENTER was pressed
if (event.which === 13) {
instance.description.set($(event.currentTarget).val());
}
},
'click .select-tree-common label'(event, instance) {
// Set the common section clicked flag
instance.commonClicked = true;
},
'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) {
console.warn('>>>>event', event);
const $label = $(event.currentTarget).closest('label');
const $container = $label.closest('.select-tree-root').find('.tree-options:first');
const top = $label.position().top;
$container.scrollTop(top);
const $target = $(event.currentTarget);
const $label = $target.closest('label');
const $treeRoot = $label.closest('.select-tree-root');
const $container = $treeRoot.find('.tree-options:first');
// Check if the targe click was a label inside common section
if (instance.commonClicked) {
const labelTop = $label.position().top;
const containerCenter = Math.round($container.height() / 2);
const labelCenter = Math.round($label.height() / 2);
// Scroll the options container to make the target input visible
$container.scrollTop(labelTop - containerCenter + labelCenter);
}
// 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');
// Get the clicked label window offset
const labelOffset = $label.offset();
// Subtract the label box shadow height from the label's position
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());
});
// Wait the fade-out transition and remove the selectTree component
$container.one('transitionend', event => Blaze.remove(instance.selectTreeView));
}
});

View File

@ -1,12 +1,122 @@
.measure-flow
@import "{design}/app"
.select-tree-root>.tree-content>.tree-options
margin-right: -20px
max-height: 250px
overflow-x: hidden
overflow-y: scroll
position: relative
z-index: 2
.measure-flow
position: relative
&.open,
&.selected
.btn-add
opacity: 0
.btn-add
background-color: $activeColor
border: 2px solid $uiBorderColor
border-radius: 14px
color: $textColorActive
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
.select-tree-root>.tree-content
width: 213px
&>.tree-options
margin-right: -17px
max-height: 250px
overflow-x: hidden
overflow-y: scroll
position: relative
z-index: 2
.tree-inputs
position: relative
&>.tree-leaf
position: relative
&:before
animateZoomIn()
background-color: $activeColor
border-radius: 20px
color: $uiGray
content: '\f00c'
display: block
font-family: FontAwesome
font-size: 24px
height: 40px
left: -46px
line-height: 40px
position: absolute
text-align: center
top: 3px
width: 40px
span
background: white
font-weight: normal
height: 46px
line-height: 46px
padding: 0 12px
input, span
display: none
.actions
margin-top: 16px
opacity: 0
&:not(.fadeOut)
animateFadeIn()
animation-delay: 0.3s
&.fadeOut
animateFadeOut()
button
background-color: transparent
border-radius: 16px
border: 1px solid $uiBorderColor
color: $textPrimaryColor
font-weight: normal
height: 31px
line-height: 31px
padding: 0 12px
text-align: center
.description
margin-top: -10px
margin-bottom: 26px
textarea
background-color: white
box-shadow: 0 10px white
border: 0
line-height: 20px
outline: none
overflow: hidden
padding: 0 12px
resize: none
transition(height 0.3s ease)
width: 100%
.descriptionText
background-color: white
box-shadow: 0 10px white
line-height: 20px
margin-bottom: 26px
margin-top: -10px
padding: 0 12px
&.selected>.tree-leaf
span
display: block

View File

@ -28,7 +28,7 @@ $circleSize = 46px
font-weight: 700
font-size: 17px
text-align: center
color: $textColorActive
color: $activeColor
top: 0
svg

View File

@ -9,7 +9,7 @@
</div>
</div>
<table class="table table-striped">
<table class="table table-responsive">
<thead>
<tr>
<th class="center">Include Study?</th>

View File

@ -5,15 +5,17 @@
{{>loadingText}}
<div class="studyModality">
<div class="studyModalityBox">
{{#if this.study.modalities}}
{{this.study.modalities}}
{{else}}
UN
{{/if}}
<div class="studyModalityText" style="{{modalityStyle}}">
{{#if this.study.modalities}}
{{ modalities }}
{{else}}
UN
{{/if}}
</div>
</div>
<div class="studyModalityText">
<div class="studyModalityDate">{{formatDA this.study.studyDate 'D-MMM-YYYY'}}</div>
<div class="studyModalityDescription">{{this.study.studyDescription}}</div>
<div class="studyText">
<div class="studyDate">{{formatDA this.study.studyDate 'D-MMM-YYYY'}}</div>
<div class="studyDescription">{{this.study.studyDescription}}</div>
</div>
</div>
</div>

View File

@ -34,6 +34,39 @@ Template.studyTimepointStudy.onRendered(() => {
}
});
Template.studyTimepointStudy.helpers({
modalities() {
const instance = Template.instance();
let modalities = instance.data.study.modalities;
// Replace backslashes with spaces
return modalities.replace(/\\/g, ' ');
},
modalityStyle() {
// Responsively styles the Modality Acronyms for studies
// with more than one modality
const instance = Template.instance();
const modalities = instance.data.study.modalities;
const numModalities = modalities.split(/\\/g).length;
if (numModalities === 1) {
// If we have only one modality, it should take up the whole
// div.
return 'font-size: 1vw';
} else if (numModalities === 2) {
// If we have two, let them sit side-by-side
return 'font-size: 0.75vw';
} else {
// If we have more than two modalities, change the line
// height to display multiple rows, depending on the number
// of modalities we need to display.
const lineHeight = Math.ceil(numModalities / 2) * 1.2;
return 'line-height: ' + lineHeight + 'vh';
}
}
});
Template.studyTimepointStudy.events({
// Recalculates the timepoint height to make CSS transition smoother
'transitionend .studyTimepointThumbnails'(event, instance) {

View File

@ -80,29 +80,31 @@ $spacerY = 12px
padding: 0
text-align: center
.studyModalityText
.studyText
font-size: 13px
left: ($spacerX * 3) + $boxWidth + ($nestingMargin * 3)
line-height: 14px
position: absolute
right: $spacerX
top: $spacerY
.studyModalityDate
.studyDate
margin-top: 8px
color: $textSecondaryColor
.studyModalityDescription
.studyDescription
margin-top: 8px
color: $textPrimaryColor
.studyModalityBox
color: $textSecondaryColor
font-size: 22px
line-height: $boxWidth
margin-left: $nestingMargin * 2
margin-top: $nestingMargin * 2
position: relative
text-align: center
text-transform: uppercase
.studyModalityText
text-align: center
text-transform: uppercase
height: 100%
&
&:before

View File

@ -133,19 +133,16 @@ Template.toolbarSection.helpers({
return buttonData;
}
});
Template.toolbarSection.events({
'click #toggleHUD'(event, instance) {
'click #toggleHUD'() {
const state = Session.get('lesionTableHudOpen');
Session.set('lesionTableHudOpen', !state);
}
});
Template.toolbarSection.onRendered(function() {
const instance = Template.instance();
// Set disabled/enabled tool buttons that are set in toolManager
const states = toolManager.getToolDefaultStates();
const disabledToolButtons = states.disabledToolButtons;

View File

@ -5,7 +5,6 @@
height: 100%
.dropdown-toggle
background-color: $primaryBackgroundColor
color: $defaultColor
cursor: pointer
font-size: 13px

View File

@ -1,5 +1,6 @@
import { OHIF } from 'meteor/ohif:core';
import { TimepointApi } from 'meteor/lesiontracker/lib/api/timepoint';
import { TimepointApi } from 'meteor/lesiontracker/client/api/timepoint';
import { MeasurementApi } from 'meteor/lesiontracker/client/api/measurement';
OHIF.viewer = OHIF.viewer || {};
OHIF.viewer.loadIndicatorDelay = 3000;
@ -18,8 +19,6 @@ Session.setDefault('rightSidebar', null);
Template.viewer.onCreated(() => {
const instance = Template.instance();
instance.data.timepointApi = new TimepointApi();
ValidationErrors.remove({});
instance.data.state = new ReactiveDict();
@ -91,6 +90,17 @@ Template.viewer.onCreated(() => {
// Set buttons as enabled/disabled when Timepoints collection is ready
timepointAutoCheck(dataContext);
// Wait until the Timepoint subscription is ready to initialize the TimepointApi
instance.data.timepointApi = new TimepointApi();
instance.data.timepointApi.currentTimepointId = instance.data.currentTimepointId;
// Provide the necessary data to the Measurement API
MeasurementApi.currentTimepointId = instance.data.currentTimepointId;
const prior = instance.data.timepointApi.prior();
if (prior) {
MeasurementApi.priorTimepointId = prior.timepointId;
}
TrialResponseCriteria.validateAllDelayed();
ViewerStudies.find().observe({

View File

@ -11,8 +11,9 @@ handleMeasurementAdded = function(e, eventData) {
TrialResponseCriteria.validateDelayed(measurementData);
// Set reviewer for this timepoint
if (measurementData.studyInstanceUid) {
Meteor.call('setReviewer',measurementData.studyInstanceUid);
Meteor.call('setReviewer', measurementData.studyInstanceUid);
}
break;
case 'ellipticalRoi':
case 'length':

View File

@ -18,7 +18,7 @@ handleMeasurementRemoved = function(e, eventData) {
}
// Set reviewer for this timepoint
if (measurementData.studyInstanceUid) {
Meteor.call('setReviewer',measurementData.studyInstanceUid);
Meteor.call('setReviewer', measurementData.studyInstanceUid);
}
clearMeasurementTimepointData(measurement._id, measurementData.timepointId);

View File

@ -265,7 +265,8 @@ Package.onUse(function(api) {
api.addFiles('client/components/serverInformation/serverInformationModal/serverInformationModal.js', 'client');
// API classes
api.addFiles('lib/api/timepoint.js');
api.addFiles('client/api/timepoint.js');
api.addFiles('client/api/measurement.js');
// Export global functions
api.export('pixelSpacingAutorunCheck', 'client');

View File

@ -15,6 +15,9 @@ OHIF.mixins.selectTree = new OHIF.Mixin({
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);
@ -25,7 +28,23 @@ OHIF.mixins.selectTree = new OHIF.Mixin({
component.value = value => {
const isGet = _.isUndefined(value);
// Return the current node value
// Return the current value
if (isGet) {
return 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;
@ -35,12 +54,12 @@ OHIF.mixins.selectTree = new OHIF.Mixin({
const items = component.templateInstance.data.items;
// Get the node for the selected value
const node = _.findWhere(items, {
value: value
const currentNode = _.findWhere(items, {
value: node
});
// Change the current node
instance.data.currentNode.set(node);
instance.data.currentNode.set(currentNode);
};
// Return plain data for all the leaves inside current node
@ -59,6 +78,14 @@ OHIF.mixins.selectTree = new OHIF.Mixin({
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();
}
}
});

View File

@ -27,6 +27,7 @@
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')

View File

@ -27,8 +27,31 @@ Template.selectTree.onRendered(() => {
// Set the margin to display the common section
$treeRoot.children('.tree-content').css('margin-right', $treeRoot.width());
// Start the component transitions
$treeRoot.addClass('started');
// Make the component respect the window boundaries
$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 = searchTerm => {
@ -43,8 +66,9 @@ Template.selectTree.onRendered(() => {
height = rootInstance.$('.tree-options:last').data('height');
}
// Update the viewport's height
rootInstance.$('.tree-options:first').height(height);
// Update the viewport's height and trigger the bounded event
rootInstance.$('.tree-options:first').first().height(height)
.on('transitionend', () => $treeRoot.trigger('spatialChanged'));
};
// Update the opened node
@ -110,23 +134,29 @@ Template.selectTree.events({
},
'input .tree-search input'(event, instance) {
// Get the tree root
const $treeRoot = $(event.currentTarget).closest('.select-tree-root');
// Change the search term to update the tree items
instance.searchTerm.set($(event.currentTarget).val());
// Change the component state
$treeRoot.addClass('interacted');
},
'change .select-tree:first>.tree-content>.tree-options>.tree-inputs>label>input'(event, instance) {
const component = instance.component;
const $target = $(event.target);
const $target = $(event.currentTarget);
const $label = $target.closest('label');
const eventComponent = $target.data('component');
const rootComponent = instance.data.root || component;
const rootInstance = rootComponent.templateInstance;
// Change the component's value
component.value(eventComponent.value());
// Change the component's node
component.node(eventComponent.value());
// Unset the active leaf
rootInstance.$('label').removeClass('active');
rootInstance.$('label').removeClass('active').css('width', '');
// Unset the selected state
instance.setSelected(false);
@ -135,7 +165,7 @@ Template.selectTree.events({
const isLeaf = $label.hasClass('tree-leaf');
if (isLeaf) {
// Change the active item
$label.addClass('active');
$label.css('width', $label.outerWidth()).addClass('active');
// Ckeck if there's a storageKey defined
const storageKey = instance.data.storageKey;
@ -151,6 +181,10 @@ Template.selectTree.events({
storedData[itemKey] = 1;
}
// Set the selected leaf value in the root component
const itemData = $target.data('component').templateInstance.data.itemData;
rootComponent.value(itemData);
// Updata the stored data with the new count
OHIF.user.setData(storageKey, storedData);
}
@ -201,6 +235,27 @@ Template.selectTree.events({
currentInstance = currentInstance.component.parent.templateInstance;
}
}
},
'click .select-tree-common label'(event, instance) {
// Get the clicked label
const $target = $(event.currentTarget);
// Build the input selector based on the label target
const inputSelector = '#' + $target.attr('for');
// Check if the input is not rendered
if (!$(inputSelector).length) {
// Change the search terms with the clicked label string
instance.$('.tree-search input').val($target.text()).trigger('input');
// Wait for options rerendering
Tracker.afterFlush(() => {
// Wait for components initialization
Meteor.defer(() => $(inputSelector).click());
});
}
}
});

View File

@ -1,4 +1,5 @@
import './base';
import './bootstrap';
import './playground/playground.html';
import './playground/playground.styl';
import './playground/playground.js';

View File

@ -1,14 +1,16 @@
<template name="componentPlayground">
<!--
<div class="p-a-3">
{{>selectTree
key='label'
items=instance.items
label='Assign label'
searchPlaceholder='Search labels'
storageKey='measureLabelCommon'
}}
<div class="component-playground">
{{>measureFlow}}
<!--
<div class="p-a-3">
{{>selectTree
key='label'
items=instance.items
label='Assign label'
searchPlaceholder='Search labels'
storageKey='measureLabelCommon'
}}
</div>
-->
</div>
-->
{{>measureFlow}}
</template>

View File

@ -42,3 +42,8 @@ Template.componentPlayground.onCreated(() => {
value: 'Category 3'
}];
});
Template.componentPlayground.onRendered(() => {
const instance = Template.instance();
instance.$('.measure-flow').draggable();
});

View File

@ -0,0 +1,2 @@
.component-playground
height: 100vh

View File

@ -7,7 +7,7 @@ OHIF.string = {};
// Search for some string inside any object or array
OHIF.string.search = (object, query, property=null, result=[]) => {
// Create the search pattern
const pattern = new RegExp($.trim(query).replace(/\s/gi, '|'), 'i');
const pattern = new RegExp($.trim(query), 'i');
_.each(object, item => {
// Stop here if item is empty

View File

@ -1,3 +1,5 @@
import { OHIF } from 'meteor/ohif:core';
// Allow attaching to jQuery selectors
$.fn.bounded = function(options) {
_.each(this, element => {
@ -63,7 +65,7 @@ class Bounded {
this.$element.removeClass('bounded');
}
spatialInfo(element) {
static spatialInfo(element) {
// Create the result object
const result = {};
@ -105,8 +107,8 @@ class Bounded {
defineEventHandlers() {
this.spatialChangedHandler = event => {
// Get the spatial information for element and its bounding element
const elementInfo = this.spatialInfo(this.element);
const boundingInfo = this.spatialInfo(this.boundingElement);
const elementInfo = Bounded.spatialInfo(this.element);
const boundingInfo = Bounded.spatialInfo(this.boundingElement);
// Fix element's x positioning and width
if (this.allowResizing && elementInfo.width > boundingInfo.width) {
@ -143,3 +145,5 @@ class Bounded {
}
}
OHIF.Bounded = Bounded;

View File

@ -2,4 +2,6 @@
#imageViewerViewports
height: 100%
width: 100%
padding-bottom: 1px
padding-right: 6px
width: 100%

View File

@ -1,7 +1,11 @@
<template name="imageControls">
<div class="imageControls">
<div id="scrollbar">
<input id="imageSlider" type="range" min="1" val="1"/>
<input id="imageSlider"
type="range"
min="1"
value="{{imageIndex}}"
max="{{numImages}}"/>
</div>
</div>
</template>

View File

@ -1,6 +1,20 @@
const slideTimeoutTime = 40;
let slideTimeout;
Template.imageControls.onRendered(() => {
const instance = Template.instance();
Meteor.defer(() => {
// Set the current imageSlider width to its parent's height
// (because webkit is stupid and can't style vertical sliders)
const $slider = instance.$('#imageSlider');
const $element = $slider.parents().eq(2).siblings('.imageViewerViewport');
const viewportHeight = $element.height();
$slider.width(viewportHeight - 20);
});
})
Template.imageControls.events({
'input #imageSlider, change #imageSlider': function(e) {
// Note that we throttle requests to prevent the

View File

@ -17,6 +17,7 @@ $imageSliderCursor = grab
padding: 5px
#scrollbar
height: calc(100% - 20px);
margin-top: 5px
width: 31px

View File

@ -86,32 +86,6 @@ function loadDisplaySetIntoViewport(data, templateData) {
imageIds: imageIds
};
// Show or hide the image scrollbar depending
// on the number of images in the stack
var currentOverlay = $(element).siblings('.imageViewerViewportOverlay');
var imageControls = currentOverlay.find('.imageControls');
currentOverlay.find('.imageControls').height($(element).height());
if (stack.imageIds.length === 1) {
imageControls.hide();
currentOverlay.find('.topright, .bottomright').css('right', '3px');
} else {
imageControls.show();
currentOverlay.find('.topright, .bottomright').css('right', '39px');
// Update the maximum value of the slider
var currentImageSlider = currentOverlay.find('#imageSlider');
currentImageSlider.attr('max', stack.imageIds.length);
currentImageSlider.val(1);
// Set it's width to its parent's height
// (because webkit is stupid and can't style vertical sliders)
var scrollbar = currentOverlay.find('#scrollbar');
scrollbar.height(scrollbar.parent().height() - 20);
var overlayHeight = currentImageSlider.parent().height();
currentImageSlider.width(overlayHeight);
}
// Get the current image ID for the stack that will be rendered
imageId = imageIds[stack.currentImageIdIndex];
@ -296,11 +270,6 @@ function loadDisplaySetIntoViewport(data, templateData) {
var stack = toolData.data[0];
// Update the imageSlider value
var currentOverlay = $(element).siblings('.imageViewerViewportOverlay');
var currentImageSlider = currentOverlay.find('#imageSlider');
currentImageSlider.val(stack.currentImageIdIndex + 1);
// If this viewport is displaying a stack of images, save the current image
// index in the stack to the global ViewerData object, as well as the Meteor Session.
var stack = cornerstoneTools.getToolState(element, 'stack');
@ -319,27 +288,6 @@ function loadDisplaySetIntoViewport(data, templateData) {
// Set a random value for the Session variable in order to trigger an overlay update
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
function OnStackScroll(e, eventData) {
// Get the element and stack data
var element = e.target;
var toolData = cornerstoneTools.getToolState(element, 'stack');
if (!toolData || !toolData.data || !toolData.data.length) {
return;
}
var stack = toolData.data[0];
// Update the imageSlider value
var currentOverlay = $(element).siblings('.imageViewerViewportOverlay');
var currentImageSlider = currentOverlay.find('#imageSlider');
currentImageSlider.val(stack.currentImageIdIndex + 1);
}
$(element).off('CornerstoneStackScroll', OnStackScroll);
if (stack.imageIds.length > 1) {
$(element).on('CornerstoneStackScroll', OnStackScroll);
}
// Define a function to trigger an event whenever a new viewport is being used
// This is used to update the value of the "active viewport", when the user interacts
// with a new viewport element

View File

@ -1,5 +1,5 @@
<template name="viewportOverlay">
<div class="imageViewerViewportOverlay noselect">
<div class="imageViewerViewportOverlay noselect {{#if gt numImages 1}}controlsVisible{{/if}}">
{{ #unless tagDisplaySpecified }}
<div class="topleft dicomTag">
<div>{{formatPN patientName}}</div>
@ -11,15 +11,19 @@
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
</div>
<div class="bottomright dicomTag">
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 2}}%{{/if}}</div>
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 0}}%{{/if}}</div>
<div>{{compression}}</div>
<div>{{wwwc}}</div>
</div>
<div class="bottomleft dicomTag">
<div>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
<div>{{#if numImages}}Img: {{imageNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
<div>{{#if gt numImages 1}}Img: {{instanceNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
<div>{{#if frameRate}}{{frameRate}} FPS{{/if}}</div>
<div>{{imageDimensions}}</div>
<div>{{#if location}}Loc: {{location}} mm{{/if}}
{{#if thickness}}Thick: {{thickness}} mm{{/if}}
{{#if spacingBetweenSlices}}Spacing: {{spacingBetweenSlices}} mm{{/if}}
</div>
<div>{{seriesDescription}}</div>
</div>
{{ /unless }}
@ -34,11 +38,11 @@
</div>
<div class="bottomright dicomTag">
<div>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
<div>{{#if numImages}}Img: {{imageNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
<div>{{#if gt numImages 1}}Img: {{instanceNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
<div>{{#if frameRate}}{{frameRate}} FPS{{/if}}</div>
<div>{{imageDimensions}}</div>
<div>{{seriesDescription}}</div>
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 2}}%{{/if}}</div>
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 0}}%{{/if}}</div>
<div>{{compression}}</div>
<div>{{wwwc}}</div>
</div>
@ -54,15 +58,18 @@
</div>
<div class="bottomleft dicomTag">
<div>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
<div>{{#if numImages}}Img: {{imageNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
<div>{{#if gt numImages 1}}Img: {{instanceNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
<div>{{#if frameRate}}{{frameRate}} FPS{{/if}}</div>
<div>{{imageDimensions}}</div>
<div>{{seriesDescription}}</div>
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 2}}%{{/if}}</div>
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 0}}%{{/if}}</div>
<div>{{compression}}</div>
<div>{{wwwc}}</div>
</div>
{{ /if }}
{{>imageControls}}
{{#if gt numImages 1}}
{{> imageControls imageIndex=imageIndex numImages=numImages}}
{{ /if }}
</div>
</template>

View File

@ -180,6 +180,12 @@ Template.viewportOverlay.helpers({
patientId: function() {
return getPatient.call(this, 'id');
},
patientBirthDate: function() {
return getPatient.call(this, 'birthDate');
},
patientSex: function() {
return getPatient.call(this, 'sex');
},
studyDate: function() {
return getStudy.call(this, 'studyDate');
},
@ -204,9 +210,67 @@ Template.viewportOverlay.helpers({
seriesNumber: function() {
return getSeries.call(this, 'seriesNumber');
},
imageNumber: function() {
instanceNumber: function() {
return getInstance.call(this, 'instanceNumber');
},
thickness() {
// Displays Slice Thickness (0018,0050)
return getInstance.call(this, 'sliceThickness');
},
location() {
// Displays Slice Location (0020,1041), if present.
// - Otherwise, displays Table Position (0018,9327)
// - TODO: Otherwise, displays a value derived from Image Position (Patient) (0020,0032)
const sliceLocation = getInstance.call(this, 'sliceLocation');
if (sliceLocation !== '') {
return sliceLocation;
}
const tablePosition = getInstance.call(this, 'tablePosition');
if (tablePosition !== '') {
return tablePosition;
}
return getInstance.call(this, 'imagePositionPatient');
},
spacingBetweenSlices() {
// Displays Spacing Between Slices (0018,0088), if present.
// TODO: Otherwise, displays a value derived from successive values
// of Image Position (Patient) (0020,0032) perpendicular to
// the Image Orientation (Patient) (0020,0037)
return getInstance.call(this, 'spacingBetweenSlices');
},
compression() {
// Displays whether or not lossy compression has been applied:
//
// - Checks Lossy Image Compression (0028,2110)
// - If so, displays the value of Lossy Image Compression Ratio (0028,2112)
// and Lossy Image Compression Method (0028,2114)
Session.get('CornerstoneNewImage' + this.viewportIndex);
if (!this.imageId) {
return false;
}
var instance = cornerstoneTools.metaData.get('instance', this.imageId);
if (!instance) {
return '';
}
if (instance.lossyImageCompression === '01' &&
instance.lossyImageCompressionRatio !== '') {
const compressionMethod = instance.lossyImageCompressionMethod || 'Lossy: ';
const compressionRatio = parseFloat(instance.lossyImageCompressionRatio).toFixed(2);
return compressionMethod + compressionRatio + ' : 1';
}
return 'Lossless / Uncompressed';
},
tagDisplayLeftOnly: function() {
return getTagDisplay.call(this, 'side') === 'L';
},
@ -216,12 +280,6 @@ Template.viewportOverlay.helpers({
tagDisplaySpecified: function() {
return getTagDisplay.call(this, 'side');
},
imageIndex: function() {
return getInstance.call(this, 'index');
},
numImages: function() {
return getSeries.call(this, 'numImages');
},
imageIndex: function() {
Session.get('CornerstoneNewImage' + this.viewportIndex);
var stack = getStackDataIfNotEmpty(this.viewportIndex);

View File

@ -30,4 +30,8 @@ $viewportTagPadding = 20px
.priorIndicator
font-weight: bold
color: yellow
color: yellow
&.controlsVisible
.topright, .bottomright
right: "calc(%s + 19px)" % $viewportTagPadding

View File

@ -26,7 +26,8 @@ addMetaData = function(imageId, data) {
studyInstanceUid: studyMetaData.studyInstanceUid,
studyDate: studyMetaData.studyDate,
studyTime: studyMetaData.studyTime,
studyDescription: studyMetaData.studyDescription
studyDescription: studyMetaData.studyDescription,
institutionName: studyMetaData.institutionName
};
metaData.series = {
@ -37,23 +38,13 @@ addMetaData = function(imageId, data) {
numImages: numImages
};
metaData.instance = {
wadouri: instanceMetaData.wadouri,
imageType: instanceMetaData.imageType,
photometricInterpretation: instanceMetaData.photometricInterpretation,
sopInstanceUid: instanceMetaData.sopInstanceUid,
sopClassUid: instanceMetaData.sopClassUid,
instanceNumber: instanceMetaData.instanceNumber,
laterality: instanceMetaData.laterality,
viewPosition: instanceMetaData.viewPosition,
sliceThickness: instanceMetaData.sliceThickness,
frameTime: instanceMetaData.frameTime,
index: imageIndex
};
metaData.instance = instanceMetaData;
metaData.patient = {
name: studyMetaData.patientName,
id: studyMetaData.patientId
id: studyMetaData.patientId,
birthDate: studyMetaData.patientBirthDate,
sex: studyMetaData.patientSex
};
// If there is sufficient information, populate

View File

@ -37,13 +37,19 @@ function configureTools() {
cornerstoneTools.toolColors.setActiveColor('#00ffff'); //rgb(0, 255, 0)'
// Set the configuration values for the text annotation (Arrow) tool
var annotateConfig = {
const annotateConfig = {
getTextCallback: getAnnotationTextCallback,
changeTextCallback: changeAnnotationTextCallback,
drawHandles: false,
arrowFirst: true
};
cornerstoneTools.arrowAnnotate.setConfiguration(annotateConfig);
const zoomConfig = {
minScale: 0.05,
maxScale: 10
};
cornerstoneTools.zoom.setConfiguration(zoomConfig);
}
toolManager = {

View File

@ -43,12 +43,15 @@ function resultDataToStudyMetadata(studyInstanceUid, resultData) {
seriesList: seriesList,
patientName: anInstance[0x00100010],
patientId: anInstance[0x00100020],
patientBirthDate: anInstance[0x00100030],
patientSex: anInstance[0x00100040],
accessionNumber: anInstance[0x00080050],
studyDate: anInstance[0x00080020],
modalities: anInstance[0x00080061],
studyDescription: anInstance[0x00081030],
imageCount: anInstance[0x00201208],
studyInstanceUid: anInstance[0x0020000D]
studyInstanceUid: anInstance[0x0020000D],
institutionName: anInstance[0x00080080]
};
resultData.forEach(function(instanceRaw) {
@ -78,7 +81,9 @@ function resultDataToStudyMetadata(studyInstanceUid, resultData) {
imagePositionPatient: instance[0x00200032],
imageOrientationPatient: instance[0x00200037],
frameOfReferenceUID: instance[0x00200052],
sliceThickness: parseFloat(instance[0x00180050]),
sliceLocation: parseFloat(instance[0x00201041]),
tablePosition: parseFloat(instance[0x00189327]),
samplesPerPixel: parseFloat(instance[0x00280002]),
photometricInterpretation: instance[0x00280004],
rows: parseFloat(instance[0x00280010]),
@ -96,7 +101,12 @@ function resultDataToStudyMetadata(studyInstanceUid, resultData) {
laterality: instance[0x00200062],
viewPosition: instance[0x00185101],
numFrames: parseFloat(instance[0x00280008]),
frameTime: parseFloat(instance[0x00181063])
frameTime: parseFloat(instance[0x00181063]),
lossyImageCompression: instance[0x00282110],
derivationDescription: instance[0x00282111],
lossyImageCompressionRatio: instance[0x00282112],
lossyImageCompressionMethod: instance[0x00282114],
spacingBetweenSlices: instance[0x00180088]
};
// Retrieve the actual data over WADO-URI

View File

@ -59,7 +59,8 @@ function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
modalities: DICOMWeb.getString(anInstance['00080061']),
studyDescription: DICOMWeb.getString(anInstance['00081030']),
imageCount: DICOMWeb.getString(anInstance['00201208']),
studyInstanceUid: DICOMWeb.getString(anInstance['0020000D'])
studyInstanceUid: DICOMWeb.getString(anInstance['0020000D']),
institutionName: DICOMWeb.getString(anInstance['00080080'])
};
resultData.forEach(function(instance) {
@ -107,7 +108,11 @@ function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
viewPosition: DICOMWeb.getString(instance['00185101']),
numFrames: DICOMWeb.getNumber(instance['00280008']),
frameTime: DICOMWeb.getNumber(instance['00181063']),
sliceThickness: DICOMWeb.getNumber(instance['00180050'])
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
derivationDescription: DICOMWeb.getString(instance['00282111']),
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
};
if (server.imageRendering === 'wadouri') {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 KiB

After

Width:  |  Height:  |  Size: 662 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 277 KiB