Merge branch 'master' of https://github.com/OHIF/Viewers
This commit is contained in:
commit
4e164215ea
@ -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>
|
||||
|
||||
@ -12,7 +12,7 @@ Template.toolbarSection.helpers({
|
||||
svgLink: '/packages/viewerbase/assets/icons.svg#icon-studies',
|
||||
svgWidth: 15,
|
||||
svgHeight: 13,
|
||||
bottomLabel: 'Studies'
|
||||
bottomLabel: 'Series'
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
@import "{design}/styles/imports/animations"
|
||||
@import "{design}/styles/imports/mixins"
|
||||
@import "{design}/styles/imports/spacings"
|
||||
@import "{design}/styles/imports/variables"
|
||||
|
||||
@ -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');
|
||||
|
||||
19
Packages/design/styles/common/keyframes.styl
Normal file
19
Packages/design/styles/common/keyframes.styl
Normal 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
|
||||
@ -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
|
||||
|
||||
17
Packages/design/styles/imports/animations.styl
Normal file
17
Packages/design/styles/imports/animations.styl
Normal 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
|
||||
@ -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
|
||||
};
|
||||
};
|
||||
|
||||
102
Packages/lesiontracker/client/api/measurement.js
Normal file
102
Packages/lesiontracker/client/api/measurement.js
Normal 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);
|
||||
}
|
||||
};
|
||||
@ -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)
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@ -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')
|
||||
};
|
||||
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
@ -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}}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
@ -28,7 +28,7 @@ $circleSize = 46px
|
||||
font-weight: 700
|
||||
font-size: 17px
|
||||
text-align: center
|
||||
color: $textColorActive
|
||||
color: $activeColor
|
||||
top: 0
|
||||
|
||||
svg
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped">
|
||||
<table class="table table-responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="center">Include Study?</th>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
height: 100%
|
||||
|
||||
.dropdown-toggle
|
||||
background-color: $primaryBackgroundColor
|
||||
color: $defaultColor
|
||||
cursor: pointer
|
||||
font-size: 13px
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import './base';
|
||||
import './bootstrap';
|
||||
import './playground/playground.html';
|
||||
import './playground/playground.styl';
|
||||
import './playground/playground.js';
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -42,3 +42,8 @@ Template.componentPlayground.onCreated(() => {
|
||||
value: 'Category 3'
|
||||
}];
|
||||
});
|
||||
|
||||
Template.componentPlayground.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
instance.$('.measure-flow').draggable();
|
||||
});
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
.component-playground
|
||||
height: 100vh
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -2,4 +2,6 @@
|
||||
|
||||
#imageViewerViewports
|
||||
height: 100%
|
||||
width: 100%
|
||||
padding-bottom: 1px
|
||||
padding-right: 6px
|
||||
width: 100%
|
||||
|
||||
@ -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>
|
||||
@ -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
|
||||
|
||||
@ -17,6 +17,7 @@ $imageSliderCursor = grab
|
||||
padding: 5px
|
||||
|
||||
#scrollbar
|
||||
height: calc(100% - 20px);
|
||||
margin-top: 5px
|
||||
width: 31px
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
@ -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);
|
||||
|
||||
@ -30,4 +30,8 @@ $viewportTagPadding = 20px
|
||||
|
||||
.priorIndicator
|
||||
font-weight: bold
|
||||
color: yellow
|
||||
color: yellow
|
||||
|
||||
&.controlsVisible
|
||||
.topright, .bottomright
|
||||
right: "calc(%s + 19px)" % $viewportTagPadding
|
||||
@ -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
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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') {
|
||||
|
||||
BIN
img/viewer.png
BIN
img/viewer.png
Binary file not shown.
|
Before Width: | Height: | Size: 330 KiB After Width: | Height: | Size: 662 KiB |
BIN
img/worklist.png
BIN
img/worklist.png
Binary file not shown.
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 277 KiB |
Loading…
Reference in New Issue
Block a user