This commit is contained in:
Weiwei 2015-11-16 12:49:55 -06:00
commit 4f1fdfab3e
142 changed files with 6190 additions and 3331 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
.idea/
docs/
.meteor/local
.meteor/meteorite
node_modules

View File

@ -34,4 +34,7 @@ stylus
fortawesome:fontawesome
random
reactive-var
reactive-dict
lesiontracker
practicalmeteor:loglevel
momentjs:moment

View File

@ -1 +1 @@
METEOR@1.2.0.2
METEOR@1.2.1

View File

@ -1,9 +1,9 @@
accounts-base@1.2.1
accounts-password@1.1.3
accounts-base@1.2.2
accounts-password@1.1.4
anti:i18n@0.4.3
arsnebula:reactive-promise@0.9.1
autopublish@1.0.4
autoupdate@1.2.3
autoupdate@1.2.4
babel-compiler@5.8.24_1
babel-runtime@0.1.4
base64@1.0.4
@ -15,21 +15,22 @@ boilerplate-generator@1.0.4
caching-compiler@1.0.0
caching-html-compiler@1.0.2
callback-hook@1.0.4
check@1.0.6
check@1.1.0
coffeescript@1.0.11
cornerstone@0.0.1
ddp@1.2.2
ddp-client@1.2.1
ddp-common@1.2.1
ddp-common@1.2.2
ddp-rate-limiter@1.0.0
ddp-server@1.2.1
ddp-server@1.2.2
deps@1.0.9
dicomweb@0.0.1
diff-sequence@1.0.1
ecmascript@0.1.5
ecmascript-collections@0.1.6
ecmascript@0.1.6
ecmascript-runtime@0.2.6
ejson@1.0.7
email@1.0.7
es5-shim@4.1.13
email@1.0.8
es5-shim@4.1.14
fastclick@1.0.7
fortawesome:fontawesome@4.4.0
geojson-utils@1.0.4
@ -37,7 +38,7 @@ hot-code-push@1.0.0
html-tools@1.0.5
htmljs@1.0.5
http@1.1.1
ian:accounts-ui-bootstrap-3@1.2.82
ian:accounts-ui-bootstrap-3@1.2.83
id-map@1.0.4
insecure@1.0.4
iron:controller@1.0.12
@ -54,23 +55,26 @@ lesiontracker@0.0.1
livedata@1.0.15
localstorage@1.0.5
logging@1.0.8
meteor@1.1.9
meteor@1.1.10
meteor-base@1.0.1
meteor-platform@1.2.3
minifiers@1.1.7
minimongo@1.0.10
mobile-experience@1.0.1
mobile-status-bar@1.0.6
mongo@1.1.2
momentjs:moment@2.10.6
mongo@1.1.3
mongo-id@1.0.1
npm-bcrypt@0.7.8_2
npm-mongo@1.4.39_1
observe-sequence@1.0.7
ordered-dict@1.0.4
promise@0.5.0
random@1.0.4
practicalmeteor:chai@1.9.2_3
practicalmeteor:loglevel@1.1.0_3
promise@0.5.1
random@1.0.5
rate-limit@1.0.0
reactive-dict@1.1.2
reactive-dict@1.1.3
reactive-var@1.0.6
reload@1.1.4
retry@1.0.4
@ -82,9 +86,9 @@ spacebars@1.0.7
spacebars-compiler@1.0.7
srp@1.0.4
standard-app-packages@1.0.6
standard-minifiers@1.0.1
stylus@2.511.0_2
templating@1.1.4
standard-minifiers@1.0.2
stylus@2.511.1
templating@1.1.5
templating-tools@1.0.0
tracker@1.0.9
twbs:bootstrap@3.3.5
@ -92,6 +96,6 @@ ui@1.0.8
underscore@1.0.4
url@1.0.5
viewerbase@0.0.1
webapp@1.2.2
webapp@1.2.3
webapp-hashing@1.0.5
worklist@0.0.1

View File

@ -0,0 +1,2 @@
PACKAGE_DIRS=..\Packages
meteor --settings ../config/localhostOrthanc.json

View File

@ -0,0 +1,2 @@
set PACKAGE_DIRS=..\Packages
meteor --settings ../config/medicalConnections.json

View File

@ -0,0 +1,2 @@
set PACKAGE_DIRS=..\Packages
meteor --settings ../config/medkenOrthanc.json

View File

@ -0,0 +1,2 @@
set PACKAGE_DIRS=..\Packages
meteor --settings ../config/siimDCM4CHEE.json

View File

@ -1,5 +1,24 @@
body
background-color: #202020
#search-panel-head
background-color: #8845AB
.tab-content {
height: calc(100% - 40px);
}
table#tblStudyList thead > tr {
background-color: #424242;
color: white;
}
table#tblStudyList tbody > tr:nth-child(even) {
background-color: #888;
color: black;
}
table#tblStudyList tbody > tr:nth-child(odd) {
background-color: #BBB;
color: black;
}
table#tblStudyList tbody > tr:hover{
background-color: #009BD2
color: white
}

View File

@ -3,4 +3,9 @@ Meteor.startup(function() {
mouse: cornerstoneTools.lesion,
touch: cornerstoneTools.lesionTouch
});
toolManager.addTool('nonTarget', {
mouse: cornerstoneTools.nonTarget,
touch: cornerstoneTools.nonTargetTouch
});
});

View File

@ -1,16 +1,9 @@
<template name="layoutLesionTracker">
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Lesion Tracker</a>
</div>
</div>
</nav>
<div class="logoContainer">
<a class="navbar-brand" href="http://ohif.org">
<img src="/images/logo.png">
<h4 class="name">Open Health Imaging Foundation</h4>
</a>
</div>
{{> yield}}
</template>

View File

@ -1,18 +1,23 @@
.navbar
margin-bottom: 0
border-radius: 0px
.logoContainer
position: absolute;
height: 40px;
top: 0;
right: 0;
color: white;
.navbar-form
margin: 0
padding: 15px 3px
.navbar-brand
display: inline-block
padding: 0;
margin-right: 5px;
.navbar-default
-webkit-transition: background-color 200ms linear
-moz-transition: background-color 200ms linear
-o-transition: background-color 200ms linear
-ms-transition: background-color 200ms linear
transition: background-color 200ms linear
@media (min-width: 768px)
.navbar
border-radius: 0px
img
height: 30px
float: left
margin-right: 10px
margin-top: 5px;
h4.name
float: left
margin: 10px 0;
font-family: "Sanchez"
color: #C1C1C1

View File

@ -1,12 +0,0 @@
.viewer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background-color: black;
}
#upperHalf {
height: 75%;
width: 100%;
}

View File

@ -1,10 +1,12 @@
<template name="viewer">
<div id="viewer" class="viewer">
{{ >lesionDialog studies=studies activeViewport=activeViewport}}
<div id="upperHalf">
{{ >hidingPanel studies=studies activeViewport=activeViewport}}
{{ >viewerMain studies=studies activeViewport=activeViewport viewportRows=viewportRows viewportColumns=viewportColumns}}
<div id="viewer">
{{ >lesionLocationDialog }}
{{ >nonTargetLesionDialog}}
{{ >hidingPanel }}
<div id="viewportAndLesionTable">
{{ >viewerMain }}
{{ >lesionTable }}
</div>
{{ >lesionTable studies=studies activeViewport=activeViewport viewportRows=viewportRows viewportColumns=viewportColumns}}
</div>
</template>

View File

@ -1,4 +1,6 @@
function resizeViewports() {
log.info("viewer resizeViewports");
// Handle resizing of image viewer viewports
// For some reason, this seems to need to be on
// another delay, or the resizing won't work properly
@ -15,7 +17,8 @@ function resizeViewports() {
}
Template.viewer.onCreated(function() {
console.log("Image Viewer onCreated");
log.info("viewer onCreated");
OHIF = {
viewer: {}
};
@ -31,6 +34,9 @@ Template.viewer.onCreated(function() {
viewport.invert = !viewport.invert;
cornerstone.setViewport(element, viewport);
},
resetViewport: function(element) {
cornerstone.reset(element);
},
playClip: function(element) {
var viewportIndex = $('.imageViewerViewport').index(element);
var isPlaying = OHIF.viewer.isPlaying[viewportIndex] || false;
@ -55,35 +61,35 @@ Template.viewer.onCreated(function() {
};
}
if (this.data.activeViewport === undefined) {
this.data.activeViewport = new ReactiveVar(0);
}
if (this.data.viewportRows === undefined) {
this.data.viewportRows = new ReactiveVar(1);
}
if (this.data.viewportColumns === undefined) {
this.data.viewportColumns = new ReactiveVar(2);
}
var contentId = this.data.contentId;
// Update the viewer data object
ViewerData[contentId].viewportColumns = this.data.viewportColumns;
ViewerData[contentId].viewportRows = this.data.viewportRows;
ViewerData[contentId].activeViewport = this.data.activeViewport;
Session.set('ViewerData', ViewerData);
if (ViewerData[contentId].loadedSeriesData) {
log.info('Reloading previous loadedSeriesData');
OHIF.viewer.loadedSeriesData = ViewerData[contentId].loadedSeriesData;
if (ViewerData[contentId].viewer) {
OHIF.viewer = ViewerData[contentId].viewer;
} else {
OHIF.viewer.imageViewerLoadedSeriesDictionary = {};
ViewerData[contentId].viewer = OHIF.viewer;
log.info('Setting default ViewerData');
OHIF.viewer.loadedSeriesData = {};
ViewerData[contentId].loadedSeriesData = OHIF.viewer.loadedSeriesData;
// Update the viewer data object
ViewerData[contentId].viewportColumns = 2;
ViewerData[contentId].viewportRows = 1;
ViewerData[contentId].activeViewport = 0;
Session.set('ViewerData', ViewerData);
}
Session.set('activeViewport', ViewerData[contentId].activeViewport || 0);
Session.set("studies", this.data.studies);
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer("CornerstoneNewImage", cornerstoneTools.updateImageSynchronizer);
});
Template.viewer.onDestroyed(function() {
log.info("onDestroyed");
OHIF.viewer.updateImageSynchronizer.destroy();
});

View File

@ -0,0 +1,29 @@
#viewer
width: 100%
height: 100%
margin: 0
padding: 0
background-color: black
#viewportAndLesionTable
height: 100%
width: calc(100% - 120px)
float: left
#imageViewerViewports
.viewportContainer
&.active
border: 2px solid #777
&:hover, &:hover.active
border: 2px solid white
#btnCollapse
background-color: #333
border-color: black
&.active
border-color: #333
.btnCollapseIcon
color: white

View File

@ -1,6 +1,6 @@
<template name="viewerMain">
<div class="viewerMain">
{{ >toolbar toolbarOptions=toolbarOptions activeViewport=activeViewport viewportRows=viewportRows viewportColumns=viewportColumns}}
{{ >imageViewerViewports studies=studies activeViewport=activeViewport viewportRows=viewportRows viewportColumns=viewportColumns}}
{{ >toolbar toolbarOptions=toolbarOptions }}
{{ >imageViewerViewports }}
</div>
</template>

View File

@ -1,12 +1,16 @@
Template.viewerMain.helpers({
studies : function() {
return this.studies;
},
'toolbarOptions': function() {
var toolbarOptions = {};
var buttonData = [];
buttonData.push({
id: 'resetViewport',
title: 'Reset Viewport',
classes: 'imageViewerCommand',
iconClasses: 'fa fa-undo'
});
buttonData.push({
id: 'wwwc',
title: 'WW/WC',
@ -14,13 +18,6 @@ Template.viewerMain.helpers({
iconClasses: 'fa fa-sun-o'
});
buttonData.push({
id: 'invert',
title: 'Invert',
classes: 'imageViewerCommand',
iconClasses: 'fa fa-adjust'
});
buttonData.push({
id: 'zoom',
title: 'Zoom',
@ -42,37 +39,23 @@ Template.viewerMain.helpers({
iconClasses: 'fa fa-bars'
});
buttonData.push({
id: 'length',
title: 'Length Measurement',
classes: 'imageViewerTool',
iconClasses: 'fa fa-arrows-v'
});
buttonData.push({
id: 'angle',
title: 'Angle Measurement',
classes: 'imageViewerTool',
iconClasses: 'fa fa-angle-left'
});
buttonData.push({
id: 'dragProbe',
title: 'Pixel Probe',
classes: 'imageViewerTool',
iconClasses: 'fa fa-dot-circle-o'
});
buttonData.push({
id: 'lesion',
title: 'Lesion Tool',
title: 'Target Tool',
classes: 'imageViewerTool',
iconClasses: 'fa fa-arrows-v'
iconClasses: 'fa fa-arrows-alt'
});
buttonData.push({
id: 'nonTarget',
title: 'Non-Target Tool',
classes: 'imageViewerTool',
iconClasses: 'fa fa-long-arrow-up'
});
toolbarOptions.buttonData = buttonData;
toolbarOptions.includePlayClipButton = true;
toolbarOptions.includeLayoutButton = true;
toolbarOptions.includePlayClipButton = false;
toolbarOptions.includeLayoutButton = false;
return toolbarOptions;
}
});

View File

@ -1,4 +1,3 @@
.viewerMain
width: calc(100% - 120px)
height: 100%
float: left
width: 100%
height: 84%

View File

@ -2,7 +2,7 @@
<meta charset="utf-8">
<title>Lesion Tracker</title>
<meta name="description" content="Prototype viewer application for oncology">
<meta name="viewport" content="user-scalable=no, initial-scale=1, minimal-ui, maximum-scale=1, minimum-scale=1" />
<meta name="viewport" content="user-scalable=no, initial-scale=1,maximum-scale=1, minimum-scale=1" />
<link rel="shortcut icon" type="image/png" href="images/logo.png" sizes="16x16 32x32 64x64">
<!--Best display for mobile devices-->
<meta http-equiv="cleartype" content="on">

View File

@ -0,0 +1,3 @@
// Create application logger using loglevel
// https://atmospherejs.com/spacejamio/loglevel
log = loglevel.createAppLogger('LesionTracker', defaultLevel = 'info');

View File

@ -1,49 +0,0 @@
Router.configure({
layoutTemplate: 'layoutLesionTracker',
//loadingTemplate: '',
notFoundTemplate: 'notFound'
});
Router.route('/', function () {
this.render('worklist', {});
});
tabs = new Meteor.Collection(null);
Session.setDefault('ViewerData', {});
Session.setDefault('ViewerDataUpdated', Random.id());
Object.keys(ViewerData).forEach(function(contentId) {
var tabData = ViewerData[contentId];
var data = {
title: tabData.title,
contentid: tabData.contentId,
};
tabs.insert(data);
});
Router.route('/viewer/:_id', {
layoutTemplate: 'layout',
name: 'viewer',
onBeforeAction: function() {
var self = this;
Meteor.call('GetStudyMetadata', this.params._id, function(error, study) {
sortStudy(study);
var title = study.seriesList[0].instances[0].patientName;
var contentid = generateUUID();
var data = {
title: title,
contentid: contentid,
};
tabs.insert(data);
data.studies = [study];
self.render('worklist');
Session.set('OpenNewTabEvent', data);
});
}
});

View File

@ -0,0 +1,44 @@
Session.setDefault('ViewerData', {});
// Re-add any tab data saved in the Session
Object.keys(ViewerData).forEach(function(contentId) {
var tabData = ViewerData[contentId];
var data = {
title: tabData.title,
contentid: tabData.contentid,
};
WorklistTabs.insert(data);
});
Router.configure({
layoutTemplate: 'layoutLesionTracker',
loadingTemplate: 'layoutLesionTracker',
notFoundTemplate: 'notFound'
});
Router.onBeforeAction('loading');
Router.route('/', function () {
this.render('worklist');
});
Router.route('/viewer/:_id', {
layoutTemplate: 'layoutLesionTracker',
name: 'viewer',
onBeforeAction: function() {
log.info('Router GetStudyMetadata');
var studyInstanceUid = this.params._id;
// Check if this study is already loaded in a tab
// If it is, stop here so we don't keep adding tabs on hot-code reloads
var tab = WorklistTabs.find({'studyInstanceUid' : studyInstanceUid}).fetch();
if (tab) {
return;
}
this.render('worklist');
openNewTab(studyInstanceUid);
}
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 15 KiB

13
Makefile Normal file
View File

@ -0,0 +1,13 @@
docs:
mkdir -p docs
rm docs/* || true
# Make jsdoc3 documentation
jsdoc . -c conf.json -r -d docs -R README.md
# Make docco documentation (not sure if we will keep this)
mkdir -p docs/docco
docco {Packages,Viewer,LesionTracker}/{**/*,*} -o docs/docco -l parallel
# Force rebuild even if the docs exist
.PHONY: docs

View File

@ -1,3 +1,4 @@
.idea
.meteor/local
.meteor/meteorite
node_modules

View File

@ -33,3 +33,4 @@ dicomweb
viewerbase
worklist
reactive-var
practicalmeteor:loglevel

View File

@ -1 +1 @@
METEOR@1.2.0.2
METEOR@1.2.1

View File

@ -1,8 +1,8 @@
accounts-base@1.2.1
accounts-password@1.1.3
accounts-base@1.2.2
accounts-password@1.1.4
anti:i18n@0.4.3
arsnebula:reactive-promise@0.9.1
autoupdate@1.2.3
autoupdate@1.2.4
babel-compiler@5.8.24_1
babel-runtime@0.1.4
base64@1.0.4
@ -14,20 +14,21 @@ boilerplate-generator@1.0.4
caching-compiler@1.0.0
caching-html-compiler@1.0.2
callback-hook@1.0.4
check@1.0.6
check@1.1.0
coffeescript@1.0.11
cornerstone@0.0.1
ddp@1.2.2
ddp-client@1.2.1
ddp-common@1.2.1
ddp-common@1.2.2
ddp-rate-limiter@1.0.0
ddp-server@1.2.1
ddp-server@1.2.2
deps@1.0.9
dicomweb@0.0.1
diff-sequence@1.0.1
ecmascript@0.1.5
ecmascript-collections@0.1.6
ecmascript@0.1.6
ecmascript-runtime@0.2.6
ejson@1.0.7
email@1.0.7
email@1.0.8
fastclick@1.0.7
fortawesome:fontawesome@4.4.0
geojson-utils@1.0.4
@ -35,39 +36,42 @@ hot-code-push@1.0.0
html-tools@1.0.5
htmljs@1.0.5
http@1.1.1
ian:accounts-ui-bootstrap-3@1.2.81
ian:accounts-ui-bootstrap-3@1.2.83
id-map@1.0.4
iron:controller@1.0.8
iron:core@1.0.8
iron:dynamic-template@1.0.8
iron:layout@1.0.8
iron:location@1.0.9
iron:middleware-stack@1.0.9
iron:router@1.0.9
iron:url@1.0.9
iron:controller@1.0.12
iron:core@1.0.11
iron:dynamic-template@1.0.12
iron:layout@1.0.12
iron:location@1.0.11
iron:middleware-stack@1.0.11
iron:router@1.0.12
iron:url@1.0.11
jquery@1.11.4
launch-screen@1.0.4
livedata@1.0.15
localstorage@1.0.5
logging@1.0.8
meteor@1.1.9
meteor@1.1.10
meteor-base@1.0.1
meteor-platform@1.2.3
minifiers@1.1.7
minimongo@1.0.10
mobile-experience@1.0.1
mobile-status-bar@1.0.6
mongo@1.1.2
momentjs:moment@2.8.4
mongo@1.1.3
mongo-id@1.0.1
mrt:moment@2.8.1
npm-bcrypt@0.7.8_2
npm-mongo@1.4.39_1
observe-sequence@1.0.7
ordered-dict@1.0.4
promise@0.5.0
random@1.0.4
practicalmeteor:chai@1.9.2_3
practicalmeteor:loglevel@1.1.0_3
promise@0.5.1
random@1.0.5
rate-limit@1.0.0
reactive-dict@1.1.2
reactive-dict@1.1.3
reactive-var@1.0.6
reload@1.1.4
retry@1.0.4
@ -79,9 +83,9 @@ spacebars@1.0.7
spacebars-compiler@1.0.7
srp@1.0.4
standard-app-packages@1.0.6
standard-minifiers@1.0.1
stylus@2.511.0_2
templating@1.1.4
standard-minifiers@1.0.2
stylus@2.511.1
templating@1.1.5
templating-tools@1.0.0
tracker@1.0.9
twbs:bootstrap@3.3.5
@ -89,6 +93,6 @@ ui@1.0.8
underscore@1.0.4
url@1.0.5
viewerbase@0.0.1
webapp@1.2.2
webapp@1.2.3
webapp-hashing@1.0.5
worklist@0.0.1

View File

@ -0,0 +1,2 @@
set PACKAGE_DIRS=..\Packages
meteor --settings ../config/localhostOrthanc.json

View File

@ -0,0 +1,2 @@
set PACKAGE_DIRS=..\Packages
meteor --settings ../config/medicalConnections.json

View File

@ -0,0 +1,2 @@
set PACKAGE_DIRS=..\Packages
meteor --settings ../config/medkenOrthanc.json

View File

@ -0,0 +1,2 @@
set PACKAGE_DIRS=..\Packages
meteor --settings ../config/siimDCM4CHEE.json

View File

@ -1,6 +1,9 @@
<template name="viewer">
<div id="viewer">
{{> studyBrowser studies=studies activeViewport=activeViewport}}
{{> viewerMain studies=studies activeViewport=activeViewport viewportRows=viewportRows viewportColumns=viewportColumns}}
{{> studyBrowser }}
<div class='viewerMain'>
{{ > toolbar }}
{{ > imageViewerViewports }}
</div>
</div>
</template>

View File

@ -1,4 +1,6 @@
function resizeViewports() {
log.info("viewer resizeViewports");
// Handle resizing of image viewer viewports
// For some reason, this seems to need to be on
// another delay, or the resizing won't work properly
@ -15,12 +17,13 @@ function resizeViewports() {
}
Template.viewer.onCreated(function() {
console.log("Image Viewer onCreated");
log.info("viewer onCreated");
OHIF = {
viewer: {}
};
OHIF.viewer.loadIndicatorDelay = 3000;
OHIF.viewer.loadIndicatorDelay = 500;
OHIF.viewer.defaultTool = 'wwwc';
OHIF.viewer.refLinesEnabled = true;
OHIF.viewer.isPlaying = {};
@ -31,6 +34,14 @@ Template.viewer.onCreated(function() {
viewport.invert = !viewport.invert;
cornerstone.setViewport(element, viewport);
},
resetViewport: function(element) {
cornerstone.reset(element);
},
clearTools: function(element) {
var toolStateManager = cornerstoneTools.globalImageIdSpecificToolStateManager;
toolStateManager.clear(element);
cornerstone.updateImage(element);
},
playClip: function(element) {
var viewportIndex = $('.imageViewerViewport').index(element);
var isPlaying = OHIF.viewer.isPlaying[viewportIndex] || false;
@ -55,35 +66,35 @@ Template.viewer.onCreated(function() {
};
}
if (this.data.activeViewport === undefined) {
this.data.activeViewport = new ReactiveVar(0);
}
if (this.data.viewportRows === undefined) {
this.data.viewportRows = new ReactiveVar(1);
}
if (this.data.viewportColumns === undefined) {
this.data.viewportColumns = new ReactiveVar(1);
}
var contentId = this.data.contentId;
// Update the viewer data object
ViewerData[contentId].viewportColumns = this.data.viewportColumns;
ViewerData[contentId].viewportRows = this.data.viewportRows;
ViewerData[contentId].activeViewport = this.data.activeViewport;
Session.set('ViewerData', ViewerData);
if (ViewerData[contentId].loadedSeriesData) {
log.info('Reloading previous loadedSeriesData');
OHIF.viewer.loadedSeriesData = ViewerData[contentId].loadedSeriesData;
if (ViewerData[contentId].viewer) {
OHIF.viewer = ViewerData[contentId].viewer;
} else {
OHIF.viewer.imageViewerLoadedSeriesDictionary = {};
ViewerData[contentId].viewer = OHIF.viewer;
log.info('Setting default ViewerData');
OHIF.viewer.loadedSeriesData = {};
ViewerData[contentId].loadedSeriesData = OHIF.viewer.loadedSeriesData;
// Update the viewer data object
ViewerData[contentId].viewportColumns = 1;
ViewerData[contentId].viewportRows = 1;
ViewerData[contentId].activeViewport = 0;
Session.set('ViewerData', ViewerData);
}
Session.set('activeViewport', ViewerData[contentId].activeViewport || 0);
Session.set("studies", this.data.studies);
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer("CornerstoneNewImage", cornerstoneTools.updateImageSynchronizer);
});
Template.viewer.onDestroyed(function() {
log.info("onDestroyed");
OHIF.viewer.updateImageSynchronizer.destroy();
});

View File

@ -12,4 +12,10 @@
user-select: none
.studyBrowser
width: auto
width: auto
.viewerMain
float: left
background-color: gray
height: 100%
width: calc(100% - 120px)

View File

@ -1,6 +0,0 @@
<template name="viewerMain">
<div class='viewerMain'>
{{ > toolbar activeViewport=activeViewport viewportRows=viewportRows viewportColumns=viewportColumns}}
{{ > imageViewerViewports studies=studies activeViewport=activeViewport viewportRows=viewportRows viewportColumns=viewportColumns}}
</div>
</template>

View File

@ -1,5 +0,0 @@
Template.viewerMain.helpers({
studies : function() {
return this.studies;
}
});

View File

@ -1,5 +0,0 @@
.viewerMain
float: left
background-color: gray
height: 100%
width: calc(100% - 120px)

View File

@ -2,7 +2,7 @@
<meta charset="utf-8">
<title>OHIF Viewer</title>
<meta name="description" content="Open Health Imaging Foundation DICOM Viewer">
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1,maximum-scale=1,user-scalable=no, minimal-ui">
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1,maximum-scale=1,user-scalable=no">
<!--Best display for mobile devices-->
<meta http-equiv="cleartype" content="on">
<meta name="MobileOptimized" content="320">

3
OHIFViewer/client/log.js Normal file
View File

@ -0,0 +1,3 @@
// Create application logger using loglevel
// https://atmospherejs.com/spacejamio/loglevel
log = loglevel.createAppLogger('OHIFViewer', defaultLevel = 'info');

View File

@ -1,24 +1,25 @@
Router.configure({
layoutTemplate: 'layout',
//loadingTemplate: '',
notFoundTemplate: 'notFound'
});
Router.route('/', function () {
this.render('worklist', {});
});
tabs = new Meteor.Collection(null);
Session.setDefault('ViewerData', {});
Session.setDefault('ViewerDataUpdated', Random.id());
// Re-add any tab data saved in the Session
Object.keys(ViewerData).forEach(function(contentId) {
var tabData = ViewerData[contentId];
var data = {
title: tabData.title,
contentid: tabData.contentId,
contentid: tabData.contentid,
};
tabs.insert(data);
WorklistTabs.insert(data);
});
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'layout',
notFoundTemplate: 'notFound'
});
Router.onBeforeAction('loading');
Router.route('/', function () {
this.render('worklist');
});
@ -26,24 +27,18 @@ Router.route('/viewer/:_id', {
layoutTemplate: 'layout',
name: 'viewer',
onBeforeAction: function() {
var self = this;
log.info('Router GetStudyMetadata');
Meteor.call('GetStudyMetadata', this.params._id, function(error, study) {
sortStudy(study);
var studyInstanceUid = this.params._id;
// Check if this study is already loaded in a tab
// If it is, stop here so we don't keep adding tabs on hot-code reloads
var tab = WorklistTabs.find({studyInstanceUid: studyInstanceUid}).fetch();
if (tab) {
return;
}
var title = study.seriesList[0].instances[0].patientName;
var contentid = generateUUID();
var data = {
title: title,
contentid: contentid,
};
tabs.insert(data);
data.studies = [study];
self.render('worklist');
Session.set('OpenNewTabEvent', data);
});
this.render('worklist');
openNewTab(studyInstanceUid);
}
});

View File

@ -2148,7 +2148,7 @@ if(typeof cornerstone === 'undefined'){
function initShaders() {
for (var id in cornerstone.webGL.shaders) {
console.log("WEBGL: Loading shader", id);
//console.log("WEBGL: Loading shader", id);
var shader = cornerstone.webGL.shaders[ id ];
shader.attributes = {};
shader.uniforms = {};
@ -2168,13 +2168,13 @@ if(typeof cornerstone === 'undefined'){
function initRenderer() {
if (cornerstone.webGL.isWebGLInitialized === true) {
console.log("WEBGL Renderer already initialized");
//console.log("WEBGL Renderer already initialized");
return;
}
if ( initWebGL( renderCanvas ) ) {
initBuffers();
initShaders();
console.log("WEBGL Renderer initialized!");
//console.log("WEBGL Renderer initialized!");
cornerstone.webGL.isWebGLInitialized = true;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,38 @@
var hangingProtocol;
/**
* This is an example of a hanging protocol
* It takes in a set of studies, as well as the
* number of rows and columns in the layout.
*
* It returns an array of objects, one for each viewport, detailing
* which series should be loaded in the viewport.
*
* @param inputData
* @returns {Array}
*/
function defaultHangingProtocol(inputData) {
var studies = inputData.studies;
var viewportRows = inputData.viewportRows;
var viewportColumns = inputData.viewportColumns;
if (!studies.length) {
log.warn("No studies provided to Hanging Protocol");
return;
}
// This is the most basic hanging protocol.
var stacks = [];
studies.forEach(function(study) {
study.seriesList.forEach(function(series) {
// Ensure that the series has image data
// (All images have rows)
var anInstance = series.instances[0];
if (!anInstance || !anInstance.rows) {
return;
}
var stack = {
series: series,
study: study

View File

@ -0,0 +1,49 @@
/**
* Converts a String to a UInt8 array of character codes
* @param {String} str Input string
* @returns {Uint8Array} Uint8 Array of character codes
*/
function stringToUint8Array(str) {
var uint=new Uint8Array(str.length);
for(var i=0,j=str.length;i<j;i++){
uint[i]=str.charCodeAt(i);
}
return uint;
}
function checkToken(token, data, dataOffset) {
if(dataOffset + token.length > data.length) {
//console.log('dataOffset >> ', dataOffset);
return false;
}
for(var i = 0; i < token.length; i++) {
if(token[i] !== data[endIndex++]) {
if(endIndex > 520000) {
console.log('token=',uint8ArrayToString(token));
console.log('data=', uint8ArrayToString(data, dataOffset, endIndex-dataOffset));
console.log('miss at %d %s dataOffset=%d', i, String.fromCharCode(data[endIndex]), endIndex);
console.log('miss at %d %s dataOffset=%d', i, String.fromCharCode(token[endIndex]), endIndex);
}
return false;
}
}
return true;
}
findIndexOfString = function(data, str, offset) {
offset = offset || 0;
var token = stringToUint8Array(str);
for(var i=offset; i < data.length; i++) {
if(data[i] === token[0]) {
//console.log('match @', i);
if(checkToken(token, data, i)) {
return i;
}
}
}
return -1;
};

View File

@ -0,0 +1,17 @@
/**
* Converts a Uint8 array to a string
* @param data
* @param offset
* @param length
* @returns {string}
*/
uint8ArrayToString = function(data, offset, length) {
offset = offset || 0;
length = length || data.length - offset;
var str = "";
for(var i=offset; i < offset + length; i++) {
str += String.fromCharCode(data[i]);
}
return str;
};

View File

@ -5,6 +5,9 @@ Package.describe({
});
Package.onUse(function (api) {
api.use('http');
// DICOMWeb API functions
api.addFiles('server/namespace.js', 'server');
api.addFiles('server/getImageFrame.js', 'server');
api.addFiles('server/getJSON.js', 'server');
@ -12,6 +15,10 @@ Package.onUse(function (api) {
api.addFiles('server/getNumber.js', 'server');
api.addFiles('server/getString.js', 'server');
// Helper functions
api.addFiles('lib/findIndexOfString.js', 'server');
api.addFiles('lib/uint8ArrayToString.js', 'server');
api.export("DICOMWeb", 'server');
});

View File

@ -1,298 +1,408 @@
var lineIndex = 0; //This holds drawn line index
var cornerstoneTools = (function ($, cornerstone, cornerstoneMath, cornerstoneTools) {
var activeLesionMeasurementData;
var timepointID;
var cornerstoneTools = (function($, cornerstone, cornerstoneMath, cornerstoneTools) {
"use strict";
if(cornerstoneTools === undefined) {
if (cornerstoneTools === undefined) {
cornerstoneTools = {};
}
var configuration = {
getLesionLocationCallback: getLesionLocationCallback,
changeLesionLocationCallback: changeLesionLocationCallback,
};
// Define a callback to get your text annotation
// This could be used, e.g. to open a modal
function getLesionLocationCallback(measurementData, eventData, doneCallback) {
doneCallback(prompt('Enter your lesion location:'));
}
function changeLesionLocationCallback(measurementData, eventData, doneCallback) {
doneCallback(prompt('Change your lesion location:'));
}
var toolType = "lesion";
///////// BEGIN ACTIVE TOOL ///////
function createNewMeasurement(mouseEventData)
{
function addNewMeasurement(mouseEventData) {
var element = mouseEventData.element;
var lesionCounter = "";
// Subscribe CornerstoneMouseup event, when mouse is up, call lesionDialog
$(element).on("CornerstoneToolsMouseUp", function (e) {
function doneCallback(lesionNumber) {
measurementData.lesionName = "Target " + lesionNumber;
measurementData.lesionNumber = lesionNumber;
measurementData.active = false;
cornerstone.updateImage(element);
}
// Unsubscribe CornerstoneToolsMouseUp event
$(element).off("CornerstoneToolsMouseUp");
var measurementData = createNewMeasurement(mouseEventData);
// Show LesionDialog
$(document).trigger("ShowLesionDialog", e);
var eventData = {
mouseButtonMask: mouseEventData.which,
};
// associate this data with this imageId so we can render it and manipulate it
cornerstoneTools.addToolState(element, toolType, measurementData);
// since we are dragging to another place to drop the end point, we can just activate
// the end point and let the moveHandle move it for us.
$(element).off('CornerstoneToolsMouseMove', cornerstoneTools.lesion.mouseMoveCallback);
$(element).off('CornerstoneToolsMouseDown', cornerstoneTools.lesion.mouseDownCallback);
$(element).off('CornerstoneToolsMouseDownActivate', cornerstoneTools.lesion.mouseDownActivateCallback);
cornerstone.updateImage(element);
cornerstoneTools.moveNewHandle(mouseEventData, measurementData.handles.end, function() {
if (cornerstoneTools.anyHandlesOutsideImage(mouseEventData, measurementData.handles)) {
// delete the measurement
cornerstoneTools.removeToolState(element, toolType, measurementData);
} else {
// Set lesionMeasurementData Session
var config = cornerstoneTools.lesion.getConfiguration();
config.getLesionLocationCallback(measurementData, mouseEventData, doneCallback);
}
$(element).on('CornerstoneToolsMouseMove', eventData, cornerstoneTools.lesion.mouseMoveCallback);
$(element).on('CornerstoneToolsMouseDown', eventData, cornerstoneTools.lesion.mouseDownCallback);
$(element).on('CornerstoneToolsMouseDownActivate', eventData, cornerstoneTools.lesion.mouseDownActivateCallback);
cornerstone.updateImage(element);
});
}
// Set Lesion Name
$(element).on("LesionNameSet", function(e,lesionName){
lesionCounter = lesionName;
function addNewMeasurementTouch(touchEventData) {
var element = touchEventData.element;
function doneCallback(lesionNumber) {
measurementData.lesionName = "Target " + lesionNumber;
measurementData.lesionNumber = lesionNumber;
measurementData.active = false;
cornerstone.updateImage(element);
}
var measurementData = createNewMeasurement(touchEventData);
// associate this data with this imageId so we can render it and manipulate it
cornerstoneTools.addToolState(element, toolType, measurementData);
// since we are dragging to another place to drop the end point, we can just activate
// the end point and let the moveHandle move it for us.
$(element).off('CornerstoneToolsTouchDrag', cornerstoneTools.lesion.touchMoveHandle);
$(element).off('CornerstoneToolsTap', cornerstoneTools.lesion.tapCallback);
$(element).off('CornerstoneToolsDragStartActive', cornerstoneTools.lesion.touchDownActivateCallback);
cornerstone.updateImage(element);
cornerstoneTools.moveNewHandleTouch(touchEventData, measurementData.handles.end, function() {
if (cornerstoneTools.anyHandlesOutsideImage(touchEventData, measurementData.handles)) {
// delete the measurement
cornerstoneTools.removeToolState(element, toolType, measurementData);
} else {
// Set lesionMeasurementData Session
var config = cornerstoneTools.lesion.getConfiguration();
config.getLesionLocationCallback(measurementData, touchEventData, doneCallback);
}
$(element).on('CornerstoneToolsTouchDrag', cornerstoneTools.lesion.touchMoveHandle);
$(element).on('CornerstoneToolsTap', cornerstoneTools.lesion.tapCallback);
$(element).on('CornerstoneToolsDragStartActive', cornerstoneTools.lesion.touchDownActivateCallback);
cornerstone.updateImage(element);
});
}
// Subscribe LesionToolModified and calls measurementModified function when lesion measurement is changed or updated.
$(element).on("LesionToolModified", measurementModified);
// Subscribe LesionMeasurementCreated
$(element).trigger("LesionMeasurementCreated");
// create the measurement data for this tool with the end handle activated
function createNewMeasurement(mouseEventData) {
// Create the measurement data for this tool with the end handle activated
var measurementData = {
visible : true,
active : true,
handles : {
start : {
x : mouseEventData.currentPoints.image.x,
y : mouseEventData.currentPoints.image.y,
visible: true,
active: true,
handles: {
start: {
x: mouseEventData.currentPoints.image.x,
y: mouseEventData.currentPoints.image.y,
highlight: true,
active: false
},
end: {
x : mouseEventData.currentPoints.image.x,
y : mouseEventData.currentPoints.image.y,
x: mouseEventData.currentPoints.image.x,
y: mouseEventData.currentPoints.image.y,
highlight: true,
active: true
},
textBox: {
x: mouseEventData.currentPoints.image.x - 50,
y: mouseEventData.currentPoints.image.y - 50,
pointNearHandle: pointNearTextBox,
active: false,
movesIndependently: true,
drawnIndependently: true
}
},
index: lineIndex,
measurementText: "",
linkedTextCoords: {
start : {
x : mouseEventData.currentPoints.image.x,
y : mouseEventData.currentPoints.image.y,
highlight: true,
active: false
},
end: {
x : mouseEventData.currentPoints.image.x,
y : mouseEventData.currentPoints.image.y,
highlight: true,
active: true
},
init: false
},
lesionName: "Target "+lesionCounter,
imageId: mouseEventData.image.imageId,
measurementText: 0,
lesionName: '',
isDeleted: false,
lineNumber:"" //Indicates line number on image
isTarget: true,
uid: uuid.v4()
};
lineIndex++;
return measurementData;
}
///////// END ACTIVE TOOL ///////
function pointNearTool(element, data, coords)
{
function pointNearTool(element, data, coords) {
var lineSegment = {
start: cornerstone.pixelToCanvas(element, data.handles.start), end: cornerstone.pixelToCanvas(element, data.handles.end)
start: cornerstone.pixelToCanvas(element, data.handles.start),
end: cornerstone.pixelToCanvas(element, data.handles.end)
};
var distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords);
if (pointNearTextBox(element, data.handles.textBox, coords)) {
return true;
}
return (distanceToPoint < 25);
}
function pointNearToolForText(element, data, coords) {
var lineSegment = {
start: cornerstone.pixelToCanvas(element, data.linkedTextCoords.start), end: cornerstone.pixelToCanvas(element, data.linkedTextCoords.end)
};
var distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords);
return (distanceToPoint < 30);
function pointNearTextBox(element, handle, coords) {
if (!handle.boundingBox) {
return;
}
return cornerstoneMath.point.insideRect(coords, handle.boundingBox);
}
function suscribeLesionToolSelectedEvent(element) {
var elementEvents = $._data(element, "events");
var index = Object.keys(elementEvents).indexOf("LesionToolSelected");
if (index < 0) {
// Subscribe LesionToolSelected and calls measurementModified function when lesion measurement is changed or updated.
$(element).on("LesionToolSelected", measurementModified);
}
}
///////// BEGIN IMAGE RENDERING ///////
function onImageRendered(e, eventData) {
suscribeLesionToolSelectedEvent(e.currentTarget);
// if we have no toolData for this element, return immediately as there is nothing to do
var toolData = cornerstoneTools.getToolState(e.currentTarget, toolType);
if (toolData === undefined) {
if (!toolData) {
return;
}
updateLesions(toolData, eventData);
}
function updateLesions(toolData, eventData) {
// we have tool data for this element - iterate over each one and draw it
var context = eventData.canvasContext.canvas.getContext('2d');
context.setTransform(1, 0, 0, 1, 0, 0);
var color;
var lineWidth = cornerstoneTools.toolStyle.getToolWidth();
var font = cornerstoneTools.textStyle.getFont();
var config = cornerstoneTools.length.getConfiguration();
for (var i = 0; i < toolData.data.length; i++) {
context.save();
// configurable shadow
if (config && config.shadow) {
context.shadowColor = '#000000';
context.shadowOffsetX = +1;
context.shadowOffsetY = +1;
}
var data = toolData.data[i];
if (data.active) {
color = cornerstoneTools.toolColors.getActiveColor();
} else {
color = cornerstoneTools.toolColors.getToolColor();
}
// draw the line
var handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.start);
var handleEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.end);
context.beginPath();
context.strokeStyle = color;
context.lineWidth = lineWidth;
context.moveTo(handleStartCanvas.x, handleStartCanvas.y);
context.lineTo(handleEndCanvas.x, handleEndCanvas.y);
context.stroke();
// draw the handles
cornerstoneTools.drawHandles(context, eventData, data.handles, color);
//Set coordinates of text
var linkedTextStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.linkedTextCoords.start);
var linkedTextEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.linkedTextCoords.end);
if(!data.linkedTextCoords.init){
data.linkedTextCoords.start.x = data.handles.start.x + 50;
data.linkedTextCoords.start.y = data.handles.start.y + 40;
linkedTextStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.linkedTextCoords.start);
//Set end point of linkedTextCoords
data.linkedTextCoords.end.x = data.linkedTextCoords.start.x + 50;
data.linkedTextCoords.end.y = data.linkedTextCoords.start.y + 30;
linkedTextEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.linkedTextCoords.end);
//initialized coordinates of text
data.linkedTextCoords.init = true;
}
//Draw linked line as dashed
context.setLineDash([2,3]);
context.beginPath();
context.strokeStyle = color;
context.lineWidth = 1 / eventData.viewport.scale;
var mid = {
x:(handleStartCanvas.x + handleEndCanvas.x) / 2, y:(handleStartCanvas.y + handleEndCanvas.y) / 2
};
context.moveTo(mid.x, mid.y);
context.lineTo(linkedTextStartCanvas.x + 20, linkedTextStartCanvas.y + 20);
context.stroke();
// Draw the text
context.fillStyle = color;
context.font = font;
var dx = (data.handles.start.x - data.handles.end.x) * (eventData.image.columnPixelSpacing || 1);
var dy = (data.handles.start.y - data.handles.end.y) * (eventData.image.rowPixelSpacing || 1);
var length = Math.sqrt(dx * dx + dy * dy);
var suffix = ' mm';
if (!eventData.image.rowPixelSpacing || !eventData.image.columnPixelSpacing) {
suffix = ' pixels';
}
var text = '' + length.toFixed(2) + suffix;
var textCoords = {
x: linkedTextStartCanvas.x, y: linkedTextStartCanvas.y
};
cornerstoneTools.drawTextBox(context, data.lesionName, textCoords.x, textCoords.y, color);
cornerstoneTools.drawTextBox(context, text, textCoords.x, textCoords.y + 20, color);
//Set measurement text to show lesion table
data.measurementText = length.toFixed(2);
$(eventData.enabledElement.element).trigger("LesionTextChanged",data);
context.restore();
renderLesion(toolData.data[i], context, eventData);
updateLesionCollection(toolData.data[i]);
}
}
///////// END IMAGE RENDERING ///////
//This function is called from cornerstone-viewport.html and updates lesion measurement and makes the lesion active
function measurementModified(e, eventObject){
var start = new Date();
var enabledElement = eventObject.enabledElement;
var lineIndex = eventObject.lineIndex;
var type = eventObject.type;
// if we have no toolData for this element, return immediately as there is nothing to do
var toolData = cornerstoneTools.getToolState(e.currentTarget, toolType);
if (toolData === undefined) {
function updateLesionCollection(lesionData) {
if (!lesionData.active) {
return;
}
//If type is delete, remove measurement
if(type === "delete") {
var deletedDataIndex = -1;
for (var i = 0; i < toolData.data.length; i++) {
var data = toolData.data[i];
//When click a row of table measurements, measurement will be active and color will be green
if (data.index === eventObject.lineIndex && eventObject.type !== "active") {
data.visible = false;
deletedDataIndex = i;
}
}
if (deletedDataIndex >= 0 && deletedDataIndex < toolData.data.length) {
toolData.data.splice(deletedDataIndex, 1);
}
if (lesionData.timepointID && lesionData.timepointID !== "") {
// Update Measurements Collection
measurementManagerDAL.updateTimepointData(lesionData);
} else {
for (var i = 0; i < toolData.data.length; i++) {
var data = toolData.data[i];
activeLesionMeasurementData = lesionData;
}
}
//When click a row of table measurements, measurement will be active and color will be green
if (data.index === eventObject.lineIndex && eventObject.type === "active") {
data.active = true;
} else{
data.active = false;
}
}
function renderLesion(data, context, eventData) {
context.save();
var color;
var element = eventData.element;
var lineWidth = cornerstoneTools.toolStyle.getToolWidth();
var font = cornerstoneTools.textStyle.getFont();
var config = cornerstoneTools.lesion.getConfiguration();
// configurable shadow from CornerstoneTools
if (config && config.shadow) {
context.shadowColor = '#000000';
context.shadowOffsetX = +1;
context.shadowOffsetY = +1;
}
if (data.active) {
color = cornerstoneTools.toolColors.getActiveColor();
} else {
color = cornerstoneTools.toolColors.getToolColor();
}
enabledElement.image.render(enabledElement, true);
// draw the line
var handleStartCanvas = cornerstone.pixelToCanvas(element, data.handles.start);
var handleEndCanvas = cornerstone.pixelToCanvas(element, data.handles.end);
var canvasTextLocation = cornerstone.pixelToCanvas(element, data.handles.textBox);
var context = enabledElement.canvas.getContext('2d');
context.beginPath();
context.strokeStyle = color;
context.lineWidth = lineWidth;
context.moveTo(handleStartCanvas.x, handleStartCanvas.y);
context.lineTo(handleEndCanvas.x, handleEndCanvas.y);
context.stroke();
var end = new Date();
var diff = end - start;
//console.log(diff + ' ms');
// draw the handles
cornerstoneTools.drawHandles(context, eventData, data.handles, color);
var eventData = {
viewport : enabledElement.viewport,
element : enabledElement.element,
image : enabledElement.image,
enabledElement : enabledElement,
canvasContext: context,
measurementText: "",
renderTimeInMs : diff,
lineIndex : lineIndex,
type : type //Holds image will be deleted or active
//Draw linked line as dashed
context.setLineDash([2, 3]);
context.beginPath();
context.strokeStyle = color;
context.lineWidth = 1 / eventData.viewport.scale;
var mid = {
x: (handleStartCanvas.x + handleEndCanvas.x) / 2,
y: (handleStartCanvas.y + handleEndCanvas.y) / 2
};
enabledElement.invalid = false;
context.moveTo(mid.x, mid.y);
context.lineTo(canvasTextLocation.x + 20, canvasTextLocation.y + 20);
context.stroke();
onImageRendered(e, eventData);
// Draw the text
var dx = (data.handles.start.x - data.handles.end.x) * (eventData.image.columnPixelSpacing || 1);
var dy = (data.handles.start.y - data.handles.end.y) * (eventData.image.rowPixelSpacing || 1);
var length = Math.sqrt(dx * dx + dy * dy);
var suffix = ' mm';
if (!eventData.image.rowPixelSpacing || !eventData.image.columnPixelSpacing) {
suffix = ' pixels';
}
var text = '' + length.toFixed(2) + suffix;
var textLines = [data.lesionName, text];
var boundingBox = cornerstoneTools.drawTextBox(context,
textLines,
canvasTextLocation.x, canvasTextLocation.y, color);
data.handles.textBox.boundingBox = boundingBox;
// Set measurement text to show lesion table
data.measurementText = length.toFixed(1);
// Lesion Measurement is changed
$(element).trigger("LesionTextChanged", data);
context.restore();
}
///////// END IMAGE RENDERING ///////
function updateLesion(e, eventData) {
var enabledElement = eventData.enabledElement;
var element = eventData.enabledElement.element;
var isTarget = eventData.lesionData.isTarget;
var lesionNumber = eventData.lesionData.lesionNumber;
var type = eventData.type;
// if we have no toolData for this element, return immediately as there is nothing to do
var toolData = cornerstoneTools.getToolState(e.currentTarget, toolType);
if (!toolData) {
return;
}
var data,
i;
switch (type) {
case "delete":
//If type is delete, remove measurement
var deletedDataIndex = -1;
for (i = 0; i < toolData.data.length; i++) {
data = toolData.data[i];
//When click a row of table measurements, measurement will be active and color will be green
if (data.lesionNumber === lesionNumber && eventData.type !== "active" && isTarget) {
data.visible = false;
deletedDataIndex = i;
}
}
if (deletedDataIndex >= 0 && deletedDataIndex < toolData.data.length) {
toolData.data.splice(deletedDataIndex, 1);
}
break;
case "active":
for (i = 0; i < toolData.data.length; i++) {
data = toolData.data[i];
//When click a row of table measurements, measurement will be active and color will be green
if (data.lesionNumber === lesionNumber && eventData.type === "active" && isTarget) {
data.active = true;
} else {
data.active = false;
}
}
break;
case "inactive":
for (i = 0; i < toolData.data.length; i++) {
data = toolData.data[i];
// Make inactive all lesions for the timepoint
data.active = false;
}
break;
}
cornerstone.updateImage(element);
}
function loadImage(e, eventData) {
// If type is active, load image and activate lesion
// If type is inactive, update lesions of enabledElement as inactive
if (eventData.type === "active") {
var stackToolDataSource = cornerstoneTools.getToolState(e.currentTarget, 'stack');
var stackData = stackToolDataSource.data[0];
var imageIds = stackData.imageIds;
var imageIdIndex = imageIds.indexOf(eventData.lesionData.imageId);
if (imageIdIndex < 0) {
return;
}
cornerstone.loadAndCacheImage(imageIds[imageIdIndex]).then(function(image) {
cornerstone.displayImage(eventData.enabledElement.element, image);
updateLesion(e, eventData);
});
} else if (eventData.type === "inactive") {
updateLesion(e, eventData);
}
}
//This function is called from cornerstone-viewport.html and updates lesion measurement and makes the lesion active
function measurementModified(e, eventData) {
loadImage(e, eventData);
}
// module exports
cornerstoneTools.lesion = cornerstoneTools.mouseButtonTool({
createNewMeasurement : createNewMeasurement,
onImageRendered: onImageRendered,
pointNearTool : pointNearTool,
pointNearToolForText: pointNearToolForText,
toolType : toolType
});
cornerstoneTools.lesionTouch = cornerstoneTools.touchTool({
createNewMeasurement: createNewMeasurement,
addNewMeasurement: addNewMeasurement,
onImageRendered: onImageRendered,
pointNearTool: pointNearTool,
pointNearToolForText: pointNearToolForText,
toolType: toolType
});
cornerstoneTools.lesionTouch = cornerstoneTools.touchTool({
createNewMeasurement: createNewMeasurement,
addNewMeasurement: addNewMeasurementTouch,
onImageRendered: onImageRendered,
pointNearTool: pointNearTool,
toolType: toolType
});
cornerstoneTools.lesion.setConfiguration(configuration);
return cornerstoneTools;
}($, cornerstone, cornerstoneMath, cornerstoneTools));

View File

@ -0,0 +1,148 @@
var measurementManagerDAL = (function() {
PatientLocations = new Meteor.Collection(null);
function getLocationName(id) {
var locationObject = PatientLocations.findOne(id);
return locationObject.location || "";
}
// Add timepoint data to Measurements collection
function addTimepointData(lesionData) {
var timepoints = Timepoints.find().fetch();
var timepointsObject = {};
for (var i = 0; i < timepoints.length; i++) {
var timepointId = timepoints[i].timepointID;
var lesionTimepointId = lesionData.timepointID;
var timepointObject;
if (timepointId === lesionTimepointId) {
// Add real mesurement
timepointObject = {
longestDiameter: lesionData.measurementText,
imageId: lesionData.imageId,
};
} else {
// Add null measurement
timepointObject = {
longestDiameter: "",
imageId: ""
};
}
timepointsObject[timepointId] = timepointObject;
}
var lesionDataObject = {
lesionUID: uuid.v4(),
number: Measurements.find().count() + 1,
lesionNumber: lesionData.lesionNumber,
isTarget: lesionData.isTarget,
locationUID: lesionData.locationUID,
location: getLocationName(lesionData.locationUID),
timepoints: timepointsObject
};
Measurements.insert(lesionDataObject);
}
// Update timepoint data in Measurements collection
function updateTimepointData(lesionData) {
// Find the specific lesion to be updated
var measurement = Measurements.find({
lesionNumber: lesionData.lesionNumber,
isTarget: lesionData.isTarget
}).fetch()[0];
// If no such lesion exists, stop here
if (!measurement) {
return;
}
// Update this specific lesion at the given timepoint
var timepointID = lesionData.timepointID;
// Update timepoints from lesion data
var timepoints = measurement.timepoints;
timepoints[timepointID].longestDiameter = lesionData.measurementText;
timepoints[timepointID].imageId = lesionData.imageId;
Measurements.update({
lesionNumber: lesionData.lesionNumber,
isTarget: lesionData.isTarget
}, {
$set: {
timepoints: timepoints
}
});
}
// Check timepointData is found in Measurements collection
function hasTimepointData(lesionData) {
var timepointData = Measurements.findOne({
lesionNumber: lesionData.lesionNumber,
isTarget: lesionData.isTarget
});
if (timepointData) {
return true;
}
return false;
}
// Adds new timepoint item to tiemepoints array
function addLesionData(lesionData) {
if (hasTimepointData(lesionData)) {
// Update data
updateTimepointData(lesionData);
} else {
// Insert data
addTimepointData(lesionData);
}
}
// Returns new lesion number according to timepointID
function getNewLesionNumber(timepointID, isTarget) {
// Get all current lesion measurements
var measurements = Measurements.find({isTarget: isTarget}).fetch();
// If no measurements exist yet, start at 1
if (!measurements.length) {
return 1;
}
// If measurements exist, find the last lesion number
// from the given timepoint
var lesionNumberCounter = 0;
for (var i = 0; i < measurements.length; i++) {
var measurement = measurements[i];
var timepoints = measurement.timepoints;
if (timepoints[timepointID].longestDiameter === '') {
return measurement.lesionNumber;
} else {
lesionNumberCounter = lesionNumberCounter + 1;
}
}
return lesionNumberCounter + 1;
}
// If lesion number is added for any timepoint, returns lesion locationUID
function lesionNumberExists(lesionData) {
if (!hasTimepointData(lesionData)) {
return;
}
var measurement = Measurements.find({
lesionNumber: lesionData.lesionNumber,
isTarget: lesionData.isTarget
}).fetch()[0];
return measurement.locationUID;
}
return {
addLesionData: addLesionData,
getNewLesionNumber: getNewLesionNumber,
lesionNumberExists: lesionNumberExists,
updateTimepointData: updateTimepointData,
getLocationName: getLocationName
};
})();

View File

@ -0,0 +1,490 @@
(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
'use strict';
var toolType = 'nonTarget';
var configuration = {
setLesionNumberCallback: setLesionNumberCallback,
getNonTargetLesionLocationCallback: getNonTargetLesionLocationCallback,
changeNonTargetLesionLocationCallback: changeNonTargetLesionLocationCallback,
drawHandles: false,
drawHandlesOnHover: true,
arrowFirst: true
};
// Set lesion number
// Get Non-Target lesions on image
function setLesionNumberCallback(measurementData, eventData, doneCallback) {
var lesionNumber = 1;
doneCallback(lesionNumber);
}
// Define a callback to get your text annotation
// This could be used, e.g. to open a modal
function getNonTargetLesionLocationCallback(measurementData, eventData, doneCallback) {
doneCallback(prompt('Enter your lesion location:'));
}
function changeNonTargetLesionLocationCallback(measurementData, eventData, doneCallback) {
doneCallback(prompt('Change your lesion location:'));
}
/// --- Mouse Tool --- ///
///////// BEGIN ACTIVE TOOL ///////
function addNewMeasurement(mouseEventData) {
var element = mouseEventData.element;
function doneCallback(lesionNumber) {
measurementData.lesionName = "Non-Target "+lesionNumber;
measurementData.lesionNumber = lesionNumber;
measurementData.active = true;
cornerstone.updateImage(element);
}
var measurementData = createNewMeasurement(mouseEventData);
var eventData = {
mouseButtonMask: mouseEventData.which
};
// associate this data with this imageId so we can render it and manipulate it
cornerstoneTools.addToolState(mouseEventData.element, toolType, measurementData);
// since we are dragging to another place to drop the end point, we can just activate
// the end point and let the moveHandle move it for us.
$(mouseEventData.element).off('CornerstoneToolsMouseMove', cornerstoneTools.nonTarget.mouseMoveCallback);
$(mouseEventData.element).off('CornerstoneToolsMouseDown', cornerstoneTools.nonTarget.mouseDownCallback);
$(mouseEventData.element).off('CornerstoneToolsMouseDownActivate', cornerstoneTools.nonTarget.mouseDownActivateCallback);
var config = cornerstoneTools.nonTarget.getConfiguration();
// Set lesion number and lesion name
if (measurementData.lesionName === undefined) {
config.setLesionNumberCallback(measurementData, mouseEventData, doneCallback);
}
cornerstone.updateImage(element);
cornerstoneTools.moveNewHandle(mouseEventData, measurementData.handles.end, function() {
if (cornerstoneTools.anyHandlesOutsideImage(mouseEventData, measurementData.handles)) {
// delete the measurement
cornerstoneTools.removeToolState(mouseEventData.element, toolType, measurementData);
}
config.getNonTargetLesionLocationCallback(measurementData, mouseEventData, doneCallback);
$(mouseEventData.element).on('CornerstoneToolsMouseMove', eventData, cornerstoneTools.nonTarget.mouseMoveCallback);
$(mouseEventData.element).on('CornerstoneToolsMouseDown', eventData, cornerstoneTools.nonTarget.mouseDownCallback);
$(mouseEventData.element).on('CornerstoneToolsMouseDownActivate', eventData, cornerstoneTools.nonTarget.mouseDownActivateCallback);
cornerstone.updateImage(mouseEventData.element);
});
}
function createNewMeasurement(mouseEventData) {
// create the measurement data for this tool with the end handle activated
var measurementData = {
visible: true,
active: true,
handles: {
start: {
x: mouseEventData.currentPoints.image.x,
y: mouseEventData.currentPoints.image.y,
highlight: true,
active: false
},
end: {
x: mouseEventData.currentPoints.image.x,
y: mouseEventData.currentPoints.image.y,
highlight: true,
active: false
},
textBox: {
x: mouseEventData.currentPoints.image.x - 50,
y: mouseEventData.currentPoints.image.y - 50,
pointNearHandle: pointNearTextBox,
active: false,
movesIndependently: true,
drawnIndependently: true
}
},
imageId: mouseEventData.image.imageId,
measurementText: '',
isTarget: false,
uid: uuid.v4()
};
return measurementData;
}
///////// END ACTIVE TOOL ///////
function pointNearTool(element, data, coords) {
var lineSegment = {
start: cornerstone.pixelToCanvas(element, data.handles.start),
end: cornerstone.pixelToCanvas(element, data.handles.end)
};
var distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords);
if (pointNearTextBox(element, data.handles.textBox, coords)) {
return true;
}
return distanceToPoint < 25;
}
function pointNearTextBox(element, handle, coords) {
if (!handle.boundingBox) {
return;
}
return cornerstoneMath.point.insideRect(coords, handle.boundingBox);
}
function drawArrow(context, start, end, color, lineWidth) {
//variables to be used when creating the arrow
var headLength = 10;
var angle = Math.atan2(end.y - start.y, end.x - start.x);
//starting path of the arrow from the start square to the end square and drawing the stroke
context.beginPath();
context.moveTo(start.x, start.y);
context.lineTo(end.x, end.y);
context.strokeStyle = color;
context.lineWidth = lineWidth;
context.stroke();
//starting a new path from the head of the arrow to one of the sides of the point
context.beginPath();
context.moveTo(end.x, end.y);
context.lineTo(end.x - headLength * Math.cos(angle - Math.PI / 7), end.y - headLength * Math.sin(angle - Math.PI / 7));
//path from the side point of the arrow, to the other side point
context.lineTo(end.x - headLength * Math.cos(angle + Math.PI / 7), end.y - headLength * Math.sin(angle + Math.PI / 7));
//path from the side point back to the tip of the arrow, and then again to the opposite side point
context.lineTo(end.x, end.y);
context.lineTo(end.x - headLength * Math.cos(angle - Math.PI / 7), end.y - headLength * Math.sin(angle - Math.PI / 7));
//draws the paths created above
context.strokeStyle = color;
context.lineWidth = lineWidth;
context.stroke();
context.fillStyle = color;
context.fill();
}
function suscribeNonTargetToolModifiedEvent(element) {
var elementEvents = $._data(element, "events");
var index = Object.keys(elementEvents).indexOf("NonTargetToolSelected");
if (index < 0) {
// Subscribe LesionToolModified and calls measurementModified function when lesion measurement is changed or updated.
$(element).on("NonTargetToolSelected", measurementModified);
}
}
///////// BEGIN IMAGE RENDERING ///////
function onImageRendered(e, eventData) {
suscribeNonTargetToolModifiedEvent(e.currentTarget);
// if we have no toolData for this element, return immediately as there is nothing to do
var toolData = cornerstoneTools.getToolState(e.currentTarget, toolType);
if (toolData === undefined) {
return;
}
updateLesions(toolData, eventData);
}
function updateLesions(toolData, eventData) {
// we have tool data for this element - iterate over each one and draw it
var context = eventData.canvasContext.canvas.getContext('2d');
context.setTransform(1, 0, 0, 1, 0, 0);
for (var i = 0; i < toolData.data.length; i++) {
renderLesion(toolData.data[i], context, eventData);
}
}
function renderLesion(lesion, context, eventData) {
context.save();
var color;
var lineWidth = cornerstoneTools.toolStyle.getToolWidth();
var font = cornerstoneTools.textStyle.getFont();
var config = cornerstoneTools.nonTarget.getConfiguration();
// configurable shadow from CornerstoneTools
if (config && config.shadow) {
context.shadowColor = '#000000';
context.shadowOffsetX = +1;
context.shadowOffsetY = +1;
}
if (lesion.active) {
color = cornerstoneTools.toolColors.getActiveColor();
} else {
color = cornerstoneTools.toolColors.getToolColor();
}
// Draw the arrow
var handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, lesion.handles.start);
var handleEndCanvas = cornerstone.pixelToCanvas(eventData.element, lesion.handles.end);
var canvasTextLocation = cornerstone.pixelToCanvas(eventData.element, lesion.handles.textBox);
if (config.arrowFirst) {
drawArrow(context, handleEndCanvas, handleStartCanvas, color, lineWidth);
} else {
drawArrow(context, handleStartCanvas, handleEndCanvas, color, lineWidth);
}
if (config.drawHandles) {
cornerstoneTools.drawHandles(context, eventData, lesion.handles, color);
} else if (config.drawHandlesOnHover && lesion.handles.start.active) {
cornerstoneTools.drawHandles(context, eventData, [ lesion.handles.start ], color);
} else if (config.drawHandlesOnHover && lesion.handles.end.active) {
cornerstoneTools.drawHandles(context, eventData, [ lesion.handles.end ], color);
}
//Draw linked line as dashed
var mid = {
x: (handleStartCanvas.x + handleEndCanvas.x) / 2,
y: (handleStartCanvas.y + handleEndCanvas.y) / 2
};
context.setLineDash([2, 3]);
context.beginPath();
context.strokeStyle = color;
context.lineWidth = 1 / eventData.viewport.scale;
var mid = {
x: mid.x,
y: mid.y
};
context.moveTo(mid.x, mid.y);
context.lineTo(canvasTextLocation.x + 20, canvasTextLocation.y + 20);
context.stroke();
// Draw the text
if (lesion.lesionName && lesion.lesionName !== '') {
context.font = font;
var boundingBox = cornerstoneTools.drawTextBox(context, lesion.lesionName, canvasTextLocation.x, canvasTextLocation.y, color);
lesion.handles.textBox.boundingBox = boundingBox;
}
context.restore();
}
// ---- Touch tool ----
///////// BEGIN ACTIVE TOOL ///////
function addNewMeasurementTouch(touchEventData) {
var element = touchEventData.element;
function doneCallback(lesionNumber) {
measurementData.lesionName = "Non-Target "+lesionNumber;
measurementData.lesionNumber = lesionNumber;
measurementData.active = true;
cornerstone.updateImage(element);
}
var measurementData = createNewMeasurement(touchEventData);
cornerstoneTools.addToolState(element, toolType, measurementData);
$(element).off('CornerstoneToolsTouchDrag', cornerstoneTools.nonTargetTouch.touchMoveHandle);
$(element).off('CornerstoneToolsDragStartActive', cornerstoneTools.nonTargetTouch.touchDownActivateCallback);
$(element).off('CornerstoneToolsTap', cornerstoneTools.nonTargetTouch.tapCallback);
var config = cornerstoneTools.nonTarget.getConfiguration();
// Set lesion number and lesion name
if (measurementData.lesionName === undefined) {
config.setLesionNumberCallback(measurementData, touchEventData, doneCallback);
}
cornerstone.updateImage(element);
cornerstoneTools.moveNewHandleTouch(touchEventData, measurementData.handles.end, function() {
cornerstone.updateImage(element);
if (cornerstoneTools.anyHandlesOutsideImage(touchEventData, measurementData.handles)) {
// delete the measurement
cornerstoneTools.removeToolState(element, toolType, measurementData);
}
config.getNonTargetLesionLocationCallback(measurementData, touchEventData, doneCallback);
$(element).on('CornerstoneToolsTouchDrag', cornerstoneTools.nonTargetTouch.touchMoveHandle);
$(element).on('CornerstoneToolsDragStartActive', cornerstoneTools.nonTargetTouch.touchDownActivateCallback);
$(element).on('CornerstoneToolsTap', cornerstoneTools.nonTargetTouch.tapCallback);
});
}
function doubleClickCallback(e, eventData) {
var element = eventData.element;
var data;
function doneChangingTextCallback(data, updatedText, deleteTool) {
if (deleteTool === true) {
cornerstoneTools.removeToolState(element, toolType, data);
} else {
data.text = updatedText;
}
data.active = false;
cornerstone.updateImage(element);
}
if (e.data && e.data.mouseButtonMask && !cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
return false;
}
var config = cornerstoneTools.nonTarget.getConfiguration();
var coords = eventData.currentPoints.canvas;
var toolData = cornerstoneTools.getToolState(element, toolType);
// now check to see if there is a handle we can move
if (!toolData) {
return false;
}
for (var i = 0; i < toolData.data.length; i++) {
data = toolData.data[i];
if (pointNearTool(element, data, coords)) {
data.active = true;
cornerstone.updateImage(element);
// Allow relabelling via a callback
config.changeTextCallback(data, doneChangingTextCallback);
e.stopImmediatePropagation();
return false;
}
}
return false; // false = causes jquery to preventDefault() and stopPropagation() this event
}
function updateLesion(e, eventObject) {
var start = new Date();
var enabledElement = eventObject.enabledElement;
var isTarget = eventObject.lesionData.isTarget;
var lesionNumber = eventObject.lesionData.lesionNumber;
var type = eventObject.type;
// if we have no toolData for this element, return immediately as there is nothing to do
var toolData = cornerstoneTools.getToolState(e.currentTarget, toolType);
if (toolData === undefined) {
return;
}
//If type is delete, remove measurement
if (type === "delete") {
var deletedDataIndex = -1;
for (var i = 0; i < toolData.data.length; i++) {
var data = toolData.data[i];
//When click a row of table measurements, measurement will be active and color will be green
if (data.lesionNumber === eventObject.lesionNumber && eventObject.type !== "active" && !isTarget) {
data.visible = false;
deletedDataIndex = i;
}
}
if (deletedDataIndex >= 0 && deletedDataIndex < toolData.data.length) {
toolData.data.splice(deletedDataIndex, 1);
}
} else if (type === "active") {
for (var i = 0; i < toolData.data.length; i++) {
var data = toolData.data[i];
//When click a row of table measurements, measurement will be active and color will be green
if (data.lesionNumber === eventObject.lesionData.lesionNumber && eventObject.type === "active" && !isTarget) {
data.active = true;
} else {
data.active = false;
}
}
} else if (type === "inactive") {
for (var i = 0; i < toolData.data.length; i++) {
var data = toolData.data[i];
// Make inactive all lesions for the timepoint
data.active = false;
}
}
var context = enabledElement.canvas.getContext('2d');
var end = new Date();
var diff = end - start;
//console.log(diff + ' ms');
var eventData = {
viewport: enabledElement.viewport,
element: enabledElement.element,
image: enabledElement.image,
enabledElement: enabledElement,
canvasContext: context,
measurementText: "",
renderTimeInMs: diff,
lesionNumber: lesionNumber,
isTarget: isTarget,
type: type //Holds image will be deleted or active
};
enabledElement.invalid = false;
onImageRendered(e, eventData);
}
function loadImage(e, eventObject) {
// If type is active, load image and activate lesion
// If type is inactive, update lesions of enabledElement as inactive
if (eventObject.type === "active") {
var stackToolDataSource = cornerstoneTools.getToolState(e.currentTarget, 'stack');
var stackData = stackToolDataSource.data[0];
var imageIdsArr = stackData.imageIds;
var indexOfImage = imageIdsArr.indexOf(eventObject.lesionData.imageId);
if (indexOfImage > -1) {
cornerstone.loadAndCacheImage(stackData.imageIds[indexOfImage]).then(function(image) {
cornerstone.displayImage(eventObject.enabledElement.element, image);
updateLesion(e, eventObject);
});
}
} else if (eventObject.type === "inactive") {
updateLesion(e, eventObject);
}
}
//This function is called from cornerstone-viewport.html and updates lesion measurement and makes the lesion active
function measurementModified(e, eventObject) {
loadImage(e, eventObject);
}
cornerstoneTools.nonTarget = cornerstoneTools.mouseButtonTool({
addNewMeasurement: addNewMeasurement,
createNewMeasurement: createNewMeasurement,
onImageRendered: onImageRendered,
pointNearTool: pointNearTool,
toolType: toolType
//mouseDoubleClickCallback: doubleClickCallback
});
cornerstoneTools.nonTarget.setConfiguration(configuration);
cornerstoneTools.nonTargetTouch = cornerstoneTools.touchTool({
addNewMeasurement: addNewMeasurementTouch,
createNewMeasurement: createNewMeasurement,
onImageRendered: onImageRendered,
pointNearTool: pointNearTool,
toolType: toolType
// pressCallback: doubleClickCallback
});
})($, cornerstone, cornerstoneMath, cornerstoneTools);

View File

@ -1,3 +0,0 @@
<template name="lesionDialog">
{{> lesionLocationDialog}}
</template>

View File

@ -1,46 +0,0 @@
Template.lesionDialog.onRendered(function () {
// Show Lesion Dialog
$(document).on("ShowLesionDialog", function (e, eventData) {
var dialogPointsOnPage = eventData.currentPoints.page;
$("#modal-dialog-container").css({
"top": dialogPointsOnPage.y,
"left": dialogPointsOnPage.x
});
$("#lesionDialog").modal("show");
});
});
// Listen lesionMeaurementCreated Event and when this event is triggered, show lesionLocationDialog Box
/*
$(document).on("lesionMeasurementCreated",function(e,eventCaller,data){
console.log("Lesion measurement xreated");
var element = data.element;
var tabId = data.tabId;
var viewports = $("#content"+tabId).find(".imageViewerViewport");
var viewportIndex = $(viewports).index($(element));
data.viewportIndex = viewportIndex;
data.totalViewportCount = viewports.length;
//Set Lesion Table Data to insert into lesion table
setLesionTableData(data);
// Check popup will shown or not
var returnVal = lesionNumberIsAdded(data);
if(!returnVal.isFound) {
if( !Session.get("lesionLocationSelected") ) {
$("#modal-dialog-container").css({
"top": eventCaller.pageY,
"left": eventCaller.pageX
});
$("#lesionDialog").modal("show");
}
} else {
// If lesionNumber is added before:
data.rowIndex = returnVal.rowIndex;
$(document).trigger("addLesionToExistedRow",data);
}
});*/

View File

@ -1,19 +1,17 @@
<template name="lesionLocationDialog">
<div class="modal fade" id="lesionDialog" style=" background-color: transparent;">
<div id="modal-dialog-container" class="modal-dialog modal-sm" style="position: absolute; border: none; ">
<div class="modal-content" style="padding: 10px;">
<div class="dialogHeader">
<div style="float:left;"><h4>Select Lesion Location</h4></div>
<div style="float:right;"><button class="btn" id="btnCloseLesionPopup" style="outline: 0; border: none; background-color: transparent;"><i class="fa fa-close"></i></button></div>
</div>
<div class="dialogContent" style="margin-bottom: 20px;">
<div class="lesionLocation">
<label>Lesion Location</label>
<select id="selectLesionLocation" style="margin-top:5px;">
<option value="-1"></option>
</select>
</div>
</div>
<div id="lesionLocationDialog">
<div class="dialogHeader">
<h4>Select Lesion Location</h4>
</div>
<div class="dialogContent">
<div class="lesionLocation">
<label>Lesion Location</label>
<select id="selectLesionLocation">
<option value="-1"></option>
{{ #each lesionLocations}}
<option value='{{_id}}'>{{location}}</option>
{{ /each}}
</select>
</div>
</div>
</div>

View File

@ -1,49 +1,172 @@
//fill selectLesionLocation element
var lesionLocationsArray = [
{location:"Brain Brainstem",hasDescription:false, description:""},
{location:"Brain Cerebellum Left",hasDescription:false, description:""},
{location:"Brain Cerebrum Left",hasDescription:false, description:""},
{location:"Brain Cerebrum Right",hasDescription:false, description:""},
{location:"Brain Multiple Sites",hasDescription:false, description:""}
];
// This event determines whether or not to show the lesion dialog
// If there already exists a lesion with this specific lesion number,
// related to the chosen location.
function getLesionLocationCallback(measurementData, eventData, doneCallback) {
// Get the lesion location dialog
var lesionDialog = $("#lesionLocationDialog");
Template.lesionLocationDialog.onRendered(function () {
// Find the select option box
var selector = lesionDialog.find("select#selectLesionLocation");
function fillSelectLesionLocation () {
var el = $("#selectLesionLocation");
el.find('option:not(:first)').remove();
$.each(lesionLocationsArray, function(key, value) {
el.append("<option value='" + key+ "'>" + value.location + "</option>");
});
// Get the current element's timepointID
// TODO: Change this when we are no longer storing timepointID in the viewport element DOM data
measurementData.timepointID = $(eventData.element).data('timepointID');
// Get a lesion number for this lesion, depending on whether or not the same lesion previously
// exists at a different timepoint
var lesionNumber = measurementManagerDAL.getNewLesionNumber(measurementData.timepointID, isTarget=true);
measurementData.lesionNumber = lesionNumber;
// Find out if this lesion number is already added in the lesion manager for another timepoint
// If it is, stop here because we don't need the dialog.
var locationUID = measurementManagerDAL.lesionNumberExists(measurementData);
if (locationUID) {
measurementData.locationUID = locationUID;
measurementManagerDAL.updateTimepointData(measurementData);
closeHandler();
return;
}
// If it isn't, continue to open the dialog and have the user choose a lesion location
// Attach close handler if the user clicks the close button
var close = lesionDialog.find("#btnCloseLesionPopup");
close.off('click');
close.on('click', function() {
closeHandler();
});
function closeHandler() {
// Hide the lesion dialog
lesionDialog.css('display', 'none');
// Fire the doneCallback with the lesion number
doneCallback(lesionNumber);
// Get the current value of the select option box
var currentValue = selector.find("option:selected").val();
// Select the first option for the next time the dialog is opened
selector.val(currentValue);
}
// Fill dropdown
fillSelectLesionLocation();
});
// Attach keypress handlers so the user can close with the Enter button
lesionDialog.off("keypress");
lesionDialog.on('keypress', keyPressHandler);
Template.lesionLocationDialog.events({
'click button#btnCloseLesionPopup': function (e) {
$("#lesionDialog").modal("hide");
},
'change select#selectLesionLocation': function (e) {
var el = $(e.target);
var selectedLocationIndex = el.val()
if(selectedLocationIndex !== "-1"){
var locationObj = lesionLocationsArray[selectedLocationIndex];
// Trigger location selected event
$(document).trigger("lesionLocationSelected",locationObj);
// Set activeModule parameters in index.html
$("#lesionDialog").modal("hide");
// Select first option
el.val($("#selectLesionLocation option:first").val());
// Set lesion location selected session to prevent open
Session.set("lesionLocationSelected", true);
// This is the keypress callback function
function keyPressHandler(e) {
// If Enter is pressed, close the dialog
if (e.which === 13) {
closeHandler();
}
}
// Show the lesion location dialog above
lesionDialog.css({
top: eventData.currentPoints.page.y - lesionDialog.outerHeight() - 40,
left: eventData.currentPoints.page.x - lesionDialog.outerWidth() / 2,
display: 'block'
});
// Attach a callback for the select box
selector.off('change');
selector.on('change', function(e) {
// Get the current value of the selector
var selectedOptionId = this.value;
// If the selected option is still the default (-1)
// then stop here
if (selectedOptionId < 0) {
return;
}
// Get selected location data
var locationObj = LesionLocations.findOne({_id: selectedOptionId});
var id;
var existingLocation = PatientLocations.findOne({location: locationObj.location});
if (existingLocation) {
id = existingLocation._id;
} else {
// Adds location data to PatientLocation and retrieve the location ID
id = PatientLocations.insert({location: locationObj.location});
}
// Link locationUID with active lesion measurementData
measurementData.locationUID = id;
/// Set the isTarget value to true, since this is the target-lesion dialog callback
measurementData.isTarget = true;
// Adds lesion data to timepoints array
measurementManagerDAL.addLesionData(measurementData);
// Close the dialog
closeHandler();
});
}
function changeLesionLocationCallback(measurementData, eventData, doneCallback) {
doneCallback(prompt('Change your lesion location:'));
}
var config = {
getLesionLocationCallback: getLesionLocationCallback,
changeLesionLocationCallback: changeLesionLocationCallback
};
cornerstoneTools.lesion.setConfiguration(config);
LesionLocations = new Meteor.Collection(null);
LesionLocations.insert({
location: "Brain Brainstem",
hasDescription: false,
description: ""
});
LesionLocations.insert({
location: "Brain Cerebellum Left",
hasDescription: false,
description: ""
});
LesionLocations.insert({
location: "Brain Cerebrum Left",
hasDescription: false,
description: ""
});
LesionLocations.insert({
location: "Brain Cerebrum Right",
hasDescription: false,
description: ""
});
LesionLocations.insert({
location: "Brain Multiple Sites",
hasDescription: false,
description: ""
});
var lastAddedLesionData;
Template.lesionLocationDialog.onRendered(function() {
// Observe Measurements Collection Changes
Measurements.find().observe({
added: function(lesionData) {
lastAddedLesionData = lesionData;
},
changed: function(lesionData) {
console.log("lesionData has changed!");
}
});
});
Template.lesionLocationDialog.helpers({
'lesionLocations': function() {
return LesionLocations.find();
}
});

View File

@ -0,0 +1,19 @@
#lesionLocationDialog
display: none
position: absolute
z-index: 100
background: white
padding: 10px
border-radius: 10px
#closeLesionPopup
outline: 0
border: none
background-color: transparent
#selectLesionLocation
margin-top: 5px
.dialogContent
margin-bottom: 20px

View File

@ -1,23 +1,24 @@
<template name="lesionTable">
<div id="lesionTableContainer">
<table class="table table-bordered lesionTable" id="tblLesion">
<table class="table table-striped noselect lesionTable" id="tblLesion">
<thead>
<tr>
<th id="thNumber">Lesion #</th>
<th id="thTarget">Target</th>
<th id="thLocation">Location</th>
<th id="thTarget">Target</th>
<!--Each time point name as a column-->
{{ #each timepointNames }}
{{ >lesionTableTimepointHeader }}
{{ #each timepoints }}
{{ >lesionTableTimepointHeader }}
{{ /each }}
<!--What is this for?-->
<!--<th id="thSpacer"> </th>-->
</tr>
</thead>
<tbody>
<!--Each measurement as row-->
{{ #each measurement }}
{{ >lesionTableRow }}
{{ /each }}
<!--Each measurement as row-->
<!--Each time point as a column-->
</tbody>
</table>
</div>

View File

@ -1,187 +1,95 @@
measurements = new Meteor.Collection(null);
Measurements = new Meteor.Collection(null);
Timepoints = new Meteor.Collection(null);
var test = {
lesionNumber: 1,
isTarget: true,
location: 'Brain',
timepoints: {
"now": {
imageId: "blah",
timepointUid: "timepointX",
date: 2010,
isDirty: true,
longestDiameter: 5
},
"earlier": {
imageId: "blah2",
timepointUid: "timepointy",
date: 2010,
isDirty: true,
longestDiameter: 8
// Activate selected lesions when lesion table row is clicked
function updateLesions(e) {
// lesionNumber of measurement = id of row
var lesionNumber = parseInt($(e.currentTarget).attr("id"), 10);
var isTarget = $(e.currentTarget).find('td').eq(2).html().trim() === 'N'?false:true;
// Find data for specific lesion
var measurementData = Measurements.find({
lesionNumber: lesionNumber,
isTarget: isTarget
}).fetch()[0];
var timepoints = measurementData.timepoints;
$(".imageViewerViewport").each(function(index, element) {
// Get the timepointID related to the image viewer viewport
// from the DOM itself. This will be changed later when a
// real association between viewports and timepoints is created.
var timepointID = $(element).data('timepointID');
var timepointObject = timepoints[timepointID];
// Defines event data
var eventData = {
enabledElement: cornerstone.getEnabledElement(element),
lesionData: {
isTarget: isTarget,
lesionNumber: lesionNumber,
imageId: timepointObject.imageId
},
type: "active"
};
if (timepointObject.longestDiameter === "") {
eventData.type = "inactive";
}
}
};
measurements.insert(test);
if (!isTarget) {
$(element).trigger("NonTargetToolSelected", eventData);
// Deactivate lesion tool measurements
eventData.type = "inactive";
$(element).trigger("LesionToolSelected", eventData);
return;
}
$(element).trigger("LesionToolSelected", eventData);
// Deactivate nonTarget tool measurements
eventData.type = "inactive";
$(element).trigger("NonTargetToolSelected", eventData);
});
}
Template.lesionTable.onRendered(function() {
// For the moment we will associate the timepoint
// with the viewport element by storing the timepointID
// inside the element's DOM data. This is temporary.
$(".imageViewerViewport").each(function(index, element) {
var timepointID = uuid.v4();
var timepointName = "Baseline";
if (index > 0) {
timepointName = "Current"; //"Follow Up "+i;
}
// FUTURE = On load series data into viewport, create a new timepoint
// unless it already exists
Timepoints.insert({
timepointID: timepointID,
timepointName: timepointName
});
$(element).data('timepointID', timepointID);
});
});
Template.lesionTable.helpers({
'measurement': function() {
return measurements.find();
return Measurements.find();
},
'timepointNames': function() {
return [
{name: 'now'},
{name: 'earlier'}
];
'timepoints': function() {
return Timepoints.find();
}
});
/*var lesionTableData;
setLesionTableData = function (data) {
lesionTableData = data;
};
getLesionTableData = function () {
return lesionTableData;
};
lesionNumberIsAdded = function(data) {
var tabId = data.tabId;
var returnValue = {isFound: false, rowIndex : -1};
$('#content'+tabId+' table tbody tr td.lesionNumber').each( function(){
console.log($(this).text());
var lesionNumber = parseInt($(this).text());
if(lesionNumber == data.lesionNumber) {
var $tr = $(this).closest('tr');
var rowIndex = $tr.index();
returnValue.rowIndex = rowIndex;
returnValue.isFound = true;
}
});
return returnValue;
};
returnTblStr = function (obj) {
// Get count of lds
var tabId = obj.tabId;
var lds = $("#content"+tabId).find(".lesionTable th.ld");
var rowStr = '<tr style="background-color: #b3b3b3;">' +
'<td style="text-align:center;"><button type="button" class="btnRemove fa fa-times fa-lg" title="Remove"></button></td>' +
'<td style="text-align:center;" class="lesionNumber">'+obj.lesionNumber+'</td>' +
'<td style="text-align:center;">Y</td>';
// Add measurement to right column
if(obj.totalViewportCount) {
for(var i=0; i< lds.length; i++) {
if(i === obj.viewportIndex) {
rowStr += '<td>'+obj.lesionLocationObj.location+'</td><td id='+obj.lineIndex+' class="lesion" style="text-align: left;">'+obj.lesions+'</td>';
}else{
rowStr += '<td>'+obj.lesionLocationObj.location+'</td><td class="lesion" style="text-align: left;"></td>';
}
}
}
rowStr += '</tr>';
return rowStr;
};
//Link lesionLocationSelected event
$(document).on("lesionLocationSelected",function(event,data ){
var lesionData = getLesionTableData();
lesionData.lesionLocationObj = data;
var tabId = lesionData.tabId;
//var el_index = lesionData.viewportIndex;
var tableEl = $("#content"+tabId).find(".lesionTable");
var tblStr = returnTblStr(lesionData);
$(tableEl).append(tblStr);
});
//Link addLesionToExistedRow event
$(document).on("addLesionToExistedRow",function(event,data ){
// Get count of lds
var tabId = data.tabId;
var lds = $("#content"+tabId).find(".lesionTable th.ld");
// Add measurement to right column
for(var i=0; i< lds.length; i++) {
if(i === data.viewportIndex) {
var rowIndex = data.rowIndex + 1;
var row = $("#content"+tabId).find(".lesionTable tr").eq(rowIndex);
var cell = $(row).find("td.lesion").eq(data.viewportIndex);
$(cell).attr("id", data.lineIndex);
$(cell).html(data.lesions);
}
}
});
$(document).on("lesionTextChanged", function(event, data){
var cellId = data.index;
$("td#"+cellId).html(data.measurementText);
});
Template.lesionTable.onRendered( function () {
// TODO: Set lesion table as hiding-panel
console.log(this);
var template = this;
var tabId = this.data.tabId;
var viewports = $("#content"+tabId).find(".imageViewerViewport");
var lesionTable = $("#content"+tabId).find(".lesionTable");
var viewportCount = viewports.length;
var lesionTableWidth = $(lesionTable).width();
var viewportWidth = viewports.width();
var ldColumnRatio = viewportWidth / lesionTableWidth * 100;
// Add LD column as viewportCount
// Set lesion table columns width
if(viewportCount > 0) {
if(viewportCount == 1) {
$('#content'+tabId+' thead tr').append( $('<th />', {text : 'Location', width: ldColumnRatio/2+'%', class:'thLocation'}) )
$('#content'+tabId+' thead tr').append( $('<th />', {text : 'LD', width: ldColumnRatio/2+'%', class:'ld'}) )
} else if(viewportCount == 2) {
for(var i = 0; i< viewportCount; i++) {
$('#content'+tabId+' thead tr').append( $('<th />', {text : 'Location'+i, width: ldColumnRatio/2+'%', class:'thLocation'}) )
$('#content'+tabId+' thead tr').append( $('<th />', {text : 'LD'+i, width: ldColumnRatio/2+'%', class:'ld'}) )
}
} else if(viewportCount > 2) {
// TODO: Set when viewport count is bigger than 2
}
}
// TODO: When layout is changed, add and calculate new columns for measurement
});
Template.lesionTable.events({
'click button.btnRemove': function (e) {
var el = $(e.target);
},
'click table tbody tr': function(e) {
console.log(this);
var row = e.currentTarget;
var lesionCells = $(row).find("td.lesion");
for(var i = 0; i< lesionCells.length; i++) {
var cell = lesionCells[i];
var idAttr = $(cell).attr("id");
if(idAttr != undefined) {
// TODO: Create eventObject and trigger LesionToolModified event
var viewports = $(".imageViewerViewport");
var eventObject = {
enabledElement: cornerstone.getEnabledElement(viewports[0]),
lineIndex: parseInt(idAttr),
type: "active"
};
$(viewports[0]).trigger("LesionToolModified", eventObject);
}
}
'click table#tblLesion tbody tr': function(e) {
updateLesions(e);
}
});*/
});

View File

@ -1,44 +1,76 @@
#lesionTableContainer
width: 100%
height: 25%
width: 100%
height: 16%
.lesionTable
table.lesionTable
thead
th
background-color: #424242
font-style: italic
td, th
border: 1px solid white
border-collapse: collapse
td.lesionNumber, th#thNumber
width: 70px
td.location, th#thLocation
width: 250px
td.target, th#thTarget
width: 70px
td.lesionTableTimepointCell
width: 70px
thead
width: 100%
display: block
tr
width: 100%
th
border-bottom-width: 0
font-weight: normal
color:white
line-height: 15px
text-align: center
height: 15px
width: 20px
margin: 0
padding: 5px
background-color: #424242
width: 70px
table.lesionTable > thead > tr > th {
padding: 4px;
margin: 0;
height: 25px;
line-height: 15px;
}
#thTarget
width: 1%
table.lesionTable > tbody > tr > td {
padding: 4px;
margin: 0;
height: 25px;
line-height: 15px;
}
#thLocation
text-align: left
width: 15%
table.lesionTable tbody tr:hover{
background-color: #d1d1d1;
}
#thSpacer
width: 61%
tbody
display: block
height: 150px // Seems like this has to be fixed for overflow to work properly!
overflow: auto
width: 100%
table.lesionTable tbody tr{
background-color: #b3b3b3;
color: #424242;
line-height: 14px;
}
tr.lesionTableRow
width: 100%
&:nth-child(even)
background: #888
&:nth-child(odd)
background: #BBB
&:hover
cursor: pointer
background-color: #009BD2
color: white
.btnRemove{
border: none;
outline:none;
background-color: transparent;
}
td
border-bottom-width: 0
margin: 0
padding: 5px
line-height: 15px
&.lesionNumber, &.target, &.lesionTableTimepointCell
text-align: center

View File

@ -1,11 +1,11 @@
<template name="lesionTableRow">
<tr>
<th class='lesionNumber'>{{lesionNumber}}</th>
<th class='target'>{{isTarget}}</th>
<th class='location'>{{location}}</th>
<tr id="{{lesionNumber}}" class="lesionTableRow">
<td class='lesionNumber'>{{number}}</td>
<td class='location'>{{location}}</td>
<td class='target'>{{#if isTarget}} Y {{else}} N {{/if}}</td>
<!--Each time point as a column-->
{{ #each timepoints}}
{{ >lesionTableTimepointCell}}
{{ /each }}
{{# each timepoints }}
{{> lesionTableTimepointCell}}
{{/ each }}
</tr>
</template>

View File

@ -1,10 +1,5 @@
Template.lesionTableRow.helpers({
'timepoints': function() {
var array = [];
var timepoints = this.timepoints;
Object.keys(timepoints).forEach(function(key) {
array.push(timepoints[key]);
});
return array;
},
return Timepoints.find();
}
});

View File

@ -1,3 +1,3 @@
<template name="lesionTableTimepointCell">
<td>{{longestDiameter}}</td>
<td class="lesionTableTimepointCell">{{longestDiameter}}</td>
</template>

View File

@ -0,0 +1,7 @@
Template.lesionTableTimepointCell.helpers({
'longestDiameter': function() {
// Search Measurements by lesion and timepoint
var lesionData = Template.parentData(1);
return lesionData.timepoints[this.timepointID].longestDiameter;
}
});

View File

@ -1,3 +1,3 @@
<template name="lesionTableTimepointHeader">
<th>{{name}}</th>
<th>{{timepointName}}</th>
</template>

View File

@ -0,0 +1,32 @@
#nonTargetLesionLocationDialog {
display: none;
position: absolute;
z-index: 100;
background: white;
padding: 10px 20px 10px 20px;
border-radius: 10px;
}
#selectNonTargetLesionLocation, #selectNonTargetLesionLocationResponse {
width: 100%;
}
.dialogContent, .lesionLocation, .locationResponse {
margin-bottom: 20px;
}
.elementContainer {
width: 100%;
margin: 0 auto;
float: none;
}
.locationOK {
text-align: center;
}
.btn{
background-color: #bbb;
border: 1px solid #888;
color: #000000;
}

View File

@ -0,0 +1,43 @@
<template name="nonTargetLesionDialog">
<div id="nonTargetLesionLocationDialog">
<div class="dialogHeader">
<div class="dialogHeader">
<div><h4>Select Lesion Location</h4></div>
</div>
</div>
<div class="dialogContent">
<div class="lesionLocation">
<div class="elementContainer">
<label>Lesion Location</label>
</div>
<div class="elementContainer">
<select id="selectNonTargetLesionLocation">
<option value="-1"></option>
{{ #each lesionLocations}}
<option value='{{_id}}'>{{location}}</option>
{{ /each}}
</select>
</div>
</div>
<div class="locationResponse">
<div class="elementContainer">
<label>Response</label>
</div>
<div class="elementContainer">
<select id="selectNonTargetLesionLocationResponse">
<option value="-1"></option>
{{ #each locationResponses}}
<option value='{{code}}'>{{code}} - {{text}}</option>
{{ /each}}
</select>
</div>
</div>
<div class="locationOK">
<button class="btn" id="nonTargetLesionOK">OK</button>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,231 @@
// This event sets lesion number for new lesion
function setLesionNumberCallback(measurementData, eventData, doneCallback) {
measurementData.timepointID = $(eventData.element).data('timepointID');
// Get a lesion number for this lesion, depending on whether or not the same lesion previously
// exists at a different timepoint
var lesionNumber = measurementManagerDAL.getNewLesionNumber(measurementData.timepointID, isTarget=false);
measurementData.lesionNumber = lesionNumber;
// Set lesion number
doneCallback(lesionNumber);
}
// This event determines whether or not to show the Non-Target lesion dialog
// If there already exists a lesion with this specific lesion number,
// related to the chosen location.
function getNonTargetLesionLocationCallback(measurementData, eventData, doneCallback) {
// Get the non-target lesion location dialog
var nonTargetlesionDialog = $("#nonTargetLesionLocationDialog");
// Find the select option box
var selectorLocation = nonTargetlesionDialog.find("select#selectNonTargetLesionLocation");
var selectorResponse = nonTargetlesionDialog.find("select#selectNonTargetLesionLocationResponse");
// OK button
var btnOK = nonTargetlesionDialog.find("button#nonTargetLesionOK");
// If selector location is disabled, make it enable
enableLocationSelection();
// Find out if this lesion number is already added in the lesion manager for another timepoint
// If it is, disable selector location
var locationUID = measurementManagerDAL.lesionNumberExists(measurementData);
if (locationUID) {
measurementData.locationUID = locationUID;
// Disable the selection of a new location
disableLocationSelection(measurementData.locationUID);
}
// Disable selector location to prevent selecting a new location
function disableLocationSelection(locationUID) {
var locationName = measurementManagerDAL.getLocationName(locationUID);
selectorLocation.find('option').each(function()
{
if ($(this).text() === locationName) {
// Select location in locations dropdown list
selectorLocation.find('option').eq($(this).index()).prop("selected", true);
return;
}
});
selectorLocation.prop("disabled", true);
}
// Enable selector location
function enableLocationSelection() {
selectorLocation.removeAttr("disabled");
}
function closeHandler() {
// Hide the lesion dialog
nonTargetlesionDialog.css('display', 'none');
// Enable selector location
enableLocationSelection();
// Get the current value of the select option box
selectorLocation.find("option:first").prop("selected", "selected");
selectorResponse.find("option:first").prop("selected", "selected");
}
// Attach keypress handlers so the user can close with the Enter button
nonTargetlesionDialog.off("keypress");
nonTargetlesionDialog.on('keypress', keyPressHandler);
// This is the keypress callback function
function keyPressHandler(e) {
// If Enter is pressed, close the dialog
if (e.which === 13) {
closeHandler();
}
}
// Show the lesion location dialog above
nonTargetlesionDialog.css({
top: eventData.currentPoints.page.y,
left: eventData.currentPoints.page.x,
display: 'block'
});
// Click OK button
// Add lesion to lesion table
btnOK.off('click');
btnOK.on('click', function(e) {
// Get the current value of the selector
var selectedOptionId = selectorLocation.find("option:selected").val();
var responseOptionCode = nonTargetlesionDialog.find("select#selectNonTargetLesionLocationResponse option:selected").val();
// If the selected option is still the default (-1)
// then stop here
if (selectedOptionId < 0) {
return;
}
// If the selected response option is still the default (-1)
// then stop here
if (responseOptionCode < 0) {
return;
}
// Get selected location data
var locationObj = LesionLocations.findOne({_id: selectedOptionId});
var id;
var existingLocation = PatientLocations.findOne({location: locationObj.location});
if (existingLocation) {
id = existingLocation._id;
} else {
// Adds location data to PatientLocation and retrieve the location ID
id = PatientLocations.insert({location: locationObj.location});
}
// Link locationUID with active lesion measurementData
measurementData.locationUID = id;
/// Set the isTarget value to true, since this is the target-lesion dialog callback
measurementData.isTarget = false;
// measurementText is set from location response list
measurementData.measurementText = responseOptionCode;
// Adds lesion data to timepoints array
measurementManagerDAL.addLesionData(measurementData);
// Close the dialog
closeHandler();
});
}
function changeNonTargetLesionLocationCallback(measurementData, eventData, doneCallback) {
doneCallback(prompt('Change your lesion location:'));
}
var config = {
setLesionNumberCallback: setLesionNumberCallback,
getNonTargetLesionLocationCallback: getNonTargetLesionLocationCallback,
changeNonTargetLesionLocationCallback: changeNonTargetLesionLocationCallback
};
cornerstoneTools.nonTarget.setConfiguration(config);
LocationResponses = new Meteor.Collection(null);
LocationResponses.insert(
{
text: "Complete response",
code: "CR",
description: ""
}
);
LocationResponses.insert(
{
text: "Progressive disease",
code: "PD",
description: ""
}
);
LocationResponses.insert(
{
text: "Complete response",
code: "CR",
description: ""
}
);
LocationResponses.insert(
{
text: "Stable disease",
code: "SD",
description: ""
}
);
LocationResponses.insert(
{
text: "Non-measurable",
code: "NM",
description: ""
}
);
LocationResponses.insert(
{
text: "Unknown",
code: "UN",
description: ""
}
);
LocationResponses.insert(
{
text: "Not Evaluable",
code: "NE",
description: ""
}
);
LocationResponses.insert(
{
text: "Non-CR/Non-PD",
code: "NN",
description: ""
}
);
LocationResponses.insert(
{
text: "Excluded",
code: "EX",
description: ""
}
);
Template.nonTargetLesionDialog.helpers({
'lesionLocations': function() {
return LesionLocations.find();
},
'locationResponses': function() {
return LocationResponses.find();
}
});

View File

@ -0,0 +1,250 @@
// uuid.js
//
// Copyright (c) 2010-2012 Robert Kieffer
// MIT License - http://opensource.org/licenses/mit-license.php
(function() {
var _global = this;
// Unique ID creation requires a high quality random # generator. We feature
// detect to determine the best RNG source, normalizing to a function that
// returns 128-bits of randomness, since that's what's usually required
var _rng;
// Allow for MSIE11 msCrypto
var _crypto = _global.crypto || _global.msCrypto;
// Node.js crypto-based RNG - http://nodejs.org/docs/v0.6.2/api/crypto.html
//
// Moderately fast, high quality
if (typeof(_global.require) == 'function') {
try {
var _rb = _global.require('crypto').randomBytes;
_rng = _rb && function() {return _rb(16);};
} catch(e) {}
}
if (!_rng && _crypto && _crypto.getRandomValues) {
// WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto
//
// Moderately fast, high quality
var _rnds8 = new Uint8Array(16);
_rng = function whatwgRNG() {
_crypto.getRandomValues(_rnds8);
return _rnds8;
};
}
if (!_rng) {
// Math.random()-based (RNG)
//
// If all else fails, use Math.random(). It's fast, but is of unspecified
// quality.
var _rnds = new Array(16);
_rng = function() {
for (var i = 0, r; i < 16; i++) {
if ((i & 0x03) === 0) r = Math.random() * 0x100000000;
_rnds[i] = r >>> ((i & 0x03) << 3) & 0xff;
}
return _rnds;
};
}
// Buffer class to use
var BufferClass = typeof(_global.Buffer) == 'function' ? _global.Buffer : Array;
// Maps for number <-> hex string conversion
var _byteToHex = [];
var _hexToByte = {};
for (var i = 0; i < 256; i++) {
_byteToHex[i] = (i + 0x100).toString(16).substr(1);
_hexToByte[_byteToHex[i]] = i;
}
// **`parse()` - Parse a UUID into it's component bytes**
function parse(s, buf, offset) {
var i = (buf && offset) || 0, ii = 0;
buf = buf || [];
s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) {
if (ii < 16) { // Don't overflow!
buf[i + ii++] = _hexToByte[oct];
}
});
// Zero out remaining bytes if string was short
while (ii < 16) {
buf[i + ii++] = 0;
}
return buf;
}
// **`unparse()` - Convert UUID byte array (ala parse()) into a string**
function unparse(buf, offset) {
var i = offset || 0, bth = _byteToHex;
return bth[buf[i++]] + bth[buf[i++]] +
bth[buf[i++]] + bth[buf[i++]] + '-' +
bth[buf[i++]] + bth[buf[i++]] + '-' +
bth[buf[i++]] + bth[buf[i++]] + '-' +
bth[buf[i++]] + bth[buf[i++]] + '-' +
bth[buf[i++]] + bth[buf[i++]] +
bth[buf[i++]] + bth[buf[i++]] +
bth[buf[i++]] + bth[buf[i++]];
}
// **`v1()` - Generate time-based UUID**
//
// Inspired by https://github.com/LiosK/UUID.js
// and http://docs.python.org/library/uuid.html
// random #'s we need to init node and clockseq
var _seedBytes = _rng();
// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
var _nodeId = [
_seedBytes[0] | 0x01,
_seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]
];
// Per 4.2.2, randomize (14 bit) clockseq
var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff;
// Previous uuid creation time
var _lastMSecs = 0, _lastNSecs = 0;
// See https://github.com/broofa/node-uuid for API details
function v1(options, buf, offset) {
var i = buf && offset || 0;
var b = buf || [];
options = options || {};
var clockseq = options.clockseq != null ? options.clockseq : _clockseq;
// UUID timestamps are 100 nano-second units since the Gregorian epoch,
// (1582-10-15 00:00). JSNumbers aren't precise enough for this, so
// time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
// (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
var msecs = options.msecs != null ? options.msecs : new Date().getTime();
// Per 4.2.1.2, use count of uuid's generated during the current clock
// cycle to simulate higher resolution clock
var nsecs = options.nsecs != null ? options.nsecs : _lastNSecs + 1;
// Time since last uuid creation (in msecs)
var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000;
// Per 4.2.1.2, Bump clockseq on clock regression
if (dt < 0 && options.clockseq == null) {
clockseq = clockseq + 1 & 0x3fff;
}
// Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
// time interval
if ((dt < 0 || msecs > _lastMSecs) && options.nsecs == null) {
nsecs = 0;
}
// Per 4.2.1.2 Throw error if too many uuids are requested
if (nsecs >= 10000) {
throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec');
}
_lastMSecs = msecs;
_lastNSecs = nsecs;
_clockseq = clockseq;
// Per 4.1.4 - Convert from unix epoch to Gregorian epoch
msecs += 12219292800000;
// `time_low`
var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
b[i++] = tl >>> 24 & 0xff;
b[i++] = tl >>> 16 & 0xff;
b[i++] = tl >>> 8 & 0xff;
b[i++] = tl & 0xff;
// `time_mid`
var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff;
b[i++] = tmh >>> 8 & 0xff;
b[i++] = tmh & 0xff;
// `time_high_and_version`
b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
b[i++] = tmh >>> 16 & 0xff;
// `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
b[i++] = clockseq >>> 8 | 0x80;
// `clock_seq_low`
b[i++] = clockseq & 0xff;
// `node`
var node = options.node || _nodeId;
for (var n = 0; n < 6; n++) {
b[i + n] = node[n];
}
return buf ? buf : unparse(b);
}
// **`v4()` - Generate random UUID**
// See https://github.com/broofa/node-uuid for API details
function v4(options, buf, offset) {
// Deprecated - 'format' argument, as supported in v1.2
var i = buf && offset || 0;
if (typeof(options) == 'string') {
buf = options == 'binary' ? new BufferClass(16) : null;
options = null;
}
options = options || {};
var rnds = options.random || (options.rng || _rng)();
// Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
rnds[6] = (rnds[6] & 0x0f) | 0x40;
rnds[8] = (rnds[8] & 0x3f) | 0x80;
// Copy bytes to buffer, if provided
if (buf) {
for (var ii = 0; ii < 16; ii++) {
buf[i + ii] = rnds[ii];
}
}
return buf || unparse(rnds);
}
// Export public API
var uuid = v4;
uuid.v1 = v1;
uuid.v4 = v4;
uuid.parse = parse;
uuid.unparse = unparse;
uuid.BufferClass = BufferClass;
if (typeof(module) != 'undefined' && module.exports) {
// Publish as node.js module
module.exports = uuid;
} else if (typeof define === 'function' && define.amd) {
// Publish as AMD module
define(function() {return uuid;});
} else {
// Publish as global (in browsers)
var _previousRoot = _global.uuid;
// **`noConflict()` - (browser only) to reset global 'uuid' var**
uuid.noConflict = function() {
_global.uuid = _previousRoot;
return uuid;
};
_global.uuid = uuid;
}
}).call(this);

View File

@ -15,12 +15,12 @@ Package.onUse(function (api) {
api.use('cornerstone');
api.addFiles('compatibility/lesionTool.js', 'client', {bare: true});
api.addFiles('components/lesionDialog/lesionDialog.html', 'client');
api.addFiles('components/lesionDialog/lesionDialog.js', 'client');
api.addFiles('compatibility/nonTargetTool.js', 'client', {bare: true});
api.addFiles('compatibility/measurementManagerDAL.js', 'client', {bare: true});
api.addFiles('components/lesionLocationDialog/lesionLocationDialog.html', 'client');
api.addFiles('components/lesionLocationDialog/lesionLocationDialog.js', 'client');
api.addFiles('components/lesionLocationDialog/lesionLocationDialog.styl', 'client');
api.addFiles('components/lesionTable/lesionTable.html', 'client');
api.addFiles('components/lesionTable/lesionTable.styl', 'client');
@ -30,6 +30,20 @@ Package.onUse(function (api) {
api.addFiles('components/lesionTableRow/lesionTableRow.js', 'client');
api.addFiles('components/lesionTableTimepointCell/lesionTableTimepointCell.html', 'client');
api.addFiles('components/lesionTableTimepointCell/lesionTableTimepointCell.js', 'client');
api.addFiles('components/lesionTableTimepointHeader/lesionTableTimepointHeader.html', 'client');
api.addFiles('components/lesionTableTimepointHeader/lesionTableTimepointHeader.js', 'client');
api.addFiles('components/nonTargetLesionDialog/nonTargetLesionDialog.html', 'client');
api.addFiles('components/nonTargetLesionDialog/nonTargetLesionDialog.css', 'client');
api.addFiles('components/nonTargetLesionDialog/nonTargetLesionDialog.js', 'client');
// Library functions
api.addFiles('lib/uuid.js', 'client');
api.export('Measurements', 'client');
api.export('Timepoints', 'client');
});

View File

@ -1,5 +1,12 @@
<template name="hidingPanel">
<div class="hidingPanel">
{{> studyBrowser}}
<div class="toggleBtnContainer">
<button id="btnCollapse">
<i id="btnCollapseIcon" class="btnCollapseIcon fa fa-angle-left fa-lg"></i>
</button>
</div>
<div class="studyBrowserContainer">
{{> studyBrowser}}
</div>
</div>
</template>

View File

@ -0,0 +1,45 @@
Template.hidingPanel.events({
'click button#btnCollapse': function(e,template) {
// Check hidingPanel is open or not
var hidingPanelOpen = template.hidingPanelOpen.get();
hidingPanelOpen = !hidingPanelOpen;
template.hidingPanelOpen.set(hidingPanelOpen);
// Get hidingPanel width from hidingPanelWidth reactiveVar
var contentId = this.contentId;
var hidingPanelElement = $("#"+contentId).find(".hidingPanel");
var hidingPanelWidth = template.hidingPanelWidth.get();
if(hidingPanelWidth == 0) {
hidingPanelWidth = $(hidingPanelElement).width();
template.hidingPanelWidth.set(parseInt(hidingPanelWidth));
}
$(hidingPanelElement).toggleClass("hidingPanelCollapse");
var studyBrowserContainerElement = $("#"+contentId).find(".studyBrowserContainer");
$(studyBrowserContainerElement).toggleClass("studyBrowserContainerCollapse");
var btnCollapseIconElement = $("#"+contentId).find("#btnCollapseIcon");
$(btnCollapseIconElement).toggleClass("btnCollapseIcon-collapse");
// Set viewportAndLesionTable width according to hiding panel status
if (hidingPanelOpen) {
var viewportAndLesionTableElement = $("#"+contentId).find("#viewportAndLesionTable");
var viewportAndLesionTableElementWidth = $(viewportAndLesionTableElement).width();
$(viewportAndLesionTableElement).width(viewportAndLesionTableElementWidth - hidingPanelWidth +"px");
} else {
var viewportAndLesionTableElement = $("#"+contentId).find("#viewportAndLesionTable");
var viewportAndLesionTableElementWidth = $(viewportAndLesionTableElement).width();
$(viewportAndLesionTableElement).width(viewportAndLesionTableElementWidth + hidingPanelWidth +"px");
}
}
});
Template.hidingPanel.onCreated(function() {
this.hidingPanelOpen = new ReactiveVar(true);
this.hidingPanelWidth = new ReactiveVar(0);
});

View File

@ -1,6 +1,70 @@
.hidingPanel
width: 120px
height: 100%
padding-top: 35px
position: relative
float: left
.hidingPanel,.hidingPanelCollapse, .toggleBtnContainer, .studyBrowserContainer, .studyBrowserContainerCollapse, #btnCollapse,.btnCollapseIcon, .btnCollapseIcon-collapse, .viewportStyle {
-o-transition: all 0.3s ease-out;
-ms-transition: all 0.3s ease-out;
-moz-transition: all 0.3s ease-out;
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.hidingPanel {
width: 120px;
height: 100%;
position: relative;
float: left;
}
.hidingPanelCollapse{
width: 0px;
}
.toggleBtnContainer{
position: absolute;
top: 0;
left: 0;
width: 25px;
height: 25px;
}
.studyBrowserContainer {
position: absolute;
display: block;
margin-top: 30px;
height: calc(100% - 30px);
width: 100%;
opacity: 1;
}
.studyBrowserContainerCollapse {
opacity: 0;
width: 0px;
}
.studyBrowserContainerCollapseDisplay {
display: none;
}
#btnCollapse{
position: absolute;
display: block;
width: 25px;
height: 25px;
line-height: 25px;
padding: 0;
border: 0;
border-top: 1px solid #e6e6e6;
background: #f6f6f6;
color: #666;
font-size: 0.875rem;
text-align: center;
cursor: pointer;
}
.btnCollapseIcon-collapse{
-moz-transform: rotate(180deg);
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}

View File

@ -0,0 +1,5 @@
<template name="loadingText">
<div class="loadingTextDiv">
<h5>Loading... <i class="fa fa-spinner fa-pulse"></i></h5>
</div>
</template>

View File

@ -0,0 +1,5 @@
.loadingTextDiv
margin: 30px
h5
text-align: center

View File

@ -0,0 +1,7 @@
<template name="removableBackdrop">
<div class="removableBackdrop noselect"
oncontextmenu='return false;'
unselectable='on'
onselectstart='return false;'>
</div>
</template>

View File

@ -0,0 +1,9 @@
.removableBackdrop
position: fixed
top: 0
right: 0
bottom: 0
left: 0
z-index: 50
background-color: transparent

View File

@ -1,4 +1,10 @@
<template name="imageThumbnail">
<div class="imageThumbnail">
<div class="imageThumbnailLoadingIndicator thumbnailLoadingIndicator">
<p>Loading {{percentComplete}}</p>
</div>
<div class="imageThumbnailErrorLoadingIndicator thumbnailLoadingIndicator">
<p>Error</p>
</div>
</div>
</template>

View File

@ -1,11 +1,33 @@
Template.imageThumbnail.onRendered(function() {
var instance = this.data.instances[0];
var element = this.find('.imageThumbnail');
cornerstone.enable(element);
var instance = this.data.stack.instances[0];
var element = this.find('.imageThumbnail');
cornerstone.enable(element);
var imageId = getImageId(instance);
var imageId = getImageId(instance);
cornerstone.loadAndCacheImage(imageId).then(function(image) {
cornerstone.displayImage(element, image);
});
var elem = $(element);
elem.find('.imageThumbnailLoadingIndicator').css('display', 'block');
var thumbnailIndex = $('.imageThumbnail').index(element);
ThumbnailLoading[thumbnailIndex] = imageId;
this.data.thumbnailIndex = thumbnailIndex;
cornerstone.loadAndCacheImage(imageId).then(function(image) {
cornerstone.displayImage(element, image);
delete ThumbnailLoading[thumbnailIndex];
elem.find('.imageThumbnailLoadingIndicator').css('display', 'none');
}, function(error) {
elem.find('.imageThumbnailErrorLoadingIndicator').css('display', 'block');
});
});
Template.imageThumbnail.helpers({
'percentComplete': function() {
var percentComplete = Session.get('CornerstoneThumbnailLoadProgress' + this.thumbnailIndex);
if (percentComplete && percentComplete !== 100) {
return percentComplete + '%';
}
}
});

View File

@ -2,4 +2,22 @@
width: 90px
height: 90px
background-color: black
margin: 0 auto
margin: 0 auto
position: relative
.thumbnailLoadingIndicator
display: none
pointer-events: none
color: #f8a13f
height: 20px
width: 100%
top: 0
left: 0
right: 0
bottom: 0
margin: auto
position: absolute
p
text-align: center
font-size: 10pt

View File

@ -1,5 +1,14 @@
Template.studyBrowser.helpers({
studies : function() {
return this.studies;
var studies = Session.get('studies');
var array = [];
studies.forEach(function(study, index) {
array.push({
studyIndex: index,
study: study
});
});
return array;
}
});

View File

@ -6,4 +6,4 @@
overflow-y: auto
overflow-x: hidden
background-color: black
padding-bottom: 20px
padding-bottom: 20px

View File

@ -26,13 +26,8 @@ function thumbnailDragStartHandler(e, data) {
var imageThumbnail = $(targetThumbnail);
// Store this data for use during drag and drop
OHIF.viewer.dragAndDropData = {
studyInstanceUid: imageThumbnail.data('studyInstanceUid'),
seriesInstanceUid: imageThumbnail.data('seriesInstanceUid')
};
OHIF.viewer.dragAndDropData = data;
$.extend(OHIF.viewer.dragAndDropData, data);
// Clone the image thumbnail
var targetId = targetThumbnail.id + 'DragClone';
var clone = cloneElement(targetThumbnail, targetId);
@ -204,6 +199,8 @@ Template.thumbnailEntry.events({
// Touch drag/drop events
'touchstart .thumbnailEntry, mousedown .thumbnailEntry': function(e) {
var data = Template.parentData(2);
data.studyInstanceUid = this.stack.studyInstanceUid;
data.seriesInstanceUid = this.stack.seriesInstanceUid;
thumbnailDragStartHandler(e, data);
},
'touchmove .thumbnailEntry': function(e) {
@ -216,9 +213,6 @@ Template.thumbnailEntry.events({
Template.thumbnailEntry.helpers({
seriesDescription : function() {
if (!this.seriesDescription) {
return '';
}
return this.seriesDescription;
return this.stack.seriesDescription || '';
}
});

View File

@ -1,6 +1,15 @@
Template.thumbnails.helpers({
thumbnails: function() {
var stacks = createStacks(this);
return stacks;
}
thumbnails: function() {
var stacks = createStacks(this.study);
var studyIndex = this.studyIndex;
var array = [];
stacks.forEach(function(stack, index) {
array.push({
thumbnailIndex: index * (studyIndex + 1),
stack: stack
});
});
return array;
}
});

View File

@ -1,13 +1,11 @@
/**
* This function enables stack prefetching for a specified element (viewport)
* It first disables any prefetching currently occurring on any other viewports.
* Prefetching will not be enabled if the series is a clip.
*
* @param element
* @param element {node} DOM Node representing the viewport element
*/
function enablePrefetchOnElement(viewportIndex) {
var element = $('.imageViewerViewport').get(viewportIndex);
//console.log('Enabling prefetch on new element');
function enablePrefetchOnElement(element) {
log.info("imageViewerViewport enablePrefetchOnElement");
// Loop through all viewports and disable stackPrefetch
$('.imageViewerViewport').each(function() {
@ -24,10 +22,21 @@ function enablePrefetchOnElement(viewportIndex) {
}
}
function displayReferenceLines(viewportIndex) {
var element = $('.imageViewerViewport').get(viewportIndex);
/**
* This function disables reference lines for a specific viewport element.
* It also enables reference lines for all other viewports with the
* class .imageViewerViewport.
*
* @param element {node} DOM Node representing the viewport element
*/
function displayReferenceLines(element) {
log.info("imageViewerViewport displayReferenceLines");
$('.imageViewerViewport').each(function(index, element) {
// Disable reference lines for the current element
cornerstoneTools.referenceLines.tool.disable(element);
// Loop through all other viewport elements and enable reference lines
$('.imageViewerViewport').not(element).each(function(index, element) {
var imageId;
try {
var enabledElement = cornerstone.getEnabledElement(element);
@ -40,193 +49,363 @@ function displayReferenceLines(viewportIndex) {
return;
}
if (index === viewportIndex) {
cornerstoneTools.referenceLines.tool.disable(element);
return;
}
cornerstoneTools.referenceLines.tool.enable(element, OHIF.viewer.updateImageSynchronizer);
});
}
var allCornerstoneEvents = 'CornerstoneToolsMouseDown CornerstoneToolsMouseDownActivate ' +
'CornerstoneToolsMouseClick CornerstoneToolsMouseDrag CornerstoneToolsMouseUp ' +
'CornerstoneToolsMouseWheel CornerstoneToolsTap CornerstoneToolsTouchPress ' +
'CornerstoneToolsTouchStart CornerstoneToolsTouchStartActive ' +
'CornerstoneToolsMultiTouchDragStart';
/**
* This function loads a study series into a viewport element.
*
* @param data {object} Object containing the study, series, and viewport element to be used
*/
function loadSeriesIntoViewport(data) {
if (!data.series || !data.element) {
log.info("imageViewerViewport loadSeriesIntoViewport");
// Make sure we have all the data required to render the series
if (!data.study || !data.series || !data.element) {
return;
}
var contentId = $("#viewer").parents('.tab-pane.active').attr('id');
var study = data.study;
var series = data.series;
// Get the current element and it's index in the list of all viewports
// The viewport index is often used to store information about a viewport element
var element = data.element;
var viewportIndex = $(".imageViewerViewport").index(element);
var allEvents = 'CornerstoneToolsMouseDown CornerstoneToolsMouseDownActivate ' +
'CornerstoneToolsMouseClick CornerstoneToolsMouseDrag CornerstoneToolsMouseUp ' +
'CornerstoneToolsMouseWheel CornerstoneToolsTap CornerstoneToolsTouchPress ' +
'CornerstoneToolsTouchStart CornerstoneToolsTouchStartActive ' +
'CornerstoneToolsDragStartActive CornerstoneToolsMultiTouchDragStart';
// Get the contentID of the current worklist tab, if the viewport is running
// alongside the worklist package
var contentId = Session.get('activeContentId');
// If the viewer is inside a tab, create an object related to the specified viewport
// This data will be saved so that the tab can be reloaded to the same state after tabs
// are switched
if (contentId) {
ViewerData[contentId].loadedSeriesData[viewportIndex] = {};
}
// Create an empty array to populate with image IDs
var imageIds = [];
// Loop through the current series and add metadata to the
// Cornerstone meta data provider. This will be used to fill information
// into the viewport overlays, and to calculate reference lines and orientation markers
var series = data.series;
var numImages = series.instances.length;
series.instances.forEach(function(instance, imageIndex) {
var imageId = getImageId(instance);
imageIds.push(imageId);
var data = {
var metaData = {
instance: instance,
series: series,
study: study,
study: data.study,
numImages: numImages,
imageIndex: imageIndex + 1
};
addMetaData(imageId, data);
addMetaData(imageId, metaData);
});
// Define the current image stack using the newly created image IDs
var stack = {
currentImageIdIndex: data.currentImageIdIndex || 0,
imageIds: imageIds
};
// Get the current image ID for the stack that will be rendered
var imageId = imageIds[stack.currentImageIdIndex];
// Save the current image ID inside the template data so it can be
// retrieved from the template helpers
var templateData = Template.currentData();
templateData.imageId = imageId;
// Save the current image ID inside the ViewportLoading object.
//
// The ViewportLoading object relates the viewport elements with whichever
// image is currently being loaded into them. This is useful so that we can
// place progress (download %) for each image inside the proper viewports.
ViewportLoading[viewportIndex] = imageId;
// Enable Cornerstone for the viewport element
//
// NOTE: This uses the experimental WebGL renderer for Cornerstone!
cornerstone.enable(element, cornerstone.webGL.renderer.render);
// If you have problems, replace it with this line instead:
// cornerstone.enable(element);
cornerstone.enable(element, cornerstone.webGL.renderer.render);
var startLoadingHandler = cornerstoneTools.loadHandlerManager.getStartLoadHandler();
// Get the handler functions that will run when loading has finished or thrown
// an error. These are used to show/hide loading / error text boxes on each viewport.
var endLoadingHandler = cornerstoneTools.loadHandlerManager.getEndLoadHandler();
var errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler();
if (startLoadingHandler) {
startLoadingHandler(element);
}
// Start loading the image.
cornerstone.loadAndCacheImage(imageId).then(function(image) {
// Fire an event to give notice that a new series was loaded in the viewport
var eventType = "ViewerBaseSeriesLoaded";
var eventData = {
element: element,
stack: stack,
study: data.study,
series: data.series
};
$(document).trigger(eventType, eventData);
// If there is a saved object containing Cornerstone viewport data
// (e.g. scale, invert, window settings) in the input data, apply it now.
//
// Otherwise, display the loaded image in the viewport element with the
// default viewport settings.
if (data.viewport) {
cornerstone.displayImage(element, image, data.viewport);
} else {
cornerstone.displayImage(element, image);
}
if (endLoadingHandler) {
endLoadingHandler(element);
}
// Remove the data for this viewport from the ViewportLoading object
// This will stop the loading percentage complete from being displayed.
delete ViewportLoading[viewportIndex];
// Call the handler function that represents the end of the image loading phase
// (e.g. hide the progress text box)
endLoadingHandler(element);
// Resize the canvas to fit the current viewport element size. Fit the displayed
// image to the canvas dimensions.
cornerstone.resize(element, true);
// Remove the 'empty' class from the viewport to hide any instruction text
element.classList.remove('empty');
// Hide the viewport instructions (i.e. 'Drag a stack here') and show
// the viewport overlay data.
$(element).siblings('.viewportInstructions').hide();
$(element).siblings('.imageViewerViewportOverlay').show();
var imagePlane = cornerstoneTools.metaData.get('imagePlane', image.imageId);
// Add stack state managers for the stack tool, CINE tool, and reference lines
cornerstoneTools.addStackStateManager(element, [ 'stack', 'playClip', 'referenceLines' ]);
// Enable orientation markers, if applicable
// Get the current viewport settings
var viewport = cornerstone.getViewport(element);
updateOrientationMarkers(element, viewport);
// Enable orientation markers, if applicable
updateOrientationMarkers(element);
// Clear any old stack data
cornerstoneTools.clearToolState(element, 'stack');
cornerstoneTools.addToolState(element, 'stack', stack);
// Enable mouse input
// Enable mouse, mouseWheel, and touch input on the element
cornerstoneTools.mouseInput.enable(element);
cornerstoneTools.touchInput.enable(element);
cornerstoneTools.mouseWheelInput.enable(element);
// Use the tool manager to enable the currently active tool for this
// newly rendered element
var activeTool = toolManager.getActiveTool();
toolManager.setActiveTool(activeTool);
cornerstoneTools.magnify.enable(element);
// Define a function to run whenever the Cornerstone viewport is rendered
// (e.g. following a change of window or zoom)
function onImageRendered(e, eventData) {
log.info('imageViewerViewport onImageRendered');
// Use Session to trigger reactive updates in the viewportOverlay helper functions
// This lets the viewport overlay always display correct window / zoom values
Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
// Save the current viewport into the ViewerData global variable, as well as the
// Meteor Session. This lets the viewport be saved/reloaded on a hot-code reload
var viewport = cornerstone.getViewport(element);
ViewerData[contentId].viewer.imageViewerLoadedSeriesDictionary[viewportIndex].viewport = viewport;
ViewerData[contentId].loadedSeriesData[viewportIndex].viewport = viewport;
Session.set('ViewerData', ViewerData);
}
// Attach the onImageRendered callback to the CornerstoneImageRendered event
$(element).off('CornerstoneImageRendered', onImageRendered);
$(element).on('CornerstoneImageRendered', onImageRendered);
// Set a random value for the Session variable in order to trigger an overlay update
Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
// Define a function to run whenever the Cornerstone viewport changes images
// (e.g. during scrolling)
function onNewImage(e, eventData) {
log.info('imageViewerViewport onNewImage');
// Update the templateData with the new imageId
// This allows the template helpers to update reactively
templateData.imageId = eventData.enabledElement.image.imageId;
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
// 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');
if (stack && stack.data.length && stack.data[0].imageIds.length > 1) {
var imageIdIndex = stack.data[0].imageIds.indexOf(templateData.imageId);
ViewerData[contentId].viewer.imageViewerLoadedSeriesDictionary[viewportIndex].currentImageIdIndex = imageIdIndex;
ViewerData[contentId].loadedSeriesData[viewportIndex].currentImageIdIndex = imageIdIndex;
Session.set('ViewerData', ViewerData);
}
}
// Attach the onNewImage callback to the CornerstoneNewImage event
$(element).off('CornerstoneNewImage', onNewImage);
$(element).on('CornerstoneNewImage', onNewImage);
// Set a random value for the Session variable in order to trigger an overlay update
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
// 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
function sendActivationTrigger(e, eventData) {
var activeViewportIndex = data.activeViewport.curValue;
log.info('imageViewerViewport sendActivationTrigger');
// Check if the current active viewport in the Meteor Session
// Is the same as the viewport in which the activation event was fired.
// If it was, no changes are necessary, so stop here.
var activeViewportIndex = Session.get('activeViewport');
var viewportIndex = $(".imageViewerViewport").index(eventData.element);
if (viewportIndex === activeViewportIndex) {
return;
}
// Otherwise, trigger an 'ActivateViewport' event to be handled by the Template event
// handler
eventData.viewportIndex = viewportIndex;
var customEvent = jQuery.Event('ActivateViewport', eventData);
customEvent.type = 'ActivateViewport'; // Need to overwrite the type set in the touch tools
var customEvent = $.Event('ActivateViewport', eventData);
// Need to overwrite the type set in the original event
customEvent.type = 'ActivateViewport';
$(e.target).trigger(customEvent, eventData);
}
$(element).off(allEvents, sendActivationTrigger);
$(element).on(allEvents, sendActivationTrigger);
// Attach the sendActivationTrigger function to all of the Cornerstone interaction events
$(element).off(allCornerstoneEvents, sendActivationTrigger);
$(element).on(allCornerstoneEvents, sendActivationTrigger);
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
OHIF.viewer.imageViewerLoadedSeriesDictionary[viewportIndex] = {
// Store the current series data inside a global variable
OHIF.viewer.loadedSeriesData[viewportIndex] = {
studyInstanceUid: data.studyInstanceUid,
seriesInstanceUid: data.seriesInstanceUid,
currentImageIdIndex: data.currentImageIdIndex,
viewport: viewport
};
ViewerData[contentId].loadedSeriesData = OHIF.viewer.loadedSeriesData;
// Check if image plane (orientation / loction) data is present for the current image
var imagePlane = cornerstoneTools.metaData.get('imagePlane', image.imageId);
// If it is, and reference lines are enabled, add this element to the global synchronizer
// that is used for updating reference lines, and enable reference lines for this viewport.
if (OHIF.viewer.refLinesEnabled && imagePlane && imagePlane.frameOfReferenceUID) {
OHIF.viewer.updateImageSynchronizer.add(element);
displayReferenceLines(viewportIndex);
displayReferenceLines(element);
}
}, function(error) {
if (errorLoadingHandler) {
errorLoadingHandler(element, imageId, error);
if (viewportIndex === Session.get('activeViewport')) {
enablePrefetchOnElement(element);
}
}, function(error) {
// If something goes wrong while loading the image, fire the error handler.
errorLoadingHandler(element, imageId, error);
});
}
/**
* This function searches an object to return the keys that contain a specific value
*
* @param object {object} The object to be searched
* @param value The value to be found
*
* @returns {array} The keys for which the object has the specified value
*/
function getKeysByValue(object, value) {
// http://stackoverflow.com/questions/9907419/javascript-object-get-key-by-value
return Object.keys(object).filter(function(key) {
return object[key] === value;
});
}
Meteor.startup(function() {
// On Meteor startup, define the global objects used to store loading imageIds
// by viewport / thumbnail element
ViewportLoading = {};
ThumbnailLoading = {};
// Whenever the CornerstoneImageLoadProgress is fired, identify which viewports
// the "in-progress" image is to be displayed in. Then pass the percent complete
// via the Meteor Session to the other templates to be displayed in the relevant viewports.
$(cornerstone).on('CornerstoneImageLoadProgress', function(e, eventData) {
viewportIndices = getKeysByValue(ViewportLoading, eventData.imageId);
viewportIndices.forEach(function(viewportIndex) {
Session.set('CornerstoneLoadProgress' + viewportIndex, eventData.percentComplete);
});
thumbnailIndices = getKeysByValue(ThumbnailLoading, eventData.imageId);
thumbnailIndices.forEach(function(thumbnailIndex) {
Session.set('CornerstoneThumbnailLoadProgress' + thumbnailIndex, eventData.percentComplete);
});
});
var config = {
magnifySize: 300,
magnificationLevel: 3
};
cornerstoneTools.magnify.setConfiguration(config);
});
Template.imageViewerViewport.onRendered(function() {
var studies = this.data.studies;
log.info("imageViewerViewport onRendered");
// When the imageViewerViewport template is rendered
var element = this.find(".imageViewerViewport");
// Display the loading indicator for this element
$(element).siblings('.imageViewerLoadingIndicator').css('display', 'block');
// Get the current active viewport index, if this viewport has the same index,
// add the CSS 'active' class to highlight this viewport.
var activeViewport = Session.get('activeViewport');
if (activeViewport === this.data.viewportIndex) {
$('#imageViewerViewports .viewportContainer').removeClass('active');
$(element).parents('.viewportContainer').addClass('active');
}
// Create a data object to pass to the series loading function (loadSeriesIntoViewport)
var data = {
element: element,
viewport: this.data.viewport,
currentImageIdIndex: this.data.currentImageIdIndex,
activeViewport: this.data.activeViewport,
studyInstanceUid: this.data.studyInstanceUid,
seriesInstanceUid: this.data.seriesInstanceUid
seriesInstanceUid: this.data.seriesInstanceUid,
activeViewport: activeViewport
};
// If no seriesInstanceUid or studyInstanceUid were supplied, display the drag/drop
// instructions and then stop here since we don't know what to display in the viewport.
if (this.data.seriesInstanceUid === undefined || this.data.studyInstanceUid === undefined) {
element.classList.add('empty');
$(element).siblings('.imageViewerLoadingIndicator').css('display', 'none');
$(element).siblings('.viewportInstructions').show();
return;
}
// Retrieve the current set of studies from the Meteor Session
var studies = Session.get('studies');
// Look through every study and their series' until we find the
// series that matches the seriesInstanceUid and studyInstanceUid
var studyInstanceUid = this.data.studyInstanceUid;
var seriesInstanceUid = this.data.seriesInstanceUid;
studies.every(function(study) {
if (study.studyInstanceUid === studyInstanceUid) {
data.study = study;
@ -242,22 +421,51 @@ Template.imageViewerViewport.onRendered(function() {
return true;
});
// If we didn't find anything, stop here
if (!data.study || !data.series) {
return;
}
// Otherwise, load pass the data object into loadSeriesIntoViewport
loadSeriesIntoViewport(data);
});
Template.imageViewerViewport.onDestroyed(function() {
log.info("imageViewerViewport onDestroyed");
// When a viewport element is being destroyed
var element = this.find(".imageViewerViewport");
// Try to stop any currently playing clips
// Otherwise the interval will continuously throw errors
try {
cornerstoneTools.stopClip(element);
} catch(error) {
log.warn(error);
}
// Disable the viewport element with Cornerstone
// This also triggers the removal of the element from all available
// synchronizers, such as the one used for reference lines.
cornerstone.disable(element);
});
Template.imageViewerViewport.events({
'ActivateViewport .imageViewerViewport': function(e) {
if (this.viewportIndex === this.activeViewport.curValue) {
return;
}
console.log('ActivateViewport index: ' + this.viewportIndex);
this.activeViewport.curValue = this.viewportIndex;
enablePrefetchOnElement(this.viewportIndex);
displayReferenceLines(this.viewportIndex);
},
log.info("imageViewerViewport ActivateViewport");
// When an ActivateViewport event is fired, update the Meteor Session
// with the viewport index that it was fired from.
Session.set("activeViewport", e.viewportIndex);
// Add the 'active' class to the parent container to highlight the active viewport
$('#imageViewerViewports .viewportContainer').removeClass('active');
$(e.currentTarget).parents('.viewportContainer').addClass('active');
// Finally, enable stack prefetching and hide the reference lines from
// the newly activated viewport
var element = e.currentTarget;
enablePrefetchOnElement(element);
displayReferenceLines(element);
}
});

View File

@ -20,4 +20,22 @@
right: 0
left: 0
margin: auto
height: 20px
height: 20px
/* These are some good CSS settings for a circular magnifying glass
Try removing the border-radius to make a square magnifying glass */
.magnifyTool{
border: 4px white solid;
box-shadow: 2px 2px 10px #1e1e1e;
border-radius: 50%;
display: none;
cursor: none;
}
/* prevents selection when left click dragging */
.disable-selection {
-moz-user-select: none; -webkit-user-select: none; -ms-user-select:none; user-select:none;
}
/* prevents cursor from changing to the i bar on the overlays*/
.noIbar {
cursor:default;
}

View File

@ -3,7 +3,7 @@
{{ #each viewportArray }}
<div class="viewportContainer" style="height:{{height}}%;width:{{width}}%;">
<div class="removable">
{{ >imageViewerViewport viewportIndex=viewportIndex studies=studies activeViewport=activeViewport seriesInstanceUid=seriesInstanceUid studyInstanceUid=studyInstanceUid currentImageIdIndex=currentImageIdIndex viewport=viewport}}
{{ >imageViewerViewport viewportIndex=viewportIndex seriesInstanceUid=seriesInstanceUid studyInstanceUid=studyInstanceUid currentImageIdIndex=currentImageIdIndex viewport=viewport}}
</div>
</div>
{{ /each }}

View File

@ -1,49 +1,55 @@
Template.imageViewerViewports.onRendered(function() {
this.autorun(function() {
var studies = Session.get('studies');
var eventType = "ViewerBaseStudyLoaded";
var eventData = {
studies: studies
};
$(document).trigger(eventType, eventData);
});
});
Template.imageViewerViewports.helpers({
height: function() {
var viewportRows;
if (!Template.parentData(1).viewportRows) {
viewportRows = 1;
} else {
viewportRows = Template.parentData(1).viewportRows.curValue; //Having issues with .get(), not sure why?
}
var viewportRows = this.viewportRows || 1;
return 100 / viewportRows;
},
width: function() {
var viewportColumns;
if (!Template.parentData(1).viewportColumns) {
viewportColumns = 1;
} else {
viewportColumns = Template.parentData(1).viewportColumns.curValue; //Having issues with .get(), not sure why?
}
var viewportColumns = this.viewportColumns || 1;
return 100 / viewportColumns;
},
viewportArray: function() {
// This is a really annoying thing to have to do, but Meteor
// doesn't want to let me use another type of helper.
var viewportRows;
if (!this.viewportRows) {
viewportRows = 1;
} else {
viewportRows = this.viewportRows.curValue; //Having issues with .get(), not sure why?
log.info("imageViewerViewports viewportArray");
var studies = Session.get('studies');
var viewportRows = this.viewportRows || 1;
var viewportColumns = this.viewportColumns || 1;
var contentId = this.contentId || $("#viewer").parents(".tab-pane.active").attr('id');
if (this.viewportRows && this.viewportColumns) {
viewportRows = this.viewportRows || 1;
viewportColumns = this.viewportColumns || 1;
} else if (ViewerData[contentId].viewportRows && ViewerData[contentId].viewportColumns) {
viewportRows = ViewerData[contentId].viewportRows;
viewportColumns = ViewerData[contentId].viewportColumns;
}
var viewportColumns;
if (!this.viewportColumns) {
viewportColumns = 1;
} else {
viewportColumns = this.viewportColumns.curValue; //Having issues with .get(), not sure why?
}
// Update viewerData
ViewerData[contentId].viewportRows = viewportRows;
ViewerData[contentId].viewportColumns = viewportColumns;
Session.set("ViewerData", ViewerData);
var viewportData;
if (OHIF && OHIF.viewer && !$.isEmptyObject(OHIF.viewer.imageViewerLoadedSeriesDictionary)) {
viewportData = OHIF.viewer.imageViewerLoadedSeriesDictionary;
if (!$.isEmptyObject(ViewerData[contentId].loadedSeriesData)) {
viewportData = ViewerData[contentId].loadedSeriesData;
}
var hangingProtocol = getHangingProtocol();
var inputData = {
viewportColumns: viewportColumns,
viewportRows: viewportRows,
studies: this.studies
studies: studies
};
var hangingProtocolViewportData = hangingProtocol(inputData);
@ -52,8 +58,10 @@ Template.imageViewerViewports.helpers({
for (var i=0; i < numViewports; ++i) {
var data = {
viewportIndex: i,
studies: this.studies,
activeViewport: this.activeViewport
// These two are necessary because otherwise the width and height helpers
// don't get the right data context. Seems to be related to the "each" loop.
viewportColumns: viewportColumns,
viewportRows: viewportRows
};
if (viewportData && viewportData[i]) {
data.seriesInstanceUid = viewportData[i].seriesInstanceUid;
@ -70,4 +78,75 @@ Template.imageViewerViewports.helpers({
}
return array;
},
});
var savedSeriesData,
savedViewportRows,
savedViewportColumns;
Template.imageViewerViewports.events({
'dblclick .imageViewerViewport': function(e) {
var container = $(".viewerMain").get(0);
var data;
var contentId = this.contentId || $("#viewer").parents(".tab-pane.active").attr('id');
// If there is more than one viewport on screen
// And one of them is double-clicked, it should be rendered alone
// If it is double-clicked again, the viewer should revert to the previous layout
if ($(e.currentTarget).hasClass('zoomed')) {
// Revert to saved settings
ViewerData[contentId].loadedSeriesData = $.extend(true, {}, savedSeriesData);
ViewerData[contentId].viewportRows = savedViewportRows;
ViewerData[contentId].viewportColumns = savedViewportColumns;
savedViewportRows = 0;
savedViewportColumns = 0;
data = {
viewportRows: ViewerData[contentId].viewportRows,
viewportColumns: ViewerData[contentId].viewportColumns,
};
// Render the imageViewerViewports template with these settings
$('#imageViewerViewports').remove();
UI.renderWithData(Template.imageViewerViewports, data, container);
// Remove the 'zoomed' class from any viewports
$('.imageViewerViewport').removeClass('zoomed');
} else {
// Zoom to single viewport
// If only one viewport is on-screen, stop here
if (ViewerData[contentId].viewportRows === 1 &&
ViewerData[contentId].viewportColumns === 1) {
return;
}
// Save the current settings
savedSeriesData = $.extend(true, {}, ViewerData[contentId].loadedSeriesData);
savedViewportRows = ViewerData[contentId].viewportRows;
savedViewportColumns = ViewerData[contentId].viewportColumns;
// Get the clicked-on viewport's index
var viewportIndex = this.viewportIndex;
// Set the first viewport's data to be the same as the currently clicked-on viewport
ViewerData[contentId].loadedSeriesData[0] = ViewerData[contentId].loadedSeriesData[viewportIndex];
// Set the basic template data
data = {
viewportRows: 1,
viewportColumns: 1,
};
// Render the imageViewerViewports template with these settings
$('#imageViewerViewports').remove();
UI.renderWithData(Template.imageViewerViewports, data, container);
// Add the 'zoomed' class to the lone remaining viewport
$('.imageViewerViewport').eq(0).addClass('zoomed');
}
log.info('dblclick');
}
});

View File

@ -6,6 +6,12 @@
float: left
border: 2px solid #777
&.active, &:hover.active
border: 2px solid #9A6617
&:hover
border: 2px solid white
.removable
width: 100%
height: 100%

View File

@ -1,3 +1,10 @@
/**
* Adds the 'hover' class to cells above and to the left of the current cell
* This is used to "fill in" the grid that the user will change the layout to,
* if they click on a specific table cell.
*
* @param currentCell
*/
function highlightCells(currentCell) {
var cells = $('.layoutChooser table td');
cells.removeClass('hover');
@ -27,31 +34,18 @@ Template.layoutChooser.events({
},
'click .layoutChooser table td': function(evt) {
$('#imageViewerViewports').remove();
var container = $(".viewerMain").get(0);
var currentCell = $(evt.currentTarget);
var rowIndex = currentCell.closest('tr').index();
var columnIndex = currentCell.index();
var data = {};
// Add 1 because the indices start from zero
if (this.viewportRows) {
this.viewportRows.curValue = rowIndex + 1;
data.viewportRows = this.viewportRows;
} else {
data.viewportRows = 1;
}
if (this.viewportColumns) {
this.viewportColumns.curValue = columnIndex + 1;
data.viewportColumns = this.viewportColumns;
} else {
data.viewportColumns = 1;
}
data.studies = Template.parentData(2).studies;
data.activeViewport = Template.parentData(2).activeViewport;
var data = {
viewportRows: rowIndex + 1,
viewportColumns: columnIndex + 1,
};
var container = $(".viewerMain").get(0);
UI.renderWithData(Template.imageViewerViewports, data, container);
}
});

View File

@ -1,7 +1,7 @@
<template name="loadingIndicator">
<div class="imageViewerLoadingIndicator loadingIndicator">
<div class="indicatorContents">
<p>Loading...</p>
<p>Loading... {{percentComplete}}</p>
</div>
</div>
<div class="imageViewerErrorLoadingIndicator loadingIndicator">

View File

@ -58,4 +58,13 @@ errorLoadingHandler = function(element, imageId, error, source) {
if (error) {
errorLoadingIndicator.find('.details').text("Details: " + error);
}
};
};
Template.loadingIndicator.helpers({
'percentComplete': function(e) {
var percentComplete = Session.get('CornerstoneLoadProgress' + this.viewportIndex);
if (percentComplete && percentComplete !== 100) {
return percentComplete + '%';
}
}
});

View File

@ -1,7 +1,7 @@
Template.playClipButton.helpers({
'isPlaying': function() {
Session.get('UpdateCINE');
var viewportIndex = this.activeViewport.curValue;
return !!OHIF.viewer.isPlaying[viewportIndex];
var activeViewport = Session.get('activeViewport');
return !!OHIF.viewer.isPlaying[activeViewport];
}
});

View File

@ -6,11 +6,11 @@
{{ /each }}
{{ #if includePlayClipButton }}
{{ >playClipButton activeViewport=activeViewport}}
{{ >playClipButton }}
{{ /if }}
{{ #if includeLayoutButton }}
{{ >layoutButton viewportRows=viewportRows viewportColumns=viewportColumns}}
{{ >layoutButton }}
{{ /if }}
</div>
</div>

View File

@ -8,6 +8,27 @@ function getDefaultButtonData() {
iconClasses: 'fa fa-sun-o'
});
buttonData.push({
id: 'wwwcRegion',
title: 'Window by Region',
classes: 'imageViewerTool',
iconClasses: 'fa fa-square'
});
buttonData.push({
id: 'magnify',
title: 'Magnify',
classes: 'imageViewerTool',
iconClasses: 'fa fa-circle'
});
buttonData.push({
id: 'annotate',
title: 'Annotation',
classes: 'imageViewerTool',
iconClasses: 'fa fa-arrows-h'
});
buttonData.push({
id: 'invert',
title: 'Invert',
@ -70,6 +91,20 @@ function getDefaultButtonData() {
classes: 'imageViewerTool',
iconClasses: 'fa fa-square-o'
});
buttonData.push({
id: 'resetViewport',
title: 'Reset Viewport',
classes: 'imageViewerCommand',
iconClasses: 'fa fa-undo'
});
buttonData.push({
id: 'clearTools',
title: 'Clear tools',
classes: 'imageViewerCommand',
iconClasses: 'fa fa-trash'
});
return buttonData;
}
@ -84,8 +119,8 @@ Template.toolbar.events({
if (!OHIF.viewer.functionList.hasOwnProperty(command)) {
return;
}
var viewportIndex = Template.parentData(0).activeViewport.get();
var element = $('.imageViewerViewport').get(viewportIndex);
var activeViewport = Session.get('activeViewport');
var element = $('.imageViewerViewport').get(activeViewport);
OHIF.viewer.functionList[command](element);
}
});

View File

@ -10,6 +10,7 @@
color: #ffffff
background-color: #424242
border-color: #424242
outline: none
&.active, &:active
background-color: #3B678E

View File

@ -1,5 +1,13 @@
updateOrientationMarkers = function(element, viewport) {
// Updates the orientation labels on the viewport
/**
* Updates the orientation labels on a Cornerstone-enabled Viewport element
* when the viewport settings change (e.g. when a horizontal flip or a rotation occurs)
*
* @param element The DOM element of the Cornerstone viewport
*/
updateOrientationMarkers = function(element) {
// Get the current viewport settings
var viewport = cornerstone.getViewport(element);
var enabledElement = cornerstone.getEnabledElement(element);
var imagePlane = cornerstoneTools.metaData.get('imagePlane', enabledElement.image.imageId);

View File

@ -1,13 +1,13 @@
<template name="viewportOverlay">
<div class="imageViewerViewportOverlay noselect">
<div class="topleft dicomTag">
<div>{{patientName}}</div>
<div>{{formatPN patientName}}</div>
<div>{{patientId}}</div>
<div></div>
</div>
<div class="topright dicomTag">
<div>{{studyDescription}}</div>
<div>{{studyDate}} {{studyTime}}</div>
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
</div>
<div class="bottomright dicomTag">
<div>Zoom: {{formatNumberPrecision zoom 2}}%</div>

View File

@ -1,7 +1,22 @@
/**
* Creates a set of series to be placed in the Study Browser
* The series that appear in the Study Browser must represent
* imaging modalities.
*
* Furthermore, for drag/drop functionality,
* it is easiest if the stack objects also contain information about
* which study they are linked to.
*
* @param study The study instance to be used
* @returns {Array} An array of series to be placed in the Study Browser
*/
createStacks = function(study) {
// Define an empty array of stacks
var stacks = [];
// TODO: Split by multi-frame, modality, image size, etc
study.seriesList.forEach(function(series) {
// If the series has no instanced, skip it
if (!series.instances) {
return;
}
@ -12,6 +27,11 @@ createStacks = function(study) {
if (!anInstance || !anInstance.rows) {
return;
}
// Include the study instance Uid for drag/drop purposes
series.studyInstanceUid = study.studyInstanceUid;
// Add this series to the list of stacks
stacks.push(series);
});
return stacks;

Some files were not shown because too many files have changed in this diff Show More