Merge branch 'master' of https://github.com/OHIF/Viewers
This commit is contained in:
commit
4f1fdfab3e
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
.idea/
|
||||
docs/
|
||||
.meteor/local
|
||||
.meteor/meteorite
|
||||
node_modules
|
||||
@ -34,4 +34,7 @@ stylus
|
||||
fortawesome:fontawesome
|
||||
random
|
||||
reactive-var
|
||||
reactive-dict
|
||||
lesiontracker
|
||||
practicalmeteor:loglevel
|
||||
momentjs:moment
|
||||
|
||||
@ -1 +1 @@
|
||||
METEOR@1.2.0.2
|
||||
METEOR@1.2.1
|
||||
|
||||
@ -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
|
||||
|
||||
2
LesionTracker/bin/localhostOrthanc.bat
Normal file
2
LesionTracker/bin/localhostOrthanc.bat
Normal file
@ -0,0 +1,2 @@
|
||||
PACKAGE_DIRS=..\Packages
|
||||
meteor --settings ../config/localhostOrthanc.json
|
||||
2
LesionTracker/bin/medicalConnections.bat
Normal file
2
LesionTracker/bin/medicalConnections.bat
Normal file
@ -0,0 +1,2 @@
|
||||
set PACKAGE_DIRS=..\Packages
|
||||
meteor --settings ../config/medicalConnections.json
|
||||
2
LesionTracker/bin/medkenOrthanc.bat
Normal file
2
LesionTracker/bin/medkenOrthanc.bat
Normal file
@ -0,0 +1,2 @@
|
||||
set PACKAGE_DIRS=..\Packages
|
||||
meteor --settings ../config/medkenOrthanc.json
|
||||
2
LesionTracker/bin/siimDCM4CHEE.bat
Normal file
2
LesionTracker/bin/siimDCM4CHEE.bat
Normal file
@ -0,0 +1,2 @@
|
||||
set PACKAGE_DIRS=..\Packages
|
||||
meteor --settings ../config/siimDCM4CHEE.json
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -3,4 +3,9 @@ Meteor.startup(function() {
|
||||
mouse: cornerstoneTools.lesion,
|
||||
touch: cornerstoneTools.lesionTouch
|
||||
});
|
||||
|
||||
toolManager.addTool('nonTarget', {
|
||||
mouse: cornerstoneTools.nonTarget,
|
||||
touch: cornerstoneTools.nonTargetTouch
|
||||
});
|
||||
});
|
||||
@ -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>
|
||||
@ -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
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
.viewer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
#upperHalf {
|
||||
height: 75%;
|
||||
width: 100%;
|
||||
}
|
||||
@ -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>
|
||||
@ -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();
|
||||
});
|
||||
|
||||
|
||||
29
LesionTracker/client/components/viewer.styl
Normal file
29
LesionTracker/client/components/viewer.styl
Normal 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
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
@ -1,4 +1,3 @@
|
||||
.viewerMain
|
||||
width: calc(100% - 120px)
|
||||
height: 100%
|
||||
float: left
|
||||
width: 100%
|
||||
height: 84%
|
||||
@ -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">
|
||||
|
||||
3
LesionTracker/client/log.js
Normal file
3
LesionTracker/client/log.js
Normal file
@ -0,0 +1,3 @@
|
||||
// Create application logger using loglevel
|
||||
// https://atmospherejs.com/spacejamio/loglevel
|
||||
log = loglevel.createAppLogger('LesionTracker', defaultLevel = 'info');
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
44
LesionTracker/client/routes.js
Normal file
44
LesionTracker/client/routes.js
Normal 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
13
Makefile
Normal 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
|
||||
1
OHIFViewer/.gitignore
vendored
1
OHIFViewer/.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
.idea
|
||||
.meteor/local
|
||||
.meteor/meteorite
|
||||
node_modules
|
||||
@ -33,3 +33,4 @@ dicomweb
|
||||
viewerbase
|
||||
worklist
|
||||
reactive-var
|
||||
practicalmeteor:loglevel
|
||||
|
||||
@ -1 +1 @@
|
||||
METEOR@1.2.0.2
|
||||
METEOR@1.2.1
|
||||
|
||||
@ -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
|
||||
|
||||
2
OHIFViewer/bin/localhostOrthanc.bat
Normal file
2
OHIFViewer/bin/localhostOrthanc.bat
Normal file
@ -0,0 +1,2 @@
|
||||
set PACKAGE_DIRS=..\Packages
|
||||
meteor --settings ../config/localhostOrthanc.json
|
||||
2
OHIFViewer/bin/medicalConnections.bat
Normal file
2
OHIFViewer/bin/medicalConnections.bat
Normal file
@ -0,0 +1,2 @@
|
||||
set PACKAGE_DIRS=..\Packages
|
||||
meteor --settings ../config/medicalConnections.json
|
||||
2
OHIFViewer/bin/medkenOrthanc.bat
Normal file
2
OHIFViewer/bin/medkenOrthanc.bat
Normal file
@ -0,0 +1,2 @@
|
||||
set PACKAGE_DIRS=..\Packages
|
||||
meteor --settings ../config/medkenOrthanc.json
|
||||
2
OHIFViewer/bin/siimDCM4CHEE.bat
Normal file
2
OHIFViewer/bin/siimDCM4CHEE.bat
Normal file
@ -0,0 +1,2 @@
|
||||
set PACKAGE_DIRS=..\Packages
|
||||
meteor --settings ../config/siimDCM4CHEE.json
|
||||
@ -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>
|
||||
@ -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();
|
||||
});
|
||||
|
||||
|
||||
@ -12,4 +12,10 @@
|
||||
user-select: none
|
||||
|
||||
.studyBrowser
|
||||
width: auto
|
||||
width: auto
|
||||
|
||||
.viewerMain
|
||||
float: left
|
||||
background-color: gray
|
||||
height: 100%
|
||||
width: calc(100% - 120px)
|
||||
@ -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>
|
||||
@ -1,5 +0,0 @@
|
||||
Template.viewerMain.helpers({
|
||||
studies : function() {
|
||||
return this.studies;
|
||||
}
|
||||
});
|
||||
@ -1,5 +0,0 @@
|
||||
.viewerMain
|
||||
float: left
|
||||
background-color: gray
|
||||
height: 100%
|
||||
width: calc(100% - 120px)
|
||||
@ -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
3
OHIFViewer/client/log.js
Normal file
@ -0,0 +1,3 @@
|
||||
// Create application logger using loglevel
|
||||
// https://atmospherejs.com/spacejamio/loglevel
|
||||
log = loglevel.createAppLogger('OHIFViewer', defaultLevel = 'info');
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
@ -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
@ -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
|
||||
|
||||
49
Packages/dicomweb/lib/findIndexOfString.js
Normal file
49
Packages/dicomweb/lib/findIndexOfString.js
Normal 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;
|
||||
};
|
||||
17
Packages/dicomweb/lib/uint8ArrayToString.js
Normal file
17
Packages/dicomweb/lib/uint8ArrayToString.js
Normal 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;
|
||||
};
|
||||
@ -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');
|
||||
});
|
||||
|
||||
|
||||
@ -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));
|
||||
148
Packages/lesiontracker/compatibility/measurementManagerDAL.js
Normal file
148
Packages/lesiontracker/compatibility/measurementManagerDAL.js
Normal 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
|
||||
};
|
||||
})();
|
||||
490
Packages/lesiontracker/compatibility/nonTargetTool.js
Normal file
490
Packages/lesiontracker/compatibility/nonTargetTool.js
Normal 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);
|
||||
@ -1,3 +0,0 @@
|
||||
<template name="lesionDialog">
|
||||
{{> lesionLocationDialog}}
|
||||
</template>
|
||||
@ -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);
|
||||
}
|
||||
});*/
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});*/
|
||||
});
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
<template name="lesionTableTimepointCell">
|
||||
<td>{{longestDiameter}}</td>
|
||||
<td class="lesionTableTimepointCell">{{longestDiameter}}</td>
|
||||
</template>
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
@ -1,3 +1,3 @@
|
||||
<template name="lesionTableTimepointHeader">
|
||||
<th>{{name}}</th>
|
||||
<th>{{timepointName}}</th>
|
||||
</template>
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
250
Packages/lesiontracker/lib/uuid.js
Executable file
250
Packages/lesiontracker/lib/uuid.js
Executable 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);
|
||||
@ -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');
|
||||
|
||||
});
|
||||
@ -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>
|
||||
@ -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);
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
<template name="loadingText">
|
||||
<div class="loadingTextDiv">
|
||||
<h5>Loading... <i class="fa fa-spinner fa-pulse"></i></h5>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,5 @@
|
||||
.loadingTextDiv
|
||||
margin: 30px
|
||||
|
||||
h5
|
||||
text-align: center
|
||||
@ -0,0 +1,7 @@
|
||||
<template name="removableBackdrop">
|
||||
<div class="removableBackdrop noselect"
|
||||
oncontextmenu='return false;'
|
||||
unselectable='on'
|
||||
onselectstart='return false;'>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,9 @@
|
||||
.removableBackdrop
|
||||
position: fixed
|
||||
top: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
z-index: 50
|
||||
background-color: transparent
|
||||
|
||||
@ -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>
|
||||
@ -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 + '%';
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
@ -6,4 +6,4 @@
|
||||
overflow-y: auto
|
||||
overflow-x: hidden
|
||||
background-color: black
|
||||
padding-bottom: 20px
|
||||
padding-bottom: 20px
|
||||
|
||||
@ -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 || '';
|
||||
}
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
@ -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 }}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
});
|
||||
@ -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%
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
@ -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">
|
||||
|
||||
@ -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 + '%';
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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];
|
||||
}
|
||||
});
|
||||
@ -6,11 +6,11 @@
|
||||
{{ /each }}
|
||||
|
||||
{{ #if includePlayClipButton }}
|
||||
{{ >playClipButton activeViewport=activeViewport}}
|
||||
{{ >playClipButton }}
|
||||
{{ /if }}
|
||||
|
||||
{{ #if includeLayoutButton }}
|
||||
{{ >layoutButton viewportRows=viewportRows viewportColumns=viewportColumns}}
|
||||
{{ >layoutButton }}
|
||||
{{ /if }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
color: #ffffff
|
||||
background-color: #424242
|
||||
border-color: #424242
|
||||
outline: none
|
||||
|
||||
&.active, &:active
|
||||
background-color: #3B678E
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user