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="HandheldFriendly" content="True">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<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=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'>
|
<link href='http://fonts.googleapis.com/css?family=Sanchez:400,700&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@ -12,7 +12,7 @@ Template.toolbarSection.helpers({
|
|||||||
svgLink: '/packages/viewerbase/assets/icons.svg#icon-studies',
|
svgLink: '/packages/viewerbase/assets/icons.svg#icon-studies',
|
||||||
svgWidth: 15,
|
svgWidth: 15,
|
||||||
svgHeight: 13,
|
svgHeight: 13,
|
||||||
bottomLabel: 'Studies'
|
bottomLabel: 'Series'
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
@import "{design}/styles/imports/animations"
|
||||||
@import "{design}/styles/imports/mixins"
|
@import "{design}/styles/imports/mixins"
|
||||||
@import "{design}/styles/imports/spacings"
|
@import "{design}/styles/imports/spacings"
|
||||||
@import "{design}/styles/imports/variables"
|
@import "{design}/styles/imports/variables"
|
||||||
|
|||||||
@ -26,6 +26,7 @@ Package.onUse(function(api) {
|
|||||||
// Common styles
|
// Common styles
|
||||||
api.addFiles([
|
api.addFiles([
|
||||||
'styles/common/webfonts.styl',
|
'styles/common/webfonts.styl',
|
||||||
|
'styles/common/keyframes.styl',
|
||||||
'styles/common/global.styl',
|
'styles/common/global.styl',
|
||||||
'styles/common/spacings.styl'
|
'styles/common/spacings.styl'
|
||||||
], 'client');
|
], '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
|
.tree-content
|
||||||
background-color: white
|
background-color: white
|
||||||
position: relative
|
position: relative
|
||||||
|
transition(transform 0.3s ease\, background-color 0.3s ease\, border-color 0.3s ease\, border-radius 0.3s ease)
|
||||||
|
|
||||||
&
|
|
||||||
.tree-search
|
.tree-search
|
||||||
a.tree-back
|
a.tree-back
|
||||||
.tree-breadcrumb span
|
.tree-breadcrumb span
|
||||||
@ -146,6 +146,7 @@ $gray6 = #303030
|
|||||||
|
|
||||||
&.active
|
&.active
|
||||||
box-shadow: 0 0 0 200px white, inset 0 0 0 200px white
|
box-shadow: 0 0 0 200px white, inset 0 0 0 200px white
|
||||||
|
position: fixed
|
||||||
transition(box-shadow 0.3s ease)
|
transition(box-shadow 0.3s ease)
|
||||||
z-index: 0
|
z-index: 0
|
||||||
|
|
||||||
@ -192,7 +193,6 @@ $gray6 = #303030
|
|||||||
&>.tree-content
|
&>.tree-content
|
||||||
transform(scale(0))
|
transform(scale(0))
|
||||||
transform-origin(100% 50%)
|
transform-origin(100% 50%)
|
||||||
transition(all 0.3s ease)
|
|
||||||
|
|
||||||
.select-tree-common
|
.select-tree-common
|
||||||
overflow: hidden
|
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 {
|
return {
|
||||||
0x0020000D: studyInstanceUID ? studyInstanceUID : '',
|
0x0020000D: studyInstanceUID ? studyInstanceUID : '',
|
||||||
0x0020000E: (studyInstanceUID && seriesInstanceUID) ? seriesInstanceUID : '',
|
0x0020000E: (studyInstanceUID && seriesInstanceUID) ? seriesInstanceUID : '',
|
||||||
0x00080005: '',
|
0x00080005: '', // specificCharacterSet
|
||||||
0x00080020: '',
|
0x00080020: '', // studyDate
|
||||||
0x00080030: '',
|
0x00080030: '', // studyDescription
|
||||||
0x00080090: '',
|
0x00080090: '', // referringPhysicianName
|
||||||
0x00100010: '',
|
0x00100010: '', // patientName
|
||||||
0x00100020: '',
|
0x00100020: '', // patientId
|
||||||
0x00200010: '',
|
0x00100030: '', // patientBirthDate
|
||||||
0x0008103E: '',
|
0x00100040: '', // patientSex
|
||||||
0x00200011: '',
|
0x00200010: '', // studyId
|
||||||
|
0x0008103E: '', // seriesDescription
|
||||||
|
0x00200011: '', // seriesNumber
|
||||||
|
0x00080080: '', // institutionName
|
||||||
0x00080016: '', // sopClassUid
|
0x00080016: '', // sopClassUid
|
||||||
0x00080018: '', // sopInstanceUid
|
0x00080018: '', // sopInstanceUid
|
||||||
0x00080060: '', // modality
|
0x00080060: '', // modality
|
||||||
@ -37,7 +40,9 @@ var getInstanceRetrievalParams = function(studyInstanceUID, seriesInstanceUID) {
|
|||||||
0x00281052: '', // rescaleIntercept
|
0x00281052: '', // rescaleIntercept
|
||||||
0x00281053: '', // rescaleSlope
|
0x00281053: '', // rescaleSlope
|
||||||
0x00280002: '', // samplesPerPixel
|
0x00280002: '', // samplesPerPixel
|
||||||
|
0x00180050: '', // sliceThickness
|
||||||
0x00201041: '', // sliceLocation
|
0x00201041: '', // sliceLocation
|
||||||
|
0x00189327: '', // tablePosition
|
||||||
0x00281050: '', // windowCenter
|
0x00281050: '', // windowCenter
|
||||||
0x00281051: '', // windowWidth
|
0x00281051: '', // windowWidth
|
||||||
0x00280030: '', // pixelSpacing
|
0x00280030: '', // pixelSpacing
|
||||||
@ -47,9 +52,14 @@ var getInstanceRetrievalParams = function(studyInstanceUID, seriesInstanceUID) {
|
|||||||
0x00200032: '', // imagePositionPatient
|
0x00200032: '', // imagePositionPatient
|
||||||
0x00200037: '', // imageOrientationPatient
|
0x00200037: '', // imageOrientationPatient
|
||||||
0x00200052: '', // frameOfReferenceUID
|
0x00200052: '', // frameOfReferenceUID
|
||||||
|
0x00282110: '', // lossyImageCompression
|
||||||
|
0x00282112: '', // lossyImageCompressionRatio
|
||||||
|
0x00282114: '', // lossyImageCompressionMethod,
|
||||||
|
0x00180088: '' // spacingBetweenSlices
|
||||||
|
|
||||||
// Orthanc has a bug here so we can't retrieve sequences at the moment
|
// Orthanc has a bug here so we can't retrieve sequences at the moment
|
||||||
// https://groups.google.com/forum/#!topic/orthanc-users/ghKJfvtnK8Y
|
// https://groups.google.com/forum/#!topic/orthanc-users/ghKJfvtnK8Y
|
||||||
|
//0x00282111: '', // derivationDescription
|
||||||
//0x00082112: '' // sourceImageSequence
|
//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 this.timepoints.find().fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return only the current and prior timepoints
|
// Return only the current timepoint
|
||||||
latest() {
|
current() {
|
||||||
const options = {
|
return this.timepoints.findOne({
|
||||||
limit: 2
|
timepointId: this.currentTimepointId
|
||||||
};
|
});
|
||||||
return this.timepoints.find({}, options).fetch();
|
}
|
||||||
|
|
||||||
|
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)
|
// Return only the key timepoints (current, prior, nadir and baseline)
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { MeasurementApi } from 'meteor/lesiontracker/client/api/measurement';
|
||||||
|
|
||||||
Template.caseProgress.onCreated(() => {
|
Template.caseProgress.onCreated(() => {
|
||||||
const instance = Template.instance();
|
const instance = Template.instance();
|
||||||
|
|
||||||
@ -5,41 +7,31 @@ Template.caseProgress.onCreated(() => {
|
|||||||
instance.progressText = new ReactiveVar();
|
instance.progressText = new ReactiveVar();
|
||||||
instance.isLocked = 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');
|
console.warn('Case has no timepointId');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTimepointId = instance.data.currentTimepointId;
|
instance.isLocked.set(current.isLocked);
|
||||||
const timepoint = Timepoints.findOne({
|
|
||||||
timepointId: currentTimepointId
|
|
||||||
});
|
|
||||||
|
|
||||||
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 we're currently reviewing a Baseline timepoint, don't do any
|
||||||
|
// progress measurement.
|
||||||
if (timepointType === 'baseline') {
|
if (current.timepointType === 'baseline') {
|
||||||
instance.progressPercent.set(100);
|
instance.progressPercent.set(100);
|
||||||
} else {
|
} 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
|
// Setup a reactive function to update the progress whenever
|
||||||
// a measurement is made
|
// a measurement is made
|
||||||
instance.autorun(() => {
|
instance.autorun(() => {
|
||||||
// Obtain the number of Measurements for which the current Timepoint has
|
// Obtain the number of Measurements for which the current Timepoint has
|
||||||
// no Measurement data
|
// no Measurement data
|
||||||
let numRemainingMeasurements = 0;
|
const numRemainingMeasurements = MeasurementApi.unmarked().length;
|
||||||
Measurements.find().forEach(measurement => {
|
|
||||||
if (!measurement.timepoints[currentTimepointId]) {
|
|
||||||
numRemainingMeasurements++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the Case Progress text with the remaining measurement count
|
// Update the Case Progress text with the remaining measurement count
|
||||||
instance.progressText.set(numRemainingMeasurements);
|
instance.progressText.set(numRemainingMeasurements);
|
||||||
@ -67,7 +59,7 @@ Template.caseProgress.helpers({
|
|||||||
},
|
},
|
||||||
|
|
||||||
progressComplete() {
|
progressComplete() {
|
||||||
let progressPercent = Template.instance().progressPercent.get();
|
const progressPercent = Template.instance().progressPercent.get();
|
||||||
return progressPercent === 100;
|
return progressPercent === 100;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { MeasurementApi } from 'meteor/lesiontracker/client/api/measurement';
|
||||||
|
|
||||||
Template.lesionTable.onCreated(() => {
|
Template.lesionTable.onCreated(() => {
|
||||||
const instance = Template.instance();
|
const instance = Template.instance();
|
||||||
|
|
||||||
@ -13,7 +15,7 @@ Template.lesionTable.onCreated(() => {
|
|||||||
if (tableLayout === 'key') {
|
if (tableLayout === 'key') {
|
||||||
timepoints = instance.data.timepointApi.key();
|
timepoints = instance.data.timepointApi.key();
|
||||||
} else {
|
} else {
|
||||||
timepoints = instance.data.timepointApi.latest();
|
timepoints = instance.data.timepointApi.currentAndPrior();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return key timepoints
|
// Return key timepoints
|
||||||
@ -47,14 +49,10 @@ Session.setDefault('NewSeriesLoaded', false);
|
|||||||
|
|
||||||
Template.lesionTable.onRendered(() => {
|
Template.lesionTable.onRendered(() => {
|
||||||
// Find the first measurement by Lesion Number
|
// Find the first measurement by Lesion Number
|
||||||
var firstLesion = Measurements.findOne({}, {
|
const firstLesion = MeasurementApi.firstLesion();
|
||||||
sort: {
|
|
||||||
lesionNumber: 1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create an object to store the ContentId inside
|
// Create an object to store the ContentId inside
|
||||||
var templateData = {
|
const templateData = {
|
||||||
contentId: Session.get('activeContentId')
|
contentId: Session.get('activeContentId')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
Template.lesionTableHUD.onCreated(() => {
|
Template.lesionTableHUD.onCreated(() => {
|
||||||
const instance = Template.instance();
|
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(() => {
|
Template.lesionTableHUD.onRendered(() => {
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
<template name="lesionTableHeaderRow">
|
<template name="lesionTableHeaderRow">
|
||||||
<div class="lesionTableHeaderRow">
|
<div class="lesionTableHeaderRow">
|
||||||
|
{{ #if anyUnmarkedLesionsLeft}}
|
||||||
<div class="add js-setTool">
|
<div class="add js-setTool">
|
||||||
<svg>
|
<svg>
|
||||||
<use xlink:href=/packages/viewerbase/assets/icons.svg#icon-ui-add></use>
|
<use xlink:href=/packages/viewerbase/assets/icons.svg#icon-ui-add></use>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
{{ /if }}
|
||||||
<div class="type">
|
<div class="type">
|
||||||
{{type}}
|
{{type}}
|
||||||
</div>
|
</div>
|
||||||
<div class="max">
|
<div class="max {{ # if gt numberOfLesions maxNumLesions }}warning{{/if}}">
|
||||||
{{ #if maxNumLesions }}
|
{{ #if maxNumLesions }}
|
||||||
<p class="maxNumLesions">
|
<p class="maxNumLesions">
|
||||||
Max {{maxNumLesions}}
|
Max {{maxNumLesions}}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { MeasurementApi } from 'meteor/lesiontracker/client/api/measurement';
|
||||||
|
|
||||||
const toolTypesById = {
|
const toolTypesById = {
|
||||||
target: 'bidirectional',
|
target: 'bidirectional',
|
||||||
nonTarget: 'nonTarget',
|
nonTarget: 'nonTarget',
|
||||||
@ -14,21 +16,14 @@ Template.lesionTableHeaderRow.onCreated(() => {
|
|||||||
const instance = Template.instance();
|
const instance = Template.instance();
|
||||||
instance.maxNumLesions = new ReactiveVar();
|
instance.maxNumLesions = new ReactiveVar();
|
||||||
|
|
||||||
|
const current = instance.data.timepointApi.current();
|
||||||
|
const timepointType = current.timepointType;
|
||||||
|
|
||||||
if (!instance.data.currentTimepointId) {
|
if (!instance.data.currentTimepointId) {
|
||||||
console.warn('Case has no timepointId');
|
console.warn('Case has no timepointId');
|
||||||
return;
|
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
|
// TODO: Check if we have criteria where maximum limits are applied to
|
||||||
// Non-Targets and/or New Lesions
|
// Non-Targets and/or New Lesions
|
||||||
if (timepointType === 'baseline' && instance.data.id === 'target') {
|
if (timepointType === 'baseline' && instance.data.id === 'target') {
|
||||||
@ -57,17 +52,35 @@ Template.lesionTableHeaderRow.onCreated(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Template.lesionTableHeaderRow.helpers({
|
Template.lesionTableHeaderRow.helpers({
|
||||||
numberOfLesions: function() {
|
type() {
|
||||||
const instance = Template.instance();
|
// Give the header a proper name
|
||||||
return instance.data.measurements.count();
|
const id = Template.instance().data.id;
|
||||||
|
return namesById[id];
|
||||||
},
|
},
|
||||||
maxNumLesions: function() {
|
|
||||||
|
numberOfLesions() {
|
||||||
|
return Template.instance().data.measurements.length;
|
||||||
|
},
|
||||||
|
|
||||||
|
maxNumLesions() {
|
||||||
return Template.instance().maxNumLesions.get();
|
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({
|
Template.lesionTableHeaderRow.events({
|
||||||
'click .js-setTool': function(event, instance) {
|
'click .js-setTool'(event, instance) {
|
||||||
const id = instance.data.id;
|
const id = instance.data.id;
|
||||||
const toolType = toolTypesById[id];
|
const toolType = toolTypesById[id];
|
||||||
toolManager.setActiveTool(toolType);
|
toolManager.setActiveTool(toolType);
|
||||||
|
|||||||
@ -63,7 +63,7 @@ $headerRowHeight = 63px
|
|||||||
text-align: right
|
text-align: right
|
||||||
|
|
||||||
.maxNumLesions
|
.maxNumLesions
|
||||||
background-color: $textSecondaryColor;
|
background-color: $textSecondaryColor
|
||||||
border-radius: 3px
|
border-radius: 3px
|
||||||
color: black
|
color: black
|
||||||
display: table
|
display: table
|
||||||
@ -75,3 +75,9 @@ $headerRowHeight = 63px
|
|||||||
margin-top: 22px
|
margin-top: 22px
|
||||||
padding: 2px 6px 0
|
padding: 2px 6px 0
|
||||||
text-transform: uppercase
|
text-transform: uppercase
|
||||||
|
transition($sidebarTransition)
|
||||||
|
|
||||||
|
&.warning
|
||||||
|
.maxNumLesions
|
||||||
|
color: $textPrimaryColor
|
||||||
|
background-color: $uiYellow
|
||||||
@ -1,17 +1,26 @@
|
|||||||
<template name="lesionTableView">
|
<template name="lesionTableView">
|
||||||
<div class="lesionTableView scrollArea">
|
<div class="lesionTableView scrollArea">
|
||||||
{{>lesionTableHeaderRow id="target" measurements=targets currentTimepointId=currentTimepointId}}
|
{{>lesionTableHeaderRow id="target"
|
||||||
|
measurements=targets
|
||||||
|
currentTimepointId=currentTimepointId
|
||||||
|
timepointApi=timepointApi}}
|
||||||
{{#each target in targets}}
|
{{#each target in targets}}
|
||||||
{{>lesionTableRow (clone this rowItem=target)}}
|
{{>lesionTableRow (clone this rowItem=target)}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
{{>lesionTableHeaderRow id="nonTarget" measurements=nonTargets currentTimepointId=currentTimepointId}}
|
{{>lesionTableHeaderRow id="nonTarget"
|
||||||
|
measurements=nonTargets
|
||||||
|
currentTimepointId=currentTimepointId
|
||||||
|
timepointApi=timepointApi}}
|
||||||
{{#each nonTarget in nonTargets}}
|
{{#each nonTarget in nonTargets}}
|
||||||
{{>lesionTableRow (clone this rowItem=nonTarget)}}
|
{{>lesionTableRow (clone this rowItem=nonTarget)}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
{{#if gt this.timepoints.get.length 1}}
|
{{#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}}
|
{{#each newLesion in newLesions}}
|
||||||
{{>lesionTableRow (clone this rowItem=newLesion)}}
|
{{>lesionTableRow (clone this rowItem=newLesion)}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|||||||
@ -1,32 +1,23 @@
|
|||||||
|
import { MeasurementApi } from 'meteor/lesiontracker/client/api/measurement';
|
||||||
|
|
||||||
Template.lesionTableView.helpers({
|
Template.lesionTableView.helpers({
|
||||||
targets() {
|
targets() {
|
||||||
// All Targets shall be listed first followed by Non-Targets
|
const withPriors = true;
|
||||||
return Measurements.find({
|
return MeasurementApi.targets(withPriors);
|
||||||
isTarget: true
|
|
||||||
}, {
|
|
||||||
sort: {
|
|
||||||
lesionNumberAbsolute: 1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
nonTargets() {
|
nonTargets() {
|
||||||
return Measurements.find({
|
const withPriors = true;
|
||||||
isTarget: false
|
return MeasurementApi.nonTargets(withPriors);
|
||||||
}, {
|
|
||||||
sort: {
|
|
||||||
lesionNumberAbsolute: 1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
newLesions() {
|
newLesions() {
|
||||||
return Measurements.find({
|
return MeasurementApi.newLesions();
|
||||||
saved: false
|
},
|
||||||
}, {
|
|
||||||
sort: {
|
isFollowup() {
|
||||||
lesionNumberAbsolute: 1
|
const instance = Template.instance();
|
||||||
}
|
const current = instance.data.timepointApi.current();
|
||||||
});
|
return (current && current.timepointType === 'followup');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template name="lesionTrackerViewportOverlay">
|
<template name="lesionTrackerViewportOverlay">
|
||||||
<div class="imageViewerViewportOverlay noselect">
|
<div class="imageViewerViewportOverlay noselect {{#if gt numImages 1}}controlsVisible{{/if}}">
|
||||||
<div class="topleft dicomTag">
|
<div class="topleft dicomTag">
|
||||||
<div>{{formatPN patientName}}</div>
|
<div>{{formatPN patientName}}</div>
|
||||||
<div>{{patientId}}</div>
|
<div>{{patientId}}</div>
|
||||||
@ -16,11 +16,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="bottomleft dicomTag">
|
<div class="bottomleft dicomTag">
|
||||||
<div>Ser: {{seriesNumber}}</div>
|
<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>{{frameRate}}</div>
|
||||||
<div>{{imageDimensions}}</div>
|
<div>{{imageDimensions}}</div>
|
||||||
<div>{{seriesDescription}}</div>
|
<div>{{seriesDescription}}</div>
|
||||||
</div>
|
</div>
|
||||||
{{>imageControls}}
|
{{#if gt numImages 1}}
|
||||||
|
{{> imageControls imageIndex=imageIndex numImages=numImages}}
|
||||||
|
{{ /if }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -1,12 +1,27 @@
|
|||||||
<template name="measureFlow">
|
<template name="measureFlow">
|
||||||
<div class="measure-flow">
|
<div class="measure-flow {{instance.state.get}}" style="position:fixed; top:200px; left:400px">
|
||||||
<button>Add label</button>
|
<button class="btn-add">Add label</button>
|
||||||
{{>selectTree
|
{{#if eq instance.state.get 'selected'}}
|
||||||
key='label'
|
<div class="tree-leaf">
|
||||||
items=instance.items
|
<span>{{instance.value.label}}</span>
|
||||||
label='Assign label'
|
{{#if instance.descriptionEdit.get}}
|
||||||
searchPlaceholder='Search labels'
|
<div class="description clearfix">
|
||||||
storageKey='measureLabelCommon'
|
<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>
|
</div>
|
||||||
</template>
|
</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(() => {
|
Template.measureFlow.onCreated(() => {
|
||||||
const instance = Template.instance();
|
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 = [
|
const items = [
|
||||||
'Adrenal',
|
'Adrenal',
|
||||||
'Bladder',
|
'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({
|
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) {
|
'click .tree-leaf input'(event, instance) {
|
||||||
console.warn('>>>>event', event);
|
const $target = $(event.currentTarget);
|
||||||
const $label = $(event.currentTarget).closest('label');
|
const $label = $target.closest('label');
|
||||||
const $container = $label.closest('.select-tree-root').find('.tree-options:first');
|
const $treeRoot = $label.closest('.select-tree-root');
|
||||||
const top = $label.position().top;
|
const $container = $treeRoot.find('.tree-options:first');
|
||||||
$container.scrollTop(top);
|
|
||||||
|
// 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
|
.measure-flow
|
||||||
margin-right: -20px
|
position: relative
|
||||||
max-height: 250px
|
|
||||||
overflow-x: hidden
|
&.open,
|
||||||
overflow-y: scroll
|
&.selected
|
||||||
position: relative
|
|
||||||
z-index: 2
|
.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
|
.tree-inputs
|
||||||
position: relative
|
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-weight: 700
|
||||||
font-size: 17px
|
font-size: 17px
|
||||||
text-align: center
|
text-align: center
|
||||||
color: $textColorActive
|
color: $activeColor
|
||||||
top: 0
|
top: 0
|
||||||
|
|
||||||
svg
|
svg
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-responsive">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="center">Include Study?</th>
|
<th class="center">Include Study?</th>
|
||||||
|
|||||||
@ -5,15 +5,17 @@
|
|||||||
{{>loadingText}}
|
{{>loadingText}}
|
||||||
<div class="studyModality">
|
<div class="studyModality">
|
||||||
<div class="studyModalityBox">
|
<div class="studyModalityBox">
|
||||||
{{#if this.study.modalities}}
|
<div class="studyModalityText" style="{{modalityStyle}}">
|
||||||
{{this.study.modalities}}
|
{{#if this.study.modalities}}
|
||||||
{{else}}
|
{{ modalities }}
|
||||||
UN
|
{{else}}
|
||||||
{{/if}}
|
UN
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="studyModalityText">
|
<div class="studyText">
|
||||||
<div class="studyModalityDate">{{formatDA this.study.studyDate 'D-MMM-YYYY'}}</div>
|
<div class="studyDate">{{formatDA this.study.studyDate 'D-MMM-YYYY'}}</div>
|
||||||
<div class="studyModalityDescription">{{this.study.studyDescription}}</div>
|
<div class="studyDescription">{{this.study.studyDescription}}</div>
|
||||||
</div>
|
</div>
|
||||||
</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({
|
Template.studyTimepointStudy.events({
|
||||||
// Recalculates the timepoint height to make CSS transition smoother
|
// Recalculates the timepoint height to make CSS transition smoother
|
||||||
'transitionend .studyTimepointThumbnails'(event, instance) {
|
'transitionend .studyTimepointThumbnails'(event, instance) {
|
||||||
|
|||||||
@ -80,29 +80,31 @@ $spacerY = 12px
|
|||||||
padding: 0
|
padding: 0
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
||||||
.studyModalityText
|
.studyText
|
||||||
font-size: 13px
|
font-size: 13px
|
||||||
left: ($spacerX * 3) + $boxWidth + ($nestingMargin * 3)
|
left: ($spacerX * 3) + $boxWidth + ($nestingMargin * 3)
|
||||||
line-height: 14px
|
line-height: 14px
|
||||||
position: absolute
|
position: absolute
|
||||||
right: $spacerX
|
right: $spacerX
|
||||||
top: $spacerY
|
top: $spacerY
|
||||||
.studyModalityDate
|
.studyDate
|
||||||
margin-top: 8px
|
margin-top: 8px
|
||||||
color: $textSecondaryColor
|
color: $textSecondaryColor
|
||||||
.studyModalityDescription
|
.studyDescription
|
||||||
margin-top: 8px
|
margin-top: 8px
|
||||||
color: $textPrimaryColor
|
color: $textPrimaryColor
|
||||||
|
|
||||||
.studyModalityBox
|
.studyModalityBox
|
||||||
color: $textSecondaryColor
|
color: $textSecondaryColor
|
||||||
font-size: 22px
|
|
||||||
line-height: $boxWidth
|
line-height: $boxWidth
|
||||||
margin-left: $nestingMargin * 2
|
margin-left: $nestingMargin * 2
|
||||||
margin-top: $nestingMargin * 2
|
margin-top: $nestingMargin * 2
|
||||||
position: relative
|
position: relative
|
||||||
text-align: center
|
|
||||||
text-transform: uppercase
|
.studyModalityText
|
||||||
|
text-align: center
|
||||||
|
text-transform: uppercase
|
||||||
|
height: 100%
|
||||||
|
|
||||||
&
|
&
|
||||||
&:before
|
&:before
|
||||||
|
|||||||
@ -133,19 +133,16 @@ Template.toolbarSection.helpers({
|
|||||||
|
|
||||||
return buttonData;
|
return buttonData;
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Template.toolbarSection.events({
|
Template.toolbarSection.events({
|
||||||
'click #toggleHUD'(event, instance) {
|
'click #toggleHUD'() {
|
||||||
const state = Session.get('lesionTableHudOpen');
|
const state = Session.get('lesionTableHudOpen');
|
||||||
Session.set('lesionTableHudOpen', !state);
|
Session.set('lesionTableHudOpen', !state);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Template.toolbarSection.onRendered(function() {
|
Template.toolbarSection.onRendered(function() {
|
||||||
const instance = Template.instance();
|
|
||||||
|
|
||||||
// Set disabled/enabled tool buttons that are set in toolManager
|
// Set disabled/enabled tool buttons that are set in toolManager
|
||||||
const states = toolManager.getToolDefaultStates();
|
const states = toolManager.getToolDefaultStates();
|
||||||
const disabledToolButtons = states.disabledToolButtons;
|
const disabledToolButtons = states.disabledToolButtons;
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
height: 100%
|
height: 100%
|
||||||
|
|
||||||
.dropdown-toggle
|
.dropdown-toggle
|
||||||
background-color: $primaryBackgroundColor
|
|
||||||
color: $defaultColor
|
color: $defaultColor
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
font-size: 13px
|
font-size: 13px
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { OHIF } from 'meteor/ohif:core';
|
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 = OHIF.viewer || {};
|
||||||
OHIF.viewer.loadIndicatorDelay = 3000;
|
OHIF.viewer.loadIndicatorDelay = 3000;
|
||||||
@ -18,8 +19,6 @@ Session.setDefault('rightSidebar', null);
|
|||||||
Template.viewer.onCreated(() => {
|
Template.viewer.onCreated(() => {
|
||||||
const instance = Template.instance();
|
const instance = Template.instance();
|
||||||
|
|
||||||
instance.data.timepointApi = new TimepointApi();
|
|
||||||
|
|
||||||
ValidationErrors.remove({});
|
ValidationErrors.remove({});
|
||||||
|
|
||||||
instance.data.state = new ReactiveDict();
|
instance.data.state = new ReactiveDict();
|
||||||
@ -91,6 +90,17 @@ Template.viewer.onCreated(() => {
|
|||||||
// Set buttons as enabled/disabled when Timepoints collection is ready
|
// Set buttons as enabled/disabled when Timepoints collection is ready
|
||||||
timepointAutoCheck(dataContext);
|
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();
|
TrialResponseCriteria.validateAllDelayed();
|
||||||
|
|
||||||
ViewerStudies.find().observe({
|
ViewerStudies.find().observe({
|
||||||
|
|||||||
@ -11,8 +11,9 @@ handleMeasurementAdded = function(e, eventData) {
|
|||||||
TrialResponseCriteria.validateDelayed(measurementData);
|
TrialResponseCriteria.validateDelayed(measurementData);
|
||||||
// Set reviewer for this timepoint
|
// Set reviewer for this timepoint
|
||||||
if (measurementData.studyInstanceUid) {
|
if (measurementData.studyInstanceUid) {
|
||||||
Meteor.call('setReviewer',measurementData.studyInstanceUid);
|
Meteor.call('setReviewer', measurementData.studyInstanceUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'ellipticalRoi':
|
case 'ellipticalRoi':
|
||||||
case 'length':
|
case 'length':
|
||||||
|
|||||||
@ -18,7 +18,7 @@ handleMeasurementRemoved = function(e, eventData) {
|
|||||||
}
|
}
|
||||||
// Set reviewer for this timepoint
|
// Set reviewer for this timepoint
|
||||||
if (measurementData.studyInstanceUid) {
|
if (measurementData.studyInstanceUid) {
|
||||||
Meteor.call('setReviewer',measurementData.studyInstanceUid);
|
Meteor.call('setReviewer', measurementData.studyInstanceUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearMeasurementTimepointData(measurement._id, measurementData.timepointId);
|
clearMeasurementTimepointData(measurement._id, measurementData.timepointId);
|
||||||
|
|||||||
@ -265,7 +265,8 @@ Package.onUse(function(api) {
|
|||||||
api.addFiles('client/components/serverInformation/serverInformationModal/serverInformationModal.js', 'client');
|
api.addFiles('client/components/serverInformation/serverInformationModal/serverInformationModal.js', 'client');
|
||||||
|
|
||||||
// API classes
|
// API classes
|
||||||
api.addFiles('lib/api/timepoint.js');
|
api.addFiles('client/api/timepoint.js');
|
||||||
|
api.addFiles('client/api/measurement.js');
|
||||||
|
|
||||||
// Export global functions
|
// Export global functions
|
||||||
api.export('pixelSpacingAutorunCheck', 'client');
|
api.export('pixelSpacingAutorunCheck', 'client');
|
||||||
|
|||||||
@ -15,6 +15,9 @@ OHIF.mixins.selectTree = new OHIF.Mixin({
|
|||||||
const rootComponent = instance.data.root || component;
|
const rootComponent = instance.data.root || component;
|
||||||
const rootInstance = rootComponent.templateInstance;
|
const rootInstance = rootComponent.templateInstance;
|
||||||
|
|
||||||
|
// Store the component's current value
|
||||||
|
instance.currentValue = null;
|
||||||
|
|
||||||
// Create the component's value storage property
|
// Create the component's value storage property
|
||||||
instance.data.currentNode = new ReactiveVar(null);
|
instance.data.currentNode = new ReactiveVar(null);
|
||||||
|
|
||||||
@ -25,7 +28,23 @@ OHIF.mixins.selectTree = new OHIF.Mixin({
|
|||||||
component.value = value => {
|
component.value = value => {
|
||||||
const isGet = _.isUndefined(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) {
|
if (isGet) {
|
||||||
const currentNode = instance.data.currentNode.get();
|
const currentNode = instance.data.currentNode.get();
|
||||||
return currentNode ? currentNode.value : null;
|
return currentNode ? currentNode.value : null;
|
||||||
@ -35,12 +54,12 @@ OHIF.mixins.selectTree = new OHIF.Mixin({
|
|||||||
const items = component.templateInstance.data.items;
|
const items = component.templateInstance.data.items;
|
||||||
|
|
||||||
// Get the node for the selected value
|
// Get the node for the selected value
|
||||||
const node = _.findWhere(items, {
|
const currentNode = _.findWhere(items, {
|
||||||
value: value
|
value: node
|
||||||
});
|
});
|
||||||
|
|
||||||
// Change the current 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
|
// 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);
|
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))
|
id=(concat this.storageKey '_' (encodeId item.value))
|
||||||
class=this.radioClass
|
class=this.radioClass
|
||||||
name=this.key
|
name=this.key
|
||||||
|
itemData=item
|
||||||
value=item.value
|
value=item.value
|
||||||
label=item.label
|
label=item.label
|
||||||
labelClass=(valueIf (isArray item.items) 'tree-node' 'tree-leaf')
|
labelClass=(valueIf (isArray item.items) 'tree-node' 'tree-leaf')
|
||||||
|
|||||||
@ -27,8 +27,31 @@ Template.selectTree.onRendered(() => {
|
|||||||
// Set the margin to display the common section
|
// Set the margin to display the common section
|
||||||
$treeRoot.children('.tree-content').css('margin-right', $treeRoot.width());
|
$treeRoot.children('.tree-content').css('margin-right', $treeRoot.width());
|
||||||
|
|
||||||
// Start the component transitions
|
// Make the component respect the window boundaries
|
||||||
$treeRoot.addClass('started');
|
$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
|
// Update the component's viewport height
|
||||||
instance.updateHeight = searchTerm => {
|
instance.updateHeight = searchTerm => {
|
||||||
@ -43,8 +66,9 @@ Template.selectTree.onRendered(() => {
|
|||||||
height = rootInstance.$('.tree-options:last').data('height');
|
height = rootInstance.$('.tree-options:last').data('height');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the viewport's height
|
// Update the viewport's height and trigger the bounded event
|
||||||
rootInstance.$('.tree-options:first').height(height);
|
rootInstance.$('.tree-options:first').first().height(height)
|
||||||
|
.on('transitionend', () => $treeRoot.trigger('spatialChanged'));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the opened node
|
// Update the opened node
|
||||||
@ -110,23 +134,29 @@ Template.selectTree.events({
|
|||||||
},
|
},
|
||||||
|
|
||||||
'input .tree-search input'(event, instance) {
|
'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
|
// Change the search term to update the tree items
|
||||||
instance.searchTerm.set($(event.currentTarget).val());
|
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) {
|
'change .select-tree:first>.tree-content>.tree-options>.tree-inputs>label>input'(event, instance) {
|
||||||
const component = instance.component;
|
const component = instance.component;
|
||||||
const $target = $(event.target);
|
const $target = $(event.currentTarget);
|
||||||
const $label = $target.closest('label');
|
const $label = $target.closest('label');
|
||||||
const eventComponent = $target.data('component');
|
const eventComponent = $target.data('component');
|
||||||
const rootComponent = instance.data.root || component;
|
const rootComponent = instance.data.root || component;
|
||||||
const rootInstance = rootComponent.templateInstance;
|
const rootInstance = rootComponent.templateInstance;
|
||||||
|
|
||||||
// Change the component's value
|
// Change the component's node
|
||||||
component.value(eventComponent.value());
|
component.node(eventComponent.value());
|
||||||
|
|
||||||
// Unset the active leaf
|
// Unset the active leaf
|
||||||
rootInstance.$('label').removeClass('active');
|
rootInstance.$('label').removeClass('active').css('width', '');
|
||||||
|
|
||||||
// Unset the selected state
|
// Unset the selected state
|
||||||
instance.setSelected(false);
|
instance.setSelected(false);
|
||||||
@ -135,7 +165,7 @@ Template.selectTree.events({
|
|||||||
const isLeaf = $label.hasClass('tree-leaf');
|
const isLeaf = $label.hasClass('tree-leaf');
|
||||||
if (isLeaf) {
|
if (isLeaf) {
|
||||||
// Change the active item
|
// Change the active item
|
||||||
$label.addClass('active');
|
$label.css('width', $label.outerWidth()).addClass('active');
|
||||||
|
|
||||||
// Ckeck if there's a storageKey defined
|
// Ckeck if there's a storageKey defined
|
||||||
const storageKey = instance.data.storageKey;
|
const storageKey = instance.data.storageKey;
|
||||||
@ -151,6 +181,10 @@ Template.selectTree.events({
|
|||||||
storedData[itemKey] = 1;
|
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
|
// Updata the stored data with the new count
|
||||||
OHIF.user.setData(storageKey, storedData);
|
OHIF.user.setData(storageKey, storedData);
|
||||||
}
|
}
|
||||||
@ -201,6 +235,27 @@ Template.selectTree.events({
|
|||||||
currentInstance = currentInstance.component.parent.templateInstance;
|
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 './base';
|
||||||
import './bootstrap';
|
import './bootstrap';
|
||||||
import './playground/playground.html';
|
import './playground/playground.html';
|
||||||
|
import './playground/playground.styl';
|
||||||
import './playground/playground.js';
|
import './playground/playground.js';
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
<template name="componentPlayground">
|
<template name="componentPlayground">
|
||||||
<!--
|
<div class="component-playground">
|
||||||
<div class="p-a-3">
|
{{>measureFlow}}
|
||||||
{{>selectTree
|
<!--
|
||||||
key='label'
|
<div class="p-a-3">
|
||||||
items=instance.items
|
{{>selectTree
|
||||||
label='Assign label'
|
key='label'
|
||||||
searchPlaceholder='Search labels'
|
items=instance.items
|
||||||
storageKey='measureLabelCommon'
|
label='Assign label'
|
||||||
}}
|
searchPlaceholder='Search labels'
|
||||||
|
storageKey='measureLabelCommon'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
-->
|
|
||||||
{{>measureFlow}}
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -42,3 +42,8 @@ Template.componentPlayground.onCreated(() => {
|
|||||||
value: 'Category 3'
|
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
|
// Search for some string inside any object or array
|
||||||
OHIF.string.search = (object, query, property=null, result=[]) => {
|
OHIF.string.search = (object, query, property=null, result=[]) => {
|
||||||
// Create the search pattern
|
// Create the search pattern
|
||||||
const pattern = new RegExp($.trim(query).replace(/\s/gi, '|'), 'i');
|
const pattern = new RegExp($.trim(query), 'i');
|
||||||
|
|
||||||
_.each(object, item => {
|
_.each(object, item => {
|
||||||
// Stop here if item is empty
|
// Stop here if item is empty
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { OHIF } from 'meteor/ohif:core';
|
||||||
|
|
||||||
// Allow attaching to jQuery selectors
|
// Allow attaching to jQuery selectors
|
||||||
$.fn.bounded = function(options) {
|
$.fn.bounded = function(options) {
|
||||||
_.each(this, element => {
|
_.each(this, element => {
|
||||||
@ -63,7 +65,7 @@ class Bounded {
|
|||||||
this.$element.removeClass('bounded');
|
this.$element.removeClass('bounded');
|
||||||
}
|
}
|
||||||
|
|
||||||
spatialInfo(element) {
|
static spatialInfo(element) {
|
||||||
// Create the result object
|
// Create the result object
|
||||||
const result = {};
|
const result = {};
|
||||||
|
|
||||||
@ -105,8 +107,8 @@ class Bounded {
|
|||||||
defineEventHandlers() {
|
defineEventHandlers() {
|
||||||
this.spatialChangedHandler = event => {
|
this.spatialChangedHandler = event => {
|
||||||
// Get the spatial information for element and its bounding element
|
// Get the spatial information for element and its bounding element
|
||||||
const elementInfo = this.spatialInfo(this.element);
|
const elementInfo = Bounded.spatialInfo(this.element);
|
||||||
const boundingInfo = this.spatialInfo(this.boundingElement);
|
const boundingInfo = Bounded.spatialInfo(this.boundingElement);
|
||||||
|
|
||||||
// Fix element's x positioning and width
|
// Fix element's x positioning and width
|
||||||
if (this.allowResizing && elementInfo.width > boundingInfo.width) {
|
if (this.allowResizing && elementInfo.width > boundingInfo.width) {
|
||||||
@ -143,3 +145,5 @@ class Bounded {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OHIF.Bounded = Bounded;
|
||||||
|
|||||||
@ -2,4 +2,6 @@
|
|||||||
|
|
||||||
#imageViewerViewports
|
#imageViewerViewports
|
||||||
height: 100%
|
height: 100%
|
||||||
|
padding-bottom: 1px
|
||||||
|
padding-right: 6px
|
||||||
width: 100%
|
width: 100%
|
||||||
@ -1,7 +1,11 @@
|
|||||||
<template name="imageControls">
|
<template name="imageControls">
|
||||||
<div class="imageControls">
|
<div class="imageControls">
|
||||||
<div id="scrollbar">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -1,6 +1,20 @@
|
|||||||
const slideTimeoutTime = 40;
|
const slideTimeoutTime = 40;
|
||||||
let slideTimeout;
|
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({
|
Template.imageControls.events({
|
||||||
'input #imageSlider, change #imageSlider': function(e) {
|
'input #imageSlider, change #imageSlider': function(e) {
|
||||||
// Note that we throttle requests to prevent the
|
// Note that we throttle requests to prevent the
|
||||||
|
|||||||
@ -17,6 +17,7 @@ $imageSliderCursor = grab
|
|||||||
padding: 5px
|
padding: 5px
|
||||||
|
|
||||||
#scrollbar
|
#scrollbar
|
||||||
|
height: calc(100% - 20px);
|
||||||
margin-top: 5px
|
margin-top: 5px
|
||||||
width: 31px
|
width: 31px
|
||||||
|
|
||||||
|
|||||||
@ -86,32 +86,6 @@ function loadDisplaySetIntoViewport(data, templateData) {
|
|||||||
imageIds: imageIds
|
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
|
// Get the current image ID for the stack that will be rendered
|
||||||
imageId = imageIds[stack.currentImageIdIndex];
|
imageId = imageIds[stack.currentImageIdIndex];
|
||||||
|
|
||||||
@ -296,11 +270,6 @@ function loadDisplaySetIntoViewport(data, templateData) {
|
|||||||
|
|
||||||
var stack = toolData.data[0];
|
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
|
// 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.
|
// index in the stack to the global ViewerData object, as well as the Meteor Session.
|
||||||
var stack = cornerstoneTools.getToolState(element, 'stack');
|
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
|
// Set a random value for the Session variable in order to trigger an overlay update
|
||||||
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
|
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
|
// 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
|
// This is used to update the value of the "active viewport", when the user interacts
|
||||||
// with a new viewport element
|
// with a new viewport element
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template name="viewportOverlay">
|
<template name="viewportOverlay">
|
||||||
<div class="imageViewerViewportOverlay noselect">
|
<div class="imageViewerViewportOverlay noselect {{#if gt numImages 1}}controlsVisible{{/if}}">
|
||||||
{{ #unless tagDisplaySpecified }}
|
{{ #unless tagDisplaySpecified }}
|
||||||
<div class="topleft dicomTag">
|
<div class="topleft dicomTag">
|
||||||
<div>{{formatPN patientName}}</div>
|
<div>{{formatPN patientName}}</div>
|
||||||
@ -11,15 +11,19 @@
|
|||||||
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
|
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bottomright dicomTag">
|
<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>{{compression}}</div>
|
||||||
<div>{{wwwc}}</div>
|
<div>{{wwwc}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bottomleft dicomTag">
|
<div class="bottomleft dicomTag">
|
||||||
<div>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
|
<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>{{#if frameRate}}{{frameRate}} FPS{{/if}}</div>
|
||||||
<div>{{imageDimensions}}</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>{{seriesDescription}}</div>
|
||||||
</div>
|
</div>
|
||||||
{{ /unless }}
|
{{ /unless }}
|
||||||
@ -34,11 +38,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="bottomright dicomTag">
|
<div class="bottomright dicomTag">
|
||||||
<div>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
|
<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>{{#if frameRate}}{{frameRate}} FPS{{/if}}</div>
|
||||||
<div>{{imageDimensions}}</div>
|
<div>{{imageDimensions}}</div>
|
||||||
<div>{{seriesDescription}}</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>{{compression}}</div>
|
||||||
<div>{{wwwc}}</div>
|
<div>{{wwwc}}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -54,15 +58,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="bottomleft dicomTag">
|
<div class="bottomleft dicomTag">
|
||||||
<div>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
|
<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>{{#if frameRate}}{{frameRate}} FPS{{/if}}</div>
|
||||||
<div>{{imageDimensions}}</div>
|
<div>{{imageDimensions}}</div>
|
||||||
<div>{{seriesDescription}}</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>{{compression}}</div>
|
||||||
<div>{{wwwc}}</div>
|
<div>{{wwwc}}</div>
|
||||||
</div>
|
</div>
|
||||||
{{ /if }}
|
{{ /if }}
|
||||||
{{>imageControls}}
|
|
||||||
|
{{#if gt numImages 1}}
|
||||||
|
{{> imageControls imageIndex=imageIndex numImages=numImages}}
|
||||||
|
{{ /if }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -180,6 +180,12 @@ Template.viewportOverlay.helpers({
|
|||||||
patientId: function() {
|
patientId: function() {
|
||||||
return getPatient.call(this, 'id');
|
return getPatient.call(this, 'id');
|
||||||
},
|
},
|
||||||
|
patientBirthDate: function() {
|
||||||
|
return getPatient.call(this, 'birthDate');
|
||||||
|
},
|
||||||
|
patientSex: function() {
|
||||||
|
return getPatient.call(this, 'sex');
|
||||||
|
},
|
||||||
studyDate: function() {
|
studyDate: function() {
|
||||||
return getStudy.call(this, 'studyDate');
|
return getStudy.call(this, 'studyDate');
|
||||||
},
|
},
|
||||||
@ -204,9 +210,67 @@ Template.viewportOverlay.helpers({
|
|||||||
seriesNumber: function() {
|
seriesNumber: function() {
|
||||||
return getSeries.call(this, 'seriesNumber');
|
return getSeries.call(this, 'seriesNumber');
|
||||||
},
|
},
|
||||||
imageNumber: function() {
|
instanceNumber: function() {
|
||||||
return getInstance.call(this, 'instanceNumber');
|
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() {
|
tagDisplayLeftOnly: function() {
|
||||||
return getTagDisplay.call(this, 'side') === 'L';
|
return getTagDisplay.call(this, 'side') === 'L';
|
||||||
},
|
},
|
||||||
@ -216,12 +280,6 @@ Template.viewportOverlay.helpers({
|
|||||||
tagDisplaySpecified: function() {
|
tagDisplaySpecified: function() {
|
||||||
return getTagDisplay.call(this, 'side');
|
return getTagDisplay.call(this, 'side');
|
||||||
},
|
},
|
||||||
imageIndex: function() {
|
|
||||||
return getInstance.call(this, 'index');
|
|
||||||
},
|
|
||||||
numImages: function() {
|
|
||||||
return getSeries.call(this, 'numImages');
|
|
||||||
},
|
|
||||||
imageIndex: function() {
|
imageIndex: function() {
|
||||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||||
var stack = getStackDataIfNotEmpty(this.viewportIndex);
|
var stack = getStackDataIfNotEmpty(this.viewportIndex);
|
||||||
|
|||||||
@ -31,3 +31,7 @@ $viewportTagPadding = 20px
|
|||||||
.priorIndicator
|
.priorIndicator
|
||||||
font-weight: bold
|
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,
|
studyInstanceUid: studyMetaData.studyInstanceUid,
|
||||||
studyDate: studyMetaData.studyDate,
|
studyDate: studyMetaData.studyDate,
|
||||||
studyTime: studyMetaData.studyTime,
|
studyTime: studyMetaData.studyTime,
|
||||||
studyDescription: studyMetaData.studyDescription
|
studyDescription: studyMetaData.studyDescription,
|
||||||
|
institutionName: studyMetaData.institutionName
|
||||||
};
|
};
|
||||||
|
|
||||||
metaData.series = {
|
metaData.series = {
|
||||||
@ -37,23 +38,13 @@ addMetaData = function(imageId, data) {
|
|||||||
numImages: numImages
|
numImages: numImages
|
||||||
};
|
};
|
||||||
|
|
||||||
metaData.instance = {
|
metaData.instance = instanceMetaData;
|
||||||
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.patient = {
|
metaData.patient = {
|
||||||
name: studyMetaData.patientName,
|
name: studyMetaData.patientName,
|
||||||
id: studyMetaData.patientId
|
id: studyMetaData.patientId,
|
||||||
|
birthDate: studyMetaData.patientBirthDate,
|
||||||
|
sex: studyMetaData.patientSex
|
||||||
};
|
};
|
||||||
|
|
||||||
// If there is sufficient information, populate
|
// If there is sufficient information, populate
|
||||||
|
|||||||
@ -37,13 +37,19 @@ function configureTools() {
|
|||||||
cornerstoneTools.toolColors.setActiveColor('#00ffff'); //rgb(0, 255, 0)'
|
cornerstoneTools.toolColors.setActiveColor('#00ffff'); //rgb(0, 255, 0)'
|
||||||
|
|
||||||
// Set the configuration values for the text annotation (Arrow) tool
|
// Set the configuration values for the text annotation (Arrow) tool
|
||||||
var annotateConfig = {
|
const annotateConfig = {
|
||||||
getTextCallback: getAnnotationTextCallback,
|
getTextCallback: getAnnotationTextCallback,
|
||||||
changeTextCallback: changeAnnotationTextCallback,
|
changeTextCallback: changeAnnotationTextCallback,
|
||||||
drawHandles: false,
|
drawHandles: false,
|
||||||
arrowFirst: true
|
arrowFirst: true
|
||||||
};
|
};
|
||||||
cornerstoneTools.arrowAnnotate.setConfiguration(annotateConfig);
|
cornerstoneTools.arrowAnnotate.setConfiguration(annotateConfig);
|
||||||
|
|
||||||
|
const zoomConfig = {
|
||||||
|
minScale: 0.05,
|
||||||
|
maxScale: 10
|
||||||
|
};
|
||||||
|
cornerstoneTools.zoom.setConfiguration(zoomConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
toolManager = {
|
toolManager = {
|
||||||
|
|||||||
@ -43,12 +43,15 @@ function resultDataToStudyMetadata(studyInstanceUid, resultData) {
|
|||||||
seriesList: seriesList,
|
seriesList: seriesList,
|
||||||
patientName: anInstance[0x00100010],
|
patientName: anInstance[0x00100010],
|
||||||
patientId: anInstance[0x00100020],
|
patientId: anInstance[0x00100020],
|
||||||
|
patientBirthDate: anInstance[0x00100030],
|
||||||
|
patientSex: anInstance[0x00100040],
|
||||||
accessionNumber: anInstance[0x00080050],
|
accessionNumber: anInstance[0x00080050],
|
||||||
studyDate: anInstance[0x00080020],
|
studyDate: anInstance[0x00080020],
|
||||||
modalities: anInstance[0x00080061],
|
modalities: anInstance[0x00080061],
|
||||||
studyDescription: anInstance[0x00081030],
|
studyDescription: anInstance[0x00081030],
|
||||||
imageCount: anInstance[0x00201208],
|
imageCount: anInstance[0x00201208],
|
||||||
studyInstanceUid: anInstance[0x0020000D]
|
studyInstanceUid: anInstance[0x0020000D],
|
||||||
|
institutionName: anInstance[0x00080080]
|
||||||
};
|
};
|
||||||
|
|
||||||
resultData.forEach(function(instanceRaw) {
|
resultData.forEach(function(instanceRaw) {
|
||||||
@ -78,7 +81,9 @@ function resultDataToStudyMetadata(studyInstanceUid, resultData) {
|
|||||||
imagePositionPatient: instance[0x00200032],
|
imagePositionPatient: instance[0x00200032],
|
||||||
imageOrientationPatient: instance[0x00200037],
|
imageOrientationPatient: instance[0x00200037],
|
||||||
frameOfReferenceUID: instance[0x00200052],
|
frameOfReferenceUID: instance[0x00200052],
|
||||||
|
sliceThickness: parseFloat(instance[0x00180050]),
|
||||||
sliceLocation: parseFloat(instance[0x00201041]),
|
sliceLocation: parseFloat(instance[0x00201041]),
|
||||||
|
tablePosition: parseFloat(instance[0x00189327]),
|
||||||
samplesPerPixel: parseFloat(instance[0x00280002]),
|
samplesPerPixel: parseFloat(instance[0x00280002]),
|
||||||
photometricInterpretation: instance[0x00280004],
|
photometricInterpretation: instance[0x00280004],
|
||||||
rows: parseFloat(instance[0x00280010]),
|
rows: parseFloat(instance[0x00280010]),
|
||||||
@ -96,7 +101,12 @@ function resultDataToStudyMetadata(studyInstanceUid, resultData) {
|
|||||||
laterality: instance[0x00200062],
|
laterality: instance[0x00200062],
|
||||||
viewPosition: instance[0x00185101],
|
viewPosition: instance[0x00185101],
|
||||||
numFrames: parseFloat(instance[0x00280008]),
|
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
|
// Retrieve the actual data over WADO-URI
|
||||||
|
|||||||
@ -59,7 +59,8 @@ function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
|||||||
modalities: DICOMWeb.getString(anInstance['00080061']),
|
modalities: DICOMWeb.getString(anInstance['00080061']),
|
||||||
studyDescription: DICOMWeb.getString(anInstance['00081030']),
|
studyDescription: DICOMWeb.getString(anInstance['00081030']),
|
||||||
imageCount: DICOMWeb.getString(anInstance['00201208']),
|
imageCount: DICOMWeb.getString(anInstance['00201208']),
|
||||||
studyInstanceUid: DICOMWeb.getString(anInstance['0020000D'])
|
studyInstanceUid: DICOMWeb.getString(anInstance['0020000D']),
|
||||||
|
institutionName: DICOMWeb.getString(anInstance['00080080'])
|
||||||
};
|
};
|
||||||
|
|
||||||
resultData.forEach(function(instance) {
|
resultData.forEach(function(instance) {
|
||||||
@ -107,7 +108,11 @@ function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
|||||||
viewPosition: DICOMWeb.getString(instance['00185101']),
|
viewPosition: DICOMWeb.getString(instance['00185101']),
|
||||||
numFrames: DICOMWeb.getNumber(instance['00280008']),
|
numFrames: DICOMWeb.getNumber(instance['00280008']),
|
||||||
frameTime: DICOMWeb.getNumber(instance['00181063']),
|
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') {
|
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