diff --git a/.gitignore b/.gitignore index b0746064f..efc739489 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.idea/ +docs/ .meteor/local .meteor/meteorite node_modules \ No newline at end of file diff --git a/LesionTracker/.meteor/packages b/LesionTracker/.meteor/packages index 808176914..a236d1a0f 100644 --- a/LesionTracker/.meteor/packages +++ b/LesionTracker/.meteor/packages @@ -34,4 +34,7 @@ stylus fortawesome:fontawesome random reactive-var +reactive-dict lesiontracker +practicalmeteor:loglevel +momentjs:moment diff --git a/LesionTracker/.meteor/release b/LesionTracker/.meteor/release index 5684262a8..3a05e0a2f 100644 --- a/LesionTracker/.meteor/release +++ b/LesionTracker/.meteor/release @@ -1 +1 @@ -METEOR@1.2.0.2 +METEOR@1.2.1 diff --git a/LesionTracker/.meteor/versions b/LesionTracker/.meteor/versions index d46560543..80add3239 100644 --- a/LesionTracker/.meteor/versions +++ b/LesionTracker/.meteor/versions @@ -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 diff --git a/LesionTracker/bin/localhostOrthanc.bat b/LesionTracker/bin/localhostOrthanc.bat new file mode 100644 index 000000000..fb565e0bb --- /dev/null +++ b/LesionTracker/bin/localhostOrthanc.bat @@ -0,0 +1,2 @@ +PACKAGE_DIRS=..\Packages +meteor --settings ../config/localhostOrthanc.json \ No newline at end of file diff --git a/LesionTracker/bin/medicalConnections.bat b/LesionTracker/bin/medicalConnections.bat new file mode 100644 index 000000000..bf0ca7e9f --- /dev/null +++ b/LesionTracker/bin/medicalConnections.bat @@ -0,0 +1,2 @@ +set PACKAGE_DIRS=..\Packages +meteor --settings ../config/medicalConnections.json diff --git a/LesionTracker/bin/medkenOrthanc.bat b/LesionTracker/bin/medkenOrthanc.bat new file mode 100644 index 000000000..5831ac369 --- /dev/null +++ b/LesionTracker/bin/medkenOrthanc.bat @@ -0,0 +1,2 @@ +set PACKAGE_DIRS=..\Packages +meteor --settings ../config/medkenOrthanc.json diff --git a/LesionTracker/bin/siimDCM4CHEE.bat b/LesionTracker/bin/siimDCM4CHEE.bat new file mode 100644 index 000000000..b63adf424 --- /dev/null +++ b/LesionTracker/bin/siimDCM4CHEE.bat @@ -0,0 +1,2 @@ +set PACKAGE_DIRS=..\Packages +meteor --settings ../config/siimDCM4CHEE.json diff --git a/LesionTracker/client/body.styl b/LesionTracker/client/body.styl index 498ae65e3..0a8bb4e1e 100644 --- a/LesionTracker/client/body.styl +++ b/LesionTracker/client/body.styl @@ -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 +} + diff --git a/LesionTracker/client/components/init.js b/LesionTracker/client/components/init.js index a81e40987..fc816f8a7 100644 --- a/LesionTracker/client/components/init.js +++ b/LesionTracker/client/components/init.js @@ -3,4 +3,9 @@ Meteor.startup(function() { mouse: cornerstoneTools.lesion, touch: cornerstoneTools.lesionTouch }); + + toolManager.addTool('nonTarget', { + mouse: cornerstoneTools.nonTarget, + touch: cornerstoneTools.nonTargetTouch + }); }); \ No newline at end of file diff --git a/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.html b/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.html index 08ff6cf46..a7116b58c 100644 --- a/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.html +++ b/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.html @@ -1,16 +1,9 @@ \ No newline at end of file diff --git a/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.styl b/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.styl index 2629a51f8..d50323ea2 100644 --- a/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.styl +++ b/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.styl @@ -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 \ No newline at end of file + img + height: 30px + float: left + margin-right: 10px + margin-top: 5px; + + h4.name + float: left + margin: 10px 0; + font-family: "Sanchez" + color: #C1C1C1 diff --git a/LesionTracker/client/components/viewer.css b/LesionTracker/client/components/viewer.css deleted file mode 100644 index 639327462..000000000 --- a/LesionTracker/client/components/viewer.css +++ /dev/null @@ -1,12 +0,0 @@ -.viewer { - width: 100%; - height: 100%; - margin: 0; - padding: 0; - background-color: black; -} - -#upperHalf { - height: 75%; - width: 100%; -} \ No newline at end of file diff --git a/LesionTracker/client/components/viewer.html b/LesionTracker/client/components/viewer.html index 8530698dc..92111a314 100644 --- a/LesionTracker/client/components/viewer.html +++ b/LesionTracker/client/components/viewer.html @@ -1,10 +1,12 @@ \ No newline at end of file diff --git a/LesionTracker/client/components/viewer.js b/LesionTracker/client/components/viewer.js index f72198aa7..41b2c44ba 100644 --- a/LesionTracker/client/components/viewer.js +++ b/LesionTracker/client/components/viewer.js @@ -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(); }); diff --git a/LesionTracker/client/components/viewer.styl b/LesionTracker/client/components/viewer.styl new file mode 100644 index 000000000..a2beb0524 --- /dev/null +++ b/LesionTracker/client/components/viewer.styl @@ -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 diff --git a/LesionTracker/client/components/viewerMain/viewerMain.html b/LesionTracker/client/components/viewerMain/viewerMain.html index e7bdf403f..6bf8bf680 100644 --- a/LesionTracker/client/components/viewerMain/viewerMain.html +++ b/LesionTracker/client/components/viewerMain/viewerMain.html @@ -1,6 +1,6 @@ \ No newline at end of file diff --git a/LesionTracker/client/components/viewerMain/viewerMain.js b/LesionTracker/client/components/viewerMain/viewerMain.js index 2062952f5..5da16623e 100644 --- a/LesionTracker/client/components/viewerMain/viewerMain.js +++ b/LesionTracker/client/components/viewerMain/viewerMain.js @@ -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; } }); \ No newline at end of file diff --git a/LesionTracker/client/components/viewerMain/viewerMain.styl b/LesionTracker/client/components/viewerMain/viewerMain.styl index 3cabe662f..4d3610e5a 100644 --- a/LesionTracker/client/components/viewerMain/viewerMain.styl +++ b/LesionTracker/client/components/viewerMain/viewerMain.styl @@ -1,4 +1,3 @@ .viewerMain - width: calc(100% - 120px) - height: 100% - float: left \ No newline at end of file + width: 100% + height: 84% \ No newline at end of file diff --git a/LesionTracker/client/head.html b/LesionTracker/client/head.html index a9819d223..3f4f775d3 100644 --- a/LesionTracker/client/head.html +++ b/LesionTracker/client/head.html @@ -2,7 +2,7 @@ Lesion Tracker - + diff --git a/LesionTracker/client/log.js b/LesionTracker/client/log.js new file mode 100644 index 000000000..f4e50c3e4 --- /dev/null +++ b/LesionTracker/client/log.js @@ -0,0 +1,3 @@ +// Create application logger using loglevel +// https://atmospherejs.com/spacejamio/loglevel +log = loglevel.createAppLogger('LesionTracker', defaultLevel = 'info'); diff --git a/LesionTracker/client/route.js b/LesionTracker/client/route.js deleted file mode 100644 index 2fa8c2777..000000000 --- a/LesionTracker/client/route.js +++ /dev/null @@ -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); - }); - } -}); \ No newline at end of file diff --git a/LesionTracker/client/routes.js b/LesionTracker/client/routes.js new file mode 100644 index 000000000..d283ee2e4 --- /dev/null +++ b/LesionTracker/client/routes.js @@ -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); + } +}); \ No newline at end of file diff --git a/LesionTracker/public/images/logo.png b/LesionTracker/public/images/logo.png index bcb42b3d7..fc83b1354 100644 Binary files a/LesionTracker/public/images/logo.png and b/LesionTracker/public/images/logo.png differ diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..b7a100f68 --- /dev/null +++ b/Makefile @@ -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 diff --git a/OHIFViewer/.gitignore b/OHIFViewer/.gitignore index b0746064f..0d0d24eac 100644 --- a/OHIFViewer/.gitignore +++ b/OHIFViewer/.gitignore @@ -1,3 +1,4 @@ +.idea .meteor/local .meteor/meteorite node_modules \ No newline at end of file diff --git a/OHIFViewer/.meteor/packages b/OHIFViewer/.meteor/packages index 2dbded030..78786adc7 100644 --- a/OHIFViewer/.meteor/packages +++ b/OHIFViewer/.meteor/packages @@ -33,3 +33,4 @@ dicomweb viewerbase worklist reactive-var +practicalmeteor:loglevel diff --git a/OHIFViewer/.meteor/release b/OHIFViewer/.meteor/release index 5684262a8..3a05e0a2f 100644 --- a/OHIFViewer/.meteor/release +++ b/OHIFViewer/.meteor/release @@ -1 +1 @@ -METEOR@1.2.0.2 +METEOR@1.2.1 diff --git a/OHIFViewer/.meteor/versions b/OHIFViewer/.meteor/versions index 1c2361305..47b1c5a5a 100644 --- a/OHIFViewer/.meteor/versions +++ b/OHIFViewer/.meteor/versions @@ -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 diff --git a/OHIFViewer/bin/localhostOrthanc.bat b/OHIFViewer/bin/localhostOrthanc.bat new file mode 100644 index 000000000..d1d615ff7 --- /dev/null +++ b/OHIFViewer/bin/localhostOrthanc.bat @@ -0,0 +1,2 @@ +set PACKAGE_DIRS=..\Packages +meteor --settings ../config/localhostOrthanc.json diff --git a/OHIFViewer/bin/medicalConnections.bat b/OHIFViewer/bin/medicalConnections.bat new file mode 100644 index 000000000..bf0ca7e9f --- /dev/null +++ b/OHIFViewer/bin/medicalConnections.bat @@ -0,0 +1,2 @@ +set PACKAGE_DIRS=..\Packages +meteor --settings ../config/medicalConnections.json diff --git a/OHIFViewer/bin/medkenOrthanc.bat b/OHIFViewer/bin/medkenOrthanc.bat new file mode 100644 index 000000000..5831ac369 --- /dev/null +++ b/OHIFViewer/bin/medkenOrthanc.bat @@ -0,0 +1,2 @@ +set PACKAGE_DIRS=..\Packages +meteor --settings ../config/medkenOrthanc.json diff --git a/OHIFViewer/bin/siimDCM4CHEE.bat b/OHIFViewer/bin/siimDCM4CHEE.bat new file mode 100644 index 000000000..b63adf424 --- /dev/null +++ b/OHIFViewer/bin/siimDCM4CHEE.bat @@ -0,0 +1,2 @@ +set PACKAGE_DIRS=..\Packages +meteor --settings ../config/siimDCM4CHEE.json diff --git a/OHIFViewer/client/components/viewer/viewer.html b/OHIFViewer/client/components/viewer/viewer.html index 173d21833..6e7b46e82 100644 --- a/OHIFViewer/client/components/viewer/viewer.html +++ b/OHIFViewer/client/components/viewer/viewer.html @@ -1,6 +1,9 @@ \ No newline at end of file diff --git a/OHIFViewer/client/components/viewer/viewer.js b/OHIFViewer/client/components/viewer/viewer.js index ae0c0b139..16a90bcd0 100644 --- a/OHIFViewer/client/components/viewer/viewer.js +++ b/OHIFViewer/client/components/viewer/viewer.js @@ -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(); }); diff --git a/OHIFViewer/client/components/viewer/viewer.styl b/OHIFViewer/client/components/viewer/viewer.styl index 854b359f5..4a1c1f8df 100644 --- a/OHIFViewer/client/components/viewer/viewer.styl +++ b/OHIFViewer/client/components/viewer/viewer.styl @@ -12,4 +12,10 @@ user-select: none .studyBrowser - width: auto \ No newline at end of file + width: auto + +.viewerMain + float: left + background-color: gray + height: 100% + width: calc(100% - 120px) \ No newline at end of file diff --git a/OHIFViewer/client/components/viewer/viewerMain/viewerMain.html b/OHIFViewer/client/components/viewer/viewerMain/viewerMain.html deleted file mode 100644 index eb39a71c8..000000000 --- a/OHIFViewer/client/components/viewer/viewerMain/viewerMain.html +++ /dev/null @@ -1,6 +0,0 @@ - \ No newline at end of file diff --git a/OHIFViewer/client/components/viewer/viewerMain/viewerMain.js b/OHIFViewer/client/components/viewer/viewerMain/viewerMain.js deleted file mode 100644 index 260bf1cc9..000000000 --- a/OHIFViewer/client/components/viewer/viewerMain/viewerMain.js +++ /dev/null @@ -1,5 +0,0 @@ -Template.viewerMain.helpers({ - studies : function() { - return this.studies; - } -}); \ No newline at end of file diff --git a/OHIFViewer/client/components/viewer/viewerMain/viewerMain.styl b/OHIFViewer/client/components/viewer/viewerMain/viewerMain.styl deleted file mode 100644 index 7b269b4cd..000000000 --- a/OHIFViewer/client/components/viewer/viewerMain/viewerMain.styl +++ /dev/null @@ -1,5 +0,0 @@ -.viewerMain - float: left - background-color: gray - height: 100% - width: calc(100% - 120px) \ No newline at end of file diff --git a/OHIFViewer/client/head.html b/OHIFViewer/client/head.html index 979732482..b3bffd530 100644 --- a/OHIFViewer/client/head.html +++ b/OHIFViewer/client/head.html @@ -2,7 +2,7 @@ OHIF Viewer - + diff --git a/OHIFViewer/client/log.js b/OHIFViewer/client/log.js new file mode 100644 index 000000000..1f567f0c5 --- /dev/null +++ b/OHIFViewer/client/log.js @@ -0,0 +1,3 @@ +// Create application logger using loglevel +// https://atmospherejs.com/spacejamio/loglevel +log = loglevel.createAppLogger('OHIFViewer', defaultLevel = 'info'); diff --git a/OHIFViewer/client/routes.js b/OHIFViewer/client/routes.js index 84ae25bfe..c05cebf2c 100644 --- a/OHIFViewer/client/routes.js +++ b/OHIFViewer/client/routes.js @@ -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); } }); \ No newline at end of file diff --git a/Packages/cornerstone/client/cornerstone.js b/Packages/cornerstone/client/cornerstone.js index b68821d71..f2cca6c2d 100644 --- a/Packages/cornerstone/client/cornerstone.js +++ b/Packages/cornerstone/client/cornerstone.js @@ -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; } } diff --git a/Packages/cornerstone/client/cornerstoneTools.js b/Packages/cornerstone/client/cornerstoneTools.js index 5b45ea67e..7aa176511 100644 --- a/Packages/cornerstone/client/cornerstoneTools.js +++ b/Packages/cornerstone/client/cornerstoneTools.js @@ -1,4 +1,4 @@ -/*! cornerstoneTools - v0.7.6 - 2015-10-08 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneTools */ +/*! cornerstoneTools - v0.7.7 - 2015-11-11 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneTools */ // Begin Source: src/header.js if (typeof cornerstone === 'undefined') { cornerstone = {}; @@ -40,7 +40,14 @@ if (typeof cornerstoneTools === 'undefined') { var direction = Math.max(-1, Math.min(1, (wheelDelta))); var mouseWheelData = { - element: element, viewport: cornerstone.getViewport(element), image: cornerstone.getEnabledElement(element).image, direction: direction, pageX: e.pageX || e.originalEvent.pageX, pageY: e.pageY || e.originalEvent.pageY, imageX: startingCoords.x, imageY: startingCoords.y + element: element, + viewport: cornerstone.getViewport(element), + image: cornerstone.getEnabledElement(element).image, + direction: direction, + pageX: e.pageX || e.originalEvent.pageX, + pageY: e.pageY || e.originalEvent.pageY, + imageX: startingCoords.x, + imageY: startingCoords.y }; $(element).trigger('CornerstoneToolsMouseWheel', mouseWheelData); @@ -58,7 +65,8 @@ if (typeof cornerstoneTools === 'undefined') { // module exports cornerstoneTools.mouseWheelInput = { - enable: enable, disable: disable + enable: enable, + disable: disable }; })($, cornerstone, cornerstoneTools); @@ -87,8 +95,11 @@ if (typeof cornerstoneTools === 'undefined') { var eventType = 'CornerstoneToolsMouseDoubleClick'; var startPoints = { - page: cornerstoneMath.point.pageToPoint(e), image: cornerstone.pageToPixel(element, e.pageX, e.pageY), client: { - x: e.clientX, y: e.clientY + page: cornerstoneMath.point.pageToPoint(e), + image: cornerstone.pageToPixel(element, e.pageX, e.pageY), + client: { + x: e.clientX, + y: e.clientY } }; startPoints.canvas = cornerstone.pixelToCanvas(element, startPoints.image); @@ -104,7 +115,8 @@ if (typeof cornerstoneTools === 'undefined') { lastPoints: lastPoints, currentPoints: startPoints, deltaPoints: { - x: 0, y: 0 + x: 0, + y: 0 }, type: eventType }; @@ -123,8 +135,11 @@ if (typeof cornerstoneTools === 'undefined') { $(element).off('mousemove', mouseMove); var startPoints = { - page: cornerstoneMath.point.pageToPoint(e), image: cornerstone.pageToPixel(element, e.pageX, e.pageY), client: { - x: e.clientX, y: e.clientY + page: cornerstoneMath.point.pageToPoint(e), + image: cornerstone.pageToPixel(element, e.pageX, e.pageY), + client: { + x: e.clientX, + y: e.clientY } }; startPoints.canvas = cornerstone.pixelToCanvas(element, startPoints.image); @@ -140,7 +155,8 @@ if (typeof cornerstoneTools === 'undefined') { lastPoints: lastPoints, currentPoints: startPoints, deltaPoints: { - x: 0, y: 0 + x: 0, + y: 0 }, type: eventType }; @@ -161,15 +177,21 @@ if (typeof cornerstoneTools === 'undefined') { // calculate our current points in page and image coordinates var eventType = 'CornerstoneToolsMouseDrag'; var currentPoints = { - page: cornerstoneMath.point.pageToPoint(e), image: cornerstone.pageToPixel(element, e.pageX, e.pageY), client: { - x: e.clientX, y: e.clientY + page: cornerstoneMath.point.pageToPoint(e), + image: cornerstone.pageToPixel(element, e.pageX, e.pageY), + client: { + x: e.clientX, + y: e.clientY } }; currentPoints.canvas = cornerstone.pixelToCanvas(element, currentPoints.image); // Calculate delta values in page and image coordinates var deltaPoints = { - page: cornerstoneMath.point.subtract(currentPoints.page, lastPoints.page), image: cornerstoneMath.point.subtract(currentPoints.image, lastPoints.image), client: cornerstoneMath.point.subtract(currentPoints.client, lastPoints.client), canvas: cornerstoneMath.point.subtract(currentPoints.canvas, lastPoints.canvas) + page: cornerstoneMath.point.subtract(currentPoints.page, lastPoints.page), + image: cornerstoneMath.point.subtract(currentPoints.image, lastPoints.image), + client: cornerstoneMath.point.subtract(currentPoints.client, lastPoints.client), + canvas: cornerstoneMath.point.subtract(currentPoints.canvas, lastPoints.canvas) }; var eventData = { @@ -228,7 +250,8 @@ if (typeof cornerstoneTools === 'undefined') { which: whichMouseButton, viewport: cornerstone.getViewport(element), image: cornerstone.getEnabledElement(element).image, - element: element, startPoints: startPoints, + element: element, + startPoints: startPoints, lastPoints: lastPoints, currentPoints: currentPoints, deltaPoints: deltaPoints, @@ -257,8 +280,11 @@ if (typeof cornerstoneTools === 'undefined') { var eventType = 'CornerstoneToolsMouseMove'; var startPoints = { - page: cornerstoneMath.point.pageToPoint(e), image: cornerstone.pageToPixel(element, e.pageX, e.pageY), client: { - x: e.clientX, y: e.clientY + page: cornerstoneMath.point.pageToPoint(e), + image: cornerstone.pageToPixel(element, e.pageX, e.pageY), + client: { + x: e.clientX, + y: e.clientY } }; startPoints.canvas = cornerstone.pixelToCanvas(element, startPoints.image); @@ -269,15 +295,21 @@ if (typeof cornerstoneTools === 'undefined') { // calculate our current points in page and image coordinates var currentPoints = { - page: cornerstoneMath.point.pageToPoint(e), image: cornerstone.pageToPixel(element, e.pageX, e.pageY), client: { - x: e.clientX, y: e.clientY + page: cornerstoneMath.point.pageToPoint(e), + image: cornerstone.pageToPixel(element, e.pageX, e.pageY), + client: { + x: e.clientX, + y: e.clientY } }; currentPoints.canvas = cornerstone.pixelToCanvas(element, currentPoints.image); // Calculate delta values in page and image coordinates var deltaPoints = { - page: cornerstoneMath.point.subtract(currentPoints.page, lastPoints.page), image: cornerstoneMath.point.subtract(currentPoints.image, lastPoints.image), client: cornerstoneMath.point.subtract(currentPoints.client, lastPoints.client), canvas: cornerstoneMath.point.subtract(currentPoints.canvas, lastPoints.canvas) + page: cornerstoneMath.point.subtract(currentPoints.page, lastPoints.page), + image: cornerstoneMath.point.subtract(currentPoints.image, lastPoints.image), + client: cornerstoneMath.point.subtract(currentPoints.client, lastPoints.client), + canvas: cornerstoneMath.point.subtract(currentPoints.canvas, lastPoints.canvas) }; var eventData = { @@ -329,16 +361,23 @@ if (typeof cornerstoneTools === 'undefined') { /*jshint newcap: false */ - var initScale = 1.0, + var lastScale = 1.0, lastRotation = 0.0, startPoints, currentPoints, lastPoints, deltaPoints, - eventData; + eventData, + touchStartDelay, + pressDelay = 700, + pressTimeout, + isPress = false, + pressMaxDistance = 5, + pageDistanceMoved, + preventNextPinch = false; function onTouch(e) { - console.log(e.type); + ///console.log(e.type); var element = e.target.parentNode, event, eventType; @@ -346,8 +385,18 @@ if (typeof cornerstoneTools === 'undefined') { // Prevent mouse events from occurring alongside touch events e.preventDefault(); + // If more than one finger is placed on the element, stop the press timeout + if ((e.pointers && e.pointers.length > 1) || + (e.originalEvent && e.originalEvent.touches && e.originalEvent.touches.length > 1)) { + isPress = false; + clearTimeout(pressTimeout); + } + switch (e.type) { case 'tap': + isPress = false; + clearTimeout(pressTimeout); + // calculate our current points in page and image coordinates currentPoints = { page: cornerstoneMath.point.pageToPoint(e.pointers[0]), @@ -375,6 +424,9 @@ if (typeof cornerstoneTools === 'undefined') { break; case 'doubletap': + isPress = false; + clearTimeout(pressTimeout); + // calculate our current points in page and image coordinates currentPoints = { page: cornerstoneMath.point.pageToPoint(e.pointers[0]), @@ -401,41 +453,25 @@ if (typeof cornerstoneTools === 'undefined') { $(element).trigger(event, eventData); break; - case 'press': - // calculate our current points in page and image coordinates - currentPoints = { - page: cornerstoneMath.point.pageToPoint(e.pointers[0]), - image: cornerstone.pageToPixel(element, e.pointers[0].pageX, e.pointers[0].pageY), - client: { - x: e.pointers[0].clientX, - y: e.pointers[0].clientY - } - }; - currentPoints.canvas = cornerstone.pixelToCanvas(element, currentPoints.image); - - eventType = 'CornerstoneToolsTouchPress'; - eventData = { - event: e, - viewport: cornerstone.getViewport(element), - image: cornerstone.getEnabledElement(element).image, - element: element, - currentPoints: currentPoints, - type: eventType, - isTouchEvent: true - }; - - event = $.Event(eventType, eventData); - $(element).trigger(event, eventData); - break; - case 'pinchstart': - var viewport = cornerstone.getViewport(element); - initScale = viewport.scale || 1; + isPress = false; + clearTimeout(pressTimeout); + + lastScale = 1.0; break; case 'pinchmove': - var scaleChange = initScale * e.scale; - + isPress = false; + clearTimeout(pressTimeout); + + if (preventNextPinch === true) { + lastScale = e.scale; + preventNextPinch = false; + break; + } + + var scaleChange = (e.scale - lastScale) / lastScale; + startPoints = { page: e.center, image: cornerstone.pageToPixel(element, e.center.x, e.center.y), @@ -457,46 +493,106 @@ if (typeof cornerstoneTools === 'undefined') { event = $.Event(eventType, eventData); $(element).trigger(event, eventData); + + lastScale = e.scale; break; case 'touchstart': - startPoints = { - page: cornerstoneMath.point.pageToPoint(e.originalEvent.touches[0]), - image: cornerstone.pageToPixel(element, e.originalEvent.touches[0].pageX, e.originalEvent.touches[0].pageY), - client: { - x: e.originalEvent.touches[0].clientX, - y: e.originalEvent.touches[0].clientY + lastScale = 1.0; + + clearTimeout(pressTimeout); + + clearTimeout(touchStartDelay); + touchStartDelay = setTimeout(function() { + startPoints = { + page: cornerstoneMath.point.pageToPoint(e.originalEvent.touches[0]), + image: cornerstone.pageToPixel(element, e.originalEvent.touches[0].pageX, e.originalEvent.touches[0].pageY), + client: { + x: e.originalEvent.touches[0].clientX, + y: e.originalEvent.touches[0].clientY + } + }; + startPoints.canvas = cornerstone.pixelToCanvas(element, startPoints.image); + + eventType = 'CornerstoneToolsTouchStart'; + if (e.originalEvent.touches.length > 1) { + eventType = 'CornerstoneToolsMultiTouchStart'; } - }; - startPoints.canvas = cornerstone.pixelToCanvas(element, startPoints.image); - eventType = 'CornerstoneToolsTouchStart'; + eventData = { + event: e, + viewport: cornerstone.getViewport(element), + image: cornerstone.getEnabledElement(element).image, + element: element, + startPoints: startPoints, + currentPoints: startPoints, + type: eventType, + isTouchEvent: true + }; - eventData = { - event: e.srcEvent, - viewport: cornerstone.getViewport(element), - image: cornerstone.getEnabledElement(element).image, - element: element, - startPoints: startPoints, - currentPoints: startPoints, - type: eventType, - isTouchEvent: true - }; + event = $.Event(eventType, eventData); + $(element).trigger(event, eventData); - event = $.Event(eventType, eventData); - $(eventData.element).trigger(event, eventData); + if (event.isImmediatePropagationStopped() === false) { + //isPress = false; + //clearTimeout(pressTimeout); - if (event.isImmediatePropagationStopped() === false) { - // No current tools responded to the drag action. - // Create new tool measurement - eventType = 'CornerstoneToolsTouchStartActive'; - eventData.type = eventType; - $(element).trigger(eventType, eventData); - } - lastPoints = cornerstoneTools.copyPoints(startPoints); + // No current tools responded to the drag action. + // Create new tool measurement + eventType = 'CornerstoneToolsTouchStartActive'; + if (e.originalEvent.touches.length > 1) { + eventType = 'CornerstoneToolsMultiTouchStartActive'; + } + + eventData.type = eventType; + $(element).trigger(eventType, eventData); + } + + //console.log(eventType); + lastPoints = cornerstoneTools.copyPoints(startPoints); + }, 50); + + isPress = true; + pageDistanceMoved = 0; + pressTimeout = setTimeout(function() { + if (!isPress) { + return; + } + + currentPoints = { + page: cornerstoneMath.point.pageToPoint(e.originalEvent.touches[0]), + image: cornerstone.pageToPixel(element, e.originalEvent.touches[0].pageX, e.originalEvent.touches[0].pageY), + client: { + x: e.originalEvent.touches[0].clientX, + y: e.originalEvent.touches[0].clientY + } + }; + currentPoints.canvas = cornerstone.pixelToCanvas(element, startPoints.image); + + eventType = 'CornerstoneToolsTouchPress'; + eventData = { + event: e, + viewport: cornerstone.getViewport(element), + image: cornerstone.getEnabledElement(element).image, + element: element, + currentPoints: currentPoints, + type: eventType, + isTouchEvent: true + }; + + event = $.Event(eventType, eventData); + $(element).trigger(event, eventData); + + //console.log(eventType); + }, pressDelay); break; case 'touchend': + lastScale = 1.0; + + isPress = false; + clearTimeout(pressTimeout); + startPoints = { page: cornerstoneMath.point.pageToPoint(e.originalEvent.changedTouches[0]), image: cornerstone.pageToPixel(element, e.originalEvent.changedTouches[0].pageX, e.originalEvent.changedTouches[0].pageY), @@ -510,7 +606,7 @@ if (typeof cornerstoneTools === 'undefined') { eventType = 'CornerstoneToolsTouchEnd'; eventData = { - event: e.srcEvent, + event: e, viewport: cornerstone.getViewport(element), image: cornerstone.getEnabledElement(element).image, element: element, @@ -521,7 +617,7 @@ if (typeof cornerstoneTools === 'undefined') { }; event = $.Event(eventType, eventData); - $(eventData.element).trigger(event, eventData); + $(element).trigger(event, eventData); break; case 'panmove': @@ -543,6 +639,14 @@ if (typeof cornerstoneTools === 'undefined') { client: cornerstoneMath.point.subtract(currentPoints.client, lastPoints.client), canvas: cornerstoneMath.point.subtract(currentPoints.canvas, lastPoints.canvas) }; + + pageDistanceMoved += Math.sqrt(deltaPoints.page.x * deltaPoints.page.x + deltaPoints.page.y * deltaPoints.page.y); + //console.log("pageDistanceMoved: " + pageDistanceMoved); + if (pageDistanceMoved > pressMaxDistance) { + //console.log('Press event aborted due to movement'); + isPress = false; + clearTimeout(pressTimeout); + } eventType = 'CornerstoneToolsTouchDrag'; if (e.pointers.length > 1) { @@ -568,7 +672,23 @@ if (typeof cornerstoneTools === 'undefined') { lastPoints = cornerstoneTools.copyPoints(currentPoints); break; + case 'panstart': + currentPoints = { + page: cornerstoneMath.point.pageToPoint(e.pointers[0]), + image: cornerstone.pageToPixel(element, e.pointers[0].pageX, e.pointers[0].pageY), + client: { + x: e.pointers[0].clientX, + y: e.pointers[0].clientY + } + }; + currentPoints.canvas = cornerstone.pixelToCanvas(element, currentPoints.image); + lastPoints = cornerstoneTools.copyPoints(currentPoints); + break; + case 'panend': + isPress = false; + clearTimeout(pressTimeout); + // If lastPoints is not yet set, it means panend fired without panstart or pan, // so we can ignore this event if (!lastPoints) { @@ -610,9 +730,18 @@ if (typeof cornerstoneTools === 'undefined') { event = $.Event(eventType, eventData); $(element).trigger(event, eventData); + + var remainingPointers = e.pointers.length - e.changedPointers.length; + if (remainingPointers === 2) { + preventNextPinch = true; + } + return cornerstoneTools.pauseEvent(e); case 'rotatemove': + isPress = false; + clearTimeout(pressTimeout); + var rotation = e.rotation - lastRotation; lastRotation = e.rotation; @@ -629,11 +758,14 @@ if (typeof cornerstoneTools === 'undefined') { $(element).trigger(event, eventData); break; } + + //console.log(eventType); return false; } function enable(element) { disable(element); + var hammerOptions = { inputClass: Hammer.SUPPORT_POINTER_EVENTS ? Hammer.PointerEventInput : Hammer.TouchInput }; @@ -653,29 +785,30 @@ if (typeof cornerstoneTools === 'undefined') { var rotate = new Hammer.Rotate({ threshold: 0 }); - var press = new Hammer.Press({ - threshold: 10 - }); // we want to detect both the same time pinch.recognizeWith(pan); pinch.recognizeWith(rotate); // add to the Manager - mc.add([ pan, press, rotate, pinch ]); - mc.on('press tap doubletap panstart panmove panend pinchstart pinchmove rotatemove', onTouch); + mc.add([ pan, rotate, pinch ]); + mc.on('tap doubletap panstart panmove panend pinchstart pinchmove rotatemove', onTouch); - $(element).data('hammer', mc); + cornerstoneTools.preventGhostClick.enable(element); $(element).on('touchstart touchend', onTouch); - cornerstoneTools.preventGhostClick(element); + $(element).data('hammer', mc); + //console.log('touchInput enabled'); } function disable(element) { + cornerstoneTools.preventGhostClick.disable(element); $(element).off('touchstart touchend', onTouch); var mc = $(element).data('hammer'); if (mc) { - mc.off('press tap doubletap panstart panmove panend pinchmove rotatemove', onTouch); + mc.off('tap doubletap panstart panmove panend pinchmove rotatemove', onTouch); } + + //console.log('touchInput disabled'); } // module exports @@ -700,7 +833,8 @@ if (typeof cornerstoneTools === 'undefined') { activate: function(element, mouseButtonMask, options) { $(element).off('CornerstoneToolsMouseDownActivate', mouseDownCallback); var eventData = { - mouseButtonMask: mouseButtonMask, options: options + mouseButtonMask: mouseButtonMask, + options: options }; $(element).on('CornerstoneToolsMouseDownActivate', eventData, mouseDownCallback); }, @@ -828,14 +962,6 @@ if (typeof cornerstoneTools === 'undefined') { data.active = !data.active; imageNeedsUpdate = true; } - - //TODO: ayselafsar - //Check hover event for lesion tool text - if (mouseToolInterface.toolType === "lesion") { - if ( mouseToolInterface.pointNearToolForText(eventData.element, data, coords) && !data.active) { - data.active = !data.active; - } - } } // Handle activation status changed, redraw the image @@ -871,7 +997,7 @@ if (typeof cornerstoneTools === 'undefined') { for (i = 0; i < toolData.data.length; i++) { data = toolData.data[i]; var distanceSq = 25; - var handle = cornerstoneTools.getHandleNearImagePoint(element, data, coords, distanceSq); + var handle = cornerstoneTools.getHandleNearImagePoint(element, data.handles, coords, distanceSq); if (handle) { $(element).off('CornerstoneToolsMouseMove', mouseToolInterface.mouseMoveCallback || mouseMoveCallback); data.active = true; @@ -900,19 +1026,6 @@ if (typeof cornerstoneTools === 'undefined') { } } } - - //TODO: ayselafsar: Add check statement if mouse down location is on text callout. If so, call moveTextCallout function - if(toolData !== undefined && mouseToolInterface.pointNearToolForText !== undefined) { - for(i=0; i < toolData.data.length; i++) { - data = toolData.data[i]; - if(mouseToolInterface.pointNearToolForText(eventData.element, data, coords)) { - $(eventData.element).off('CornerstoneToolsMouseMove', mouseMoveCallback); - cornerstoneTools.moveTextHandles(e, data, toolData, true); - e.stopImmediatePropagation(); - return false; - } - } - } } } ///////// END DEACTIVE TOOL /////// @@ -1137,10 +1250,12 @@ if (typeof cornerstoneTools === 'undefined') { var i; // now check to see if there is a handle we can move + var distanceSq = 25; + if (toolData !== undefined) { for (i = 0; i < toolData.data.length; i++) { data = toolData.data[i]; - var handle = cornerstoneTools.getHandleNearImagePoint(eventData.element, data, coords); + var handle = cornerstoneTools.getHandleNearImagePoint(eventData.element, data.handles, coords, distanceSq); if (handle !== undefined) { $(eventData.element).off('CornerstoneToolsMouseMove', mouseMoveCallback); data.active = true; @@ -1153,12 +1268,17 @@ if (typeof cornerstoneTools === 'undefined') { // Now check to see if there is a line we can move // now check to see if we have a tool that we can move + var options = { + deleteIfHandleOutsideImage: true, + preventHandleOutsideImage: preventHandleOutsideImage + }; + if (toolData !== undefined && mouseToolInterface.pointInsideRect !== undefined) { for (i = 0; i < toolData.data.length; i++) { data = toolData.data[i]; if (mouseToolInterface.pointInsideRect(eventData.element, data, coords)) { $(eventData.element).off('CornerstoneToolsMouseMove', mouseMoveCallback); - cornerstoneTools.moveAllHandles(e, data, toolData, false, preventHandleOutsideImage); + cornerstoneTools.moveAllHandles(e, data, toolData, options); $(eventData.element).on('CornerstoneToolsMouseMove', mouseMoveCallback); e.stopImmediatePropagation(); return false; @@ -1296,17 +1416,20 @@ if (typeof cornerstoneTools === 'undefined') { if (options && options.activateCallback) { options.activateCallback(element); } - }, disable: function(element) { + }, + disable: function(element) { $(element).off(events, touchDragCallback); if (options && options.disableCallback) { options.disableCallback(element); } - }, enable: function(element) { + }, + enable: function(element) { $(element).off(events, touchDragCallback); if (options && options.enableCallback) { options.enableCallback(element); } - }, deactivate: function(element) { + }, + deactivate: function(element) { $(element).off(events, touchDragCallback); if (options && options.deactivateCallback) { options.deactivateCallback(element); @@ -1384,6 +1507,7 @@ if (typeof cornerstoneTools === 'undefined') { ///////// BEGIN ACTIVE TOOL /////// function addNewMeasurement(touchEventData) { + //console.log('touchTool addNewMeasurement'); var element = touchEventData.element; var measurementData = touchToolInterface.createNewMeasurement(touchEventData); @@ -1408,7 +1532,6 @@ if (typeof cornerstoneTools === 'undefined') { } $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); - $(element).off('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback); $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); cornerstone.updateImage(element); @@ -1421,13 +1544,13 @@ if (typeof cornerstoneTools === 'undefined') { } $(element).on('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); - $(element).on('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback); $(element).on('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); cornerstone.updateImage(element); }); } function touchDownActivateCallback(e, eventData) { + //console.log('touchTool touchDownActivateCallback'); if (touchToolInterface.addNewMeasurement) { touchToolInterface.addNewMeasurement(eventData); } else { @@ -1439,38 +1562,8 @@ if (typeof cornerstoneTools === 'undefined') { ///////// END ACTIVE TOOL /////// ///////// BEGIN INACTIVE TOOL /////// - function touchMoveCallback(e, eventData) { - cornerstoneTools.toolCoordinates.setCoords(eventData); - - // if we have no tool data for this element, do nothing - var toolData = cornerstoneTools.getToolState(eventData.element, touchToolInterface.toolType); - if (!toolData) { - return; - } - - // We have tool data, search through all data - // and see if we can activate a handle - var imageNeedsUpdate = false; - var distanceThreshold = 50; - for (var i = 0; i < toolData.data.length; i++) { - // get the touch position in canvas coordinates - var coords = eventData.currentPoints.canvas; - - var data = toolData.data[i]; - if (cornerstoneTools.handleActivator(eventData.element, data.handles, coords, distanceThreshold) === true) { - imageNeedsUpdate = true; - } - } - - // Handle activation status changed, redraw the image - if (imageNeedsUpdate === true) { - cornerstone.updateImage(eventData.element); - } - - return false; - } - function tapCallback(e, eventData) { + //console.log('touchTool tapCallback'); var element = eventData.element; var coords = eventData.currentPoints.canvas; var toolData = cornerstoneTools.getToolState(e.currentTarget, touchToolInterface.toolType); @@ -1481,6 +1574,7 @@ if (typeof cornerstoneTools === 'undefined') { deactivateAllToolInstances(toolData); function doneMovingCallback() { + console.log('touchTool tapCallback doneMovingCallback'); deactivateAllToolInstances(toolData); if (cornerstoneTools.anyHandlesOutsideImage(eventData, data.handles)) { // delete the measurement @@ -1488,7 +1582,6 @@ if (typeof cornerstoneTools === 'undefined') { } cornerstone.updateImage(element); - $(element).on('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback); $(element).on('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); $(element).on('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); } @@ -1498,9 +1591,8 @@ if (typeof cornerstoneTools === 'undefined') { for (i = 0; i < toolData.data.length; i++) { data = toolData.data[i]; var distanceSq = 75; // Should probably make this a settable property later - var handle = cornerstoneTools.getHandleNearImagePoint(element, data, coords, distanceSq); + var handle = cornerstoneTools.getHandleNearImagePoint(element, data.handles, coords, distanceSq); if (handle) { - $(element).off('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback); $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); data.active = true; @@ -1518,7 +1610,6 @@ if (typeof cornerstoneTools === 'undefined') { for (i = 0; i < toolData.data.length; i++) { data = toolData.data[i]; if (touchToolInterface.pointNearTool(element, data, coords)) { - $(element).off('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback); $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); data.active = true; @@ -1542,6 +1633,7 @@ if (typeof cornerstoneTools === 'undefined') { } function touchStartCallback(e, eventData) { + //console.log('touchTool touchStartCallback'); var element = eventData.element; var coords = eventData.startPoints.canvas; var data; @@ -1549,6 +1641,7 @@ if (typeof cornerstoneTools === 'undefined') { var i; function doneMovingCallback() { + //console.log('touchTool touchStartCallback doneMovingCallback'); data.active = false; data.invalidated = true; if (cornerstoneTools.anyHandlesOutsideImage(eventData, data.handles)) { @@ -1557,7 +1650,6 @@ if (typeof cornerstoneTools === 'undefined') { } cornerstone.updateImage(eventData.element); - $(element).on('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback); $(element).on('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); $(element).on('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); } @@ -1569,9 +1661,8 @@ if (typeof cornerstoneTools === 'undefined') { for (i = 0; i < toolData.data.length; i++) { data = toolData.data[i]; - var handle = cornerstoneTools.getHandleNearImagePoint(eventData.element, data, coords, distanceSq); + var handle = cornerstoneTools.getHandleNearImagePoint(eventData.element, data.handles, coords, distanceSq); if (handle) { - $(element).off('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback); $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); data.active = true; @@ -1587,11 +1678,9 @@ if (typeof cornerstoneTools === 'undefined') { for (i = 0; i < toolData.data.length; i++) { data = toolData.data[i]; if (touchToolInterface.pointNearTool(eventData.element, data, coords)) { - $(element).off('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback); $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); cornerstoneTools.touchMoveAllHandles(e, data, toolData, true, doneMovingCallback); - $(element).on('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback); e.stopImmediatePropagation(); return false; // false = causes jquery to preventDefault() and stopPropagation() this event } @@ -1603,7 +1692,6 @@ if (typeof cornerstoneTools === 'undefined') { // not visible, not interactive function disable(element) { $(element).off('CornerstoneImageRendered', touchToolInterface.onImageRendered); - $(element).off('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback); $(element).off('CornerstoneToolsTouchStart', touchToolInterface.touchStartCallback || touchStartCallback); $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); @@ -1622,7 +1710,6 @@ if (typeof cornerstoneTools === 'undefined') { // visible but not interactive function enable(element) { $(element).off('CornerstoneImageRendered', touchToolInterface.onImageRendered); - $(element).off('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback); $(element).off('CornerstoneToolsTouchStart', touchToolInterface.touchStartCallback || touchStartCallback); $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); @@ -1642,14 +1729,14 @@ if (typeof cornerstoneTools === 'undefined') { // visible, interactive and can create function activate(element) { + //console.log('activate touchTool'); + $(element).off('CornerstoneImageRendered', touchToolInterface.onImageRendered); - $(element).off('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback); $(element).off('CornerstoneToolsTouchStart', touchToolInterface.touchStartCallback || touchStartCallback); $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); $(element).on('CornerstoneImageRendered', touchToolInterface.onImageRendered); - $(element).on('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback); $(element).on('CornerstoneToolsTouchStart', touchToolInterface.touchStartCallback || touchStartCallback); $(element).on('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); $(element).on('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); @@ -1669,14 +1756,14 @@ if (typeof cornerstoneTools === 'undefined') { // visible, interactive function deactivate(element) { + //console.log('deactivate touchTool'); + $(element).off('CornerstoneImageRendered', touchToolInterface.onImageRendered); - $(element).off('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback); $(element).off('CornerstoneToolsTouchStart', touchToolInterface.touchStartCallback || touchStartCallback); $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); $(element).on('CornerstoneImageRendered', touchToolInterface.onImageRendered); - $(element).on('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback); //$(element).on('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); if (touchToolInterface.doubleTapCallback) { @@ -1698,11 +1785,15 @@ if (typeof cornerstoneTools === 'undefined') { activate: activate, deactivate: deactivate, touchStartCallback: touchToolInterface.touchStartCallback || touchStartCallback, - touchMoveCallback: touchToolInterface.touchMoveCallback || touchMoveCallback, touchDownActivateCallback: touchToolInterface.touchDownActivateCallback || touchDownActivateCallback, tapCallback: touchToolInterface.tapCallback || tapCallback }; + // Expose pointNearTool if available + if (touchToolInterface.pointNearTool) { + toolInterface.pointNearTool = touchToolInterface.pointNearTool; + } + if (touchToolInterface.doubleTapCallback) { toolInterface.doubleTapCallback = touchToolInterface.doubleTapCallback; } @@ -1812,9 +1903,9 @@ if (typeof cornerstoneTools === 'undefined') { // configurable shadow if (config && config.shadow) { - context.shadowColor = '#000000'; - context.shadowOffsetX = +1; - context.shadowOffsetY = +1; + context.shadowColor = config.shadowColor || '#000000'; + context.shadowOffsetX = config.shadowOffsetX || 1; + context.shadowOffsetY = config.shadowOffsetY || 1; } var data = toolData.data[i]; @@ -2000,7 +2091,24 @@ if (typeof cornerstoneTools === 'undefined') { }; var distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords); - return (distanceToPoint < 25); + if (distanceToPoint < 25) { + return true; + } + + if (data.textCoords) { + var padding = 5; + var fontSize = cornerstoneTools.textStyle.getFontSize(); + + var rect = { + left: data.textCoords.x, + top: data.textCoords.y, + width: data.textWidth + (padding * 2), + height: fontSize + (padding * 2) + }; + + var distanceToTextRect = cornerstoneMath.rect.distanceToPoint(rect, coords); + return (distanceToTextRect < 25); + } } function drawArrow(context, start, end, color, lineWidth) { @@ -2057,6 +2165,12 @@ if (typeof cornerstoneTools === 'undefined') { for (var i = 0; i < toolData.data.length; i++) { context.save(); + if (config && config.shadow) { + context.shadowColor = config.shadowColor || '#000000'; + context.shadowOffsetX = config.shadowOffsetX || 1; + context.shadowOffsetY = config.shadowOffsetY || 1; + } + var data = toolData.data[i]; if (data.active) { @@ -2102,11 +2216,13 @@ if (typeof cornerstoneTools === 'undefined') { y: handleEndCanvas.y - handleStartCanvas.y }; + var textWidth = context.measureText(data.text).width; + var textCoords; if (config.arrowFirst) { // Fix text placement if arrow faces right if (vector.x < 0) { - displacement.x = -displacement.x - context.measureText(data.text).width; + displacement.x = -displacement.x - textWidth; } textCoords = { @@ -2116,7 +2232,7 @@ if (typeof cornerstoneTools === 'undefined') { } else { // Fix text placement if arrow faces right if (vector.x > 0) { - displacement.x = -displacement.x - context.measureText(data.text).width; + displacement.x = -displacement.x - textWidth; } textCoords = { @@ -2125,6 +2241,8 @@ if (typeof cornerstoneTools === 'undefined') { }; } + data.textCoords = textCoords; + data.textWidth = textWidth; cornerstoneTools.drawTextBox(context, data.text, textCoords.x, textCoords.y, color); } @@ -2150,8 +2268,7 @@ if (typeof cornerstoneTools === 'undefined') { var measurementData = createNewMeasurement(touchEventData); cornerstoneTools.addToolState(element, toolType, measurementData); - $(element).off('CornerstoneToolsTouchDrag', cornerstoneTools.arrowAnnotateTouch.touchMoveCallback); - $(element).off('CornerstoneToolsDragStartActive', cornerstoneTools.arrowAnnotateTouch.touchDownActivateCallback); + $(element).off('CornerstoneToolsTouchStartActive', cornerstoneTools.arrowAnnotateTouch.touchDownActivateCallback); $(element).off('CornerstoneToolsTap', cornerstoneTools.arrowAnnotateTouch.tapCallback); cornerstone.updateImage(element); @@ -2168,8 +2285,7 @@ if (typeof cornerstoneTools === 'undefined') { config.getTextCallback(doneChangingTextCallback); } - $(element).on('CornerstoneToolsTouchDrag', cornerstoneTools.arrowAnnotateTouch.touchMoveCallback); - $(element).on('CornerstoneToolsDragStartActive', cornerstoneTools.arrowAnnotateTouch.touchDownActivateCallback); + $(element).on('CornerstoneToolsTouchStartActive', cornerstoneTools.arrowAnnotateTouch.touchDownActivateCallback); $(element).on('CornerstoneToolsTap', cornerstoneTools.arrowAnnotateTouch.tapCallback); }); } @@ -2219,6 +2335,55 @@ if (typeof cornerstoneTools === 'undefined') { return false; // false = causes jquery to preventDefault() and stopPropagation() this event } + function pressCallback(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); + $(element).on('CornerstoneToolsTouchStart', cornerstoneTools.arrowAnnotateTouch.touchStartCallback); + $(element).on('CornerstoneToolsTouchStartActive', cornerstoneTools.arrowAnnotateTouch.touchDownActivateCallback); + } + + if (e.data && e.data.mouseButtonMask && !cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) { + return false; + } + + var config = cornerstoneTools.arrowAnnotate.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 + $(element).off('CornerstoneToolsTouchStart', cornerstoneTools.arrowAnnotateTouch.touchStartCallback); + $(element).off('CornerstoneToolsTouchStartActive', cornerstoneTools.arrowAnnotateTouch.touchDownActivateCallback); + config.changeTextCallback(data, doneChangingTextCallback); + + e.stopImmediatePropagation(); + return false; + } + } + + return false; // false = causes jquery to preventDefault() and stopPropagation() this event + } + cornerstoneTools.arrowAnnotate = cornerstoneTools.mouseButtonTool({ addNewMeasurement: addNewMeasurement, createNewMeasurement: createNewMeasurement, @@ -2236,7 +2401,7 @@ if (typeof cornerstoneTools === 'undefined') { onImageRendered: onImageRendered, pointNearTool: pointNearTool, toolType: toolType, - pressCallback: doubleClickCallback + pressCallback: pressCallback }); })($, cornerstone, cornerstoneMath, cornerstoneTools); @@ -2251,6 +2416,8 @@ if (typeof cornerstoneTools === 'undefined') { var toolType = 'crosshairs'; function chooseLocation(e, eventData) { + e.stopImmediatePropagation(); // Prevent CornerstoneToolsTouchStartActive from killing any press events + // 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) { @@ -2478,9 +2645,16 @@ if (typeof cornerstoneTools === 'undefined') { var color = cornerstoneTools.toolColors.getActiveColor(); var font = cornerstoneTools.textStyle.getFont(); var fontHeight = cornerstoneTools.textStyle.getFontSize(); + var config = cornerstoneTools.dragProbe.getConfiguration(); context.save(); + if (config && config.shadow) { + context.shadowColor = config.shadowColor || '#000000'; + context.shadowOffsetX = config.shadowOffsetX || 1; + context.shadowOffsetY = config.shadowOffsetY || 1; + } + var x = Math.round(eventData.currentPoints.image.x); var y = Math.round(eventData.currentPoints.image.y); @@ -2538,17 +2712,20 @@ if (typeof cornerstoneTools === 'undefined') { var color = cornerstoneTools.toolColors.getActiveColor(); var font = cornerstoneTools.textStyle.getFont(); - + var config = cornerstoneTools.dragProbe.getConfiguration(); + context.save(); + if (config && config.shadow) { + context.shadowColor = config.shadowColor || '#000000'; + context.shadowOffsetX = config.shadowOffsetX || 1; + context.shadowOffsetY = config.shadowOffsetY || 1; + } + var toolCoords; if (eventData.isTouchEvent === true) { - var ratio = image.height / enabledElement.canvas.height; - console.log ('Image to canvas height ratio: ' + ratio); - toolCoords = { - x: eventData.currentPoints.image.x, - y: eventData.currentPoints.image.y - cornerstoneTools.textStyle.getFontSize() * 4 * ratio - }; + toolCoords = cornerstone.pageToPixel(element, eventData.currentPoints.page.x, + eventData.currentPoints.page.y - cornerstoneTools.textStyle.getFontSize() * 4); } else { toolCoords = eventData.currentPoints.image; } @@ -2796,9 +2973,16 @@ if (typeof cornerstoneTools === 'undefined') { var lineWidth = cornerstoneTools.toolStyle.getToolWidth(); var font = cornerstoneTools.textStyle.getFont(); var fontHeight = cornerstoneTools.textStyle.getFontSize(); + var config = cornerstoneTools.ellipticalRoi.getConfiguration(); for (var i = 0; i < toolData.data.length; i++) { context.save(); + + if (config && config.shadow) { + context.shadowColor = config.shadowColor || '#000000'; + context.shadowOffsetX = config.shadowOffsetX || 1; + context.shadowOffsetY = config.shadowOffsetY || 1; + } var data = toolData.data[i]; @@ -3599,11 +3783,12 @@ if (typeof cornerstoneTools === 'undefined') { 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; + context.shadowColor = config.shadowColor || '#000000'; + context.shadowOffsetX = config.shadowOffsetX || 1; + context.shadowOffsetY = config.shadowOffsetY || 1; } var data = toolData.data[i]; @@ -3777,9 +3962,15 @@ if (typeof cornerstoneTools === 'undefined') { var zoomCtx = magnify.getContext('2d'); zoomCtx.setTransform(1, 0, 0, 1, 0, 0); + var getSize = magnifySize / magnificationLevel; + // Calculate the on-canvas location of the mouse pointer / touch var canvasLocation = cornerstone.pixelToCanvas(eventData.element, eventData.currentPoints.image); + if (eventData.isTouchEvent === true) { + canvasLocation.y -= 1.25 * getSize; + } + canvasLocation.x = Math.max(canvasLocation.x, 0); canvasLocation.x = Math.min(canvasLocation.x, canvas.width); @@ -3793,7 +3984,6 @@ if (typeof cornerstoneTools === 'undefined') { // Fill it with the pixels that the mouse is clicking on zoomCtx.fillRect(0, 0, magnifySize, magnifySize); - var getSize = magnifySize / magnificationLevel; var copyFrom = { x: canvasLocation.x - 0.5 * getSize, y: canvasLocation.y - 0.5 * getSize @@ -3815,14 +4005,9 @@ if (typeof cornerstoneTools === 'undefined') { }; zoomCtx.drawImage(canvas, copyFrom.x, copyFrom.y, canvas.width - copyFrom.x, canvas.height - copyFrom.y, 0, 0, scaledMagnify.x, scaledMagnify.y); - if (eventData.isTouchEvent === true) { - magnify.style.top = canvasLocation.y - 1 * magnifySize + 'px'; - magnify.style.left = canvasLocation.x - 0.5 * magnifySize + 'px'; - } else { - // Place the magnification tool at the same location as the pointer - magnify.style.top = canvasLocation.y - 0.5 * magnifySize + 'px'; - magnify.style.left = canvasLocation.x - 0.5 * magnifySize + 'px'; - } + // Place the magnification tool at the same location as the pointer + magnify.style.top = canvasLocation.y - 0.5 * magnifySize + 'px'; + magnify.style.left = canvasLocation.x - 0.5 * magnifySize + 'px'; magnify.style.display = 'block'; @@ -4445,7 +4630,8 @@ if (typeof cornerstoneTools === 'undefined') { var rect = eventData.element.getBoundingClientRect(eventData.element); var points = { - x: eventData.currentPoints.client.x, y: eventData.currentPoints.client.y + x: eventData.currentPoints.client.x, + y: eventData.currentPoints.client.y }; var width = eventData.element.clientWidth; @@ -4530,7 +4716,8 @@ if (typeof cornerstoneTools === 'undefined') { } cornerstoneTools.rotateTouch = { - activate: activate, disable: disable + activate: activate, + disable: disable }; })($, cornerstone, cornerstoneTools); @@ -4588,13 +4775,26 @@ if (typeof cornerstoneTools === 'undefined') { function createNewMeasurement(mouseEventData) { // create the measurement data for this tool with the end handle activated var angleData = { - visible: true, active: true, handles: { + visible: true, + active: true, + handles: { start: { - x: mouseEventData.currentPoints.image.x, y: mouseEventData.currentPoints.image.y, highlight: true, active: false - }, middle: { - x: mouseEventData.currentPoints.image.x, y: mouseEventData.currentPoints.image.y, highlight: true, active: true - }, end: { - x: mouseEventData.currentPoints.image.x, y: mouseEventData.currentPoints.image.y, highlight: true, active: false + x: mouseEventData.currentPoints.image.x, + y: mouseEventData.currentPoints.image.y, + highlight: true, + active: false + }, + middle: { + x: mouseEventData.currentPoints.image.x, + y: mouseEventData.currentPoints.image.y, + highlight: true, + active: true + }, + end: { + x: mouseEventData.currentPoints.image.x, + y: mouseEventData.currentPoints.image.y, + highlight: true, + active: false } } }; @@ -4605,11 +4805,12 @@ if (typeof cornerstoneTools === 'undefined') { function pointNearTool(element, data, coords) { var lineSegment = { - start: cornerstone.pixelToCanvas(element, data.handles.start), end: cornerstone.pixelToCanvas(element, data.handles.middle) + start: cornerstone.pixelToCanvas(element, data.handles.start), + end: cornerstone.pixelToCanvas(element, data.handles.middle) }; var distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords); - if (distanceToPoint < 5) { + if (distanceToPoint < 25) { return true; } @@ -4617,7 +4818,7 @@ if (typeof cornerstoneTools === 'undefined') { lineSegment.end = cornerstone.pixelToCanvas(element, data.handles.end); distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords); - return (distanceToPoint < 5); + return (distanceToPoint < 25); } function length(vector) { @@ -4641,10 +4842,17 @@ if (typeof cornerstoneTools === 'undefined') { var color; var lineWidth = cornerstoneTools.toolStyle.getToolWidth(); var font = cornerstoneTools.textStyle.getFont(); + var config = cornerstoneTools.simpleAngle.getConfiguration(); for (var i = 0; i < toolData.data.length; i++) { context.save(); + if (config && config.shadow) { + context.shadowColor = config.shadowColor || '#000000'; + context.shadowOffsetX = config.shadowOffsetX || 1; + context.shadowOffsetY = config.shadowOffsetY || 1; + } + var data = toolData.data[i]; //differentiate the color of activation tool @@ -4685,15 +4893,18 @@ if (typeof cornerstoneTools === 'undefined') { } var sideA = { - x: (Math.ceil(data.handles.middle.x) - Math.ceil(data.handles.start.x)) * columnPixelSpacing, y: (Math.ceil(data.handles.middle.y) - Math.ceil(data.handles.start.y)) * rowPixelSpacing + x: (Math.ceil(data.handles.middle.x) - Math.ceil(data.handles.start.x)) * columnPixelSpacing, + y: (Math.ceil(data.handles.middle.y) - Math.ceil(data.handles.start.y)) * rowPixelSpacing }; var sideB = { - x: (Math.ceil(data.handles.end.x) - Math.ceil(data.handles.middle.x)) * columnPixelSpacing, y: (Math.ceil(data.handles.end.y) - Math.ceil(data.handles.middle.y)) * rowPixelSpacing + x: (Math.ceil(data.handles.end.x) - Math.ceil(data.handles.middle.x)) * columnPixelSpacing, + y: (Math.ceil(data.handles.end.y) - Math.ceil(data.handles.middle.y)) * rowPixelSpacing }; var sideC = { - x: (Math.ceil(data.handles.end.x) - Math.ceil(data.handles.start.x)) * columnPixelSpacing, y: (Math.ceil(data.handles.end.y) - Math.ceil(data.handles.start.y)) * rowPixelSpacing + x: (Math.ceil(data.handles.end.x) - Math.ceil(data.handles.start.x)) * columnPixelSpacing, + y: (Math.ceil(data.handles.end.y) - Math.ceil(data.handles.start.y)) * rowPixelSpacing }; var sideALength = length(sideA); @@ -4789,7 +5000,8 @@ if (typeof cornerstoneTools === 'undefined') { // 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.simpleAngleTouch.touchMoveCallback); - $(element).off('CornerstoneToolsDragStartActive', cornerstoneTools.simpleAngleTouch.touchDownActivateCallback); + $(element).off('CornerstoneToolsTouchStartActive', cornerstoneTools.simpleAngleTouch.touchDownActivateCallback); + $(element).off('CornerstoneToolsTouchStart', cornerstoneTools.simpleAngleTouch.touchStartCallback); $(element).off('CornerstoneToolsTap', cornerstoneTools.simpleAngleTouch.tapCallback); cornerstone.updateImage(element); @@ -4811,7 +5023,8 @@ if (typeof cornerstoneTools === 'undefined') { } $(element).on('CornerstoneToolsTouchDrag', cornerstoneTools.simpleAngleTouch.touchMoveCallback); - $(element).on('CornerstoneToolsDragStartActive', cornerstoneTools.simpleAngleTouch.touchDownActivateCallback); + $(element).on('CornerstoneToolsTouchStart', cornerstoneTools.simpleAngleTouch.touchStartCallback); + $(element).on('CornerstoneToolsTouchStartActive', cornerstoneTools.simpleAngleTouch.touchDownActivateCallback); $(element).on('CornerstoneToolsTap', cornerstoneTools.simpleAngleTouch.tapCallback); cornerstone.updateImage(element); }); @@ -4855,7 +5068,10 @@ if (typeof cornerstoneTools === 'undefined') { // create the measurement data for this tool with the end handle activated var measurementData = { - visible: true, active: true, text: config.current, handles: { + visible: true, + active: true, + text: config.current, + handles: { end: { x: mouseEventData.currentPoints.image.x, y: mouseEventData.currentPoints.image.y, @@ -4921,6 +5137,7 @@ if (typeof cornerstoneTools === 'undefined') { var font = cornerstoneTools.textStyle.getFont(); var fontSize = cornerstoneTools.textStyle.getFontSize(); + var config = cornerstoneTools.textMarker.getConfiguration(); for (var i = 0; i < toolData.data.length; i++) { var data = toolData.data[i]; @@ -4932,6 +5149,12 @@ if (typeof cornerstoneTools === 'undefined') { context.save(); + if (config && config.shadow) { + context.shadowColor = config.shadowColor || '#000000'; + context.shadowOffsetX = config.shadowOffsetX || 1; + context.shadowOffsetY = config.shadowOffsetY || 1; + } + // Draw text context.font = font; context.fillStyle = color; @@ -4966,9 +5189,10 @@ if (typeof cornerstoneTools === 'undefined') { data.active = false; cornerstone.updateImage(element); - $(element).off('CornerstoneToolsMouseMove', cornerstoneTools.textMarker.mouseMoveCallback); - $(element).off('CornerstoneToolsMouseDown', cornerstoneTools.textMarker.mouseDownCallback); - $(element).off('CornerstoneToolsMouseDownActivate', cornerstoneTools.textMarker.mouseDownActivateCallback); + $(element).on('CornerstoneToolsMouseMove', eventData, cornerstoneTools.textMarker.mouseMoveCallback); + $(element).on('CornerstoneToolsMouseDown', eventData, cornerstoneTools.textMarker.mouseDownCallback); + $(element).on('CornerstoneToolsMouseDownActivate', eventData, cornerstoneTools.textMarker.mouseDownActivateCallback); + $(element).on('CornerstoneToolsMouseDoubleClick', eventData, cornerstoneTools.textMarker.mouseDoubleClickCallback); } if (e.data && e.data.mouseButtonMask && !cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) { @@ -4994,6 +5218,7 @@ if (typeof cornerstoneTools === 'undefined') { $(element).off('CornerstoneToolsMouseMove', cornerstoneTools.textMarker.mouseMoveCallback); $(element).off('CornerstoneToolsMouseDown', cornerstoneTools.textMarker.mouseDownCallback); $(element).off('CornerstoneToolsMouseDownActivate', cornerstoneTools.textMarker.mouseDownActivateCallback); + $(element).off('CornerstoneToolsMouseDoubleClick', cornerstoneTools.textMarker.mouseDoubleClickCallback); // Allow relabelling via a callback config.changeTextCallback(data, doneChangingTextCallback); @@ -5018,6 +5243,12 @@ if (typeof cornerstoneTools === 'undefined') { data.active = false; cornerstone.updateImage(element); + $(element).off('CornerstoneToolsTouchDrag', cornerstoneTools.textMarkerTouch.touchMoveCallback); + $(element).off('CornerstoneToolsTouchStartActive', cornerstoneTools.textMarkerTouch.touchDownActivateCallback); + $(element).off('CornerstoneToolsTouchStart', cornerstoneTools.textMarkerTouch.touchStartCallback); + $(element).off('CornerstoneToolsTap', cornerstoneTools.textMarkerTouch.tapCallback); + $(element).off('CornerstoneToolsTouchPress', cornerstoneTools.textMarkerTouch.pressCallback); + $(element).on('CornerstoneToolsTouchDrag', cornerstoneTools.textMarkerTouch.touchMoveCallback); $(element).on('CornerstoneToolsTouchStartActive', cornerstoneTools.textMarkerTouch.touchDownActivateCallback); $(element).on('CornerstoneToolsTouchStart', cornerstoneTools.textMarkerTouch.touchStartCallback); @@ -5125,6 +5356,7 @@ if (typeof cornerstoneTools === 'undefined') { } function touchDragCallback(e, eventData) { + e.stopImmediatePropagation(); // Prevent CornerstoneToolsTouchStartActive from killing any press events var dragData = eventData; var maxVOI = dragData.image.maxPixelValue * dragData.image.slope + dragData.image.intercept; @@ -5239,7 +5471,7 @@ if (typeof cornerstoneTools === 'undefined') { /** Calculates the minimum and maximum value in the given pixel array */ function applyWWWCRegion(eventData) { var toolData = cornerstoneTools.getToolState(eventData.element, toolType); - if (toolData === undefined) { + if (!toolData) { return; } @@ -5274,6 +5506,10 @@ if (typeof cornerstoneTools === 'undefined') { // Adjust the viewport window width and center based on the calculated values var config = cornerstoneTools.wwwcRegion.getConfiguration(); var viewport = cornerstone.getViewport(eventData.element); + if (config.minWindowWidth === undefined) { + config.minWindowWidth = 10; + } + viewport.voi.windowWidth = Math.max(Math.abs(minMaxMean.max - minMaxMean.min), config.minWindowWidth); viewport.voi.windowCenter = minMaxMean.mean; cornerstone.setViewport(eventData.element, viewport); @@ -5353,7 +5589,7 @@ if (typeof cornerstoneTools === 'undefined') { function onImageRendered(e, eventData) { var toolData = cornerstoneTools.getToolState(eventData.element, toolType); - if (toolData === undefined) { + if (!toolData) { return; } @@ -5386,10 +5622,17 @@ if (typeof cornerstoneTools === 'undefined') { var height = Math.abs(startPointCanvas.y - endPointCanvas.y); var lineWidth = cornerstoneTools.toolStyle.getToolWidth(); + var config = cornerstoneTools.wwwcRegion.getConfiguration(); // Draw the rectangle context.save(); + if (config && config.shadow) { + context.shadowColor = config.shadowColor || '#000000'; + context.shadowOffsetX = config.shadowOffsetX || 1; + context.shadowOffsetY = config.shadowOffsetY || 1; + } + context.beginPath(); context.strokeStyle = color; context.lineWidth = lineWidth; @@ -5677,15 +5920,13 @@ if (typeof cornerstoneTools === 'undefined') { var config = cornerstoneTools.zoom.getConfiguration(); var viewport = eventData.viewport; var element = eventData.element; - var scale = eventData.scaleChange; // Change the scale based on the pinch gesture's scale change - if (config.maxScale && scale > config.maxScale) { + viewport.scale += eventData.scaleChange * viewport.scale; + if (config.maxScale && viewport.scale > config.maxScale) { viewport.scale = config.maxScale; - } else if (config.minScale && scale < config.minScale) { + } else if (config.minScale && viewport.scale < config.minScale) { viewport.scale = config.minScale; - } else { - viewport.scale = scale; } cornerstone.setViewport(element, viewport); @@ -5733,7 +5974,15 @@ if (typeof cornerstoneTools === 'undefined') { e = window.event || e; // old IE support var keyPressData = { - element: element, viewport: cornerstone.getViewport(element), image: cornerstone.getEnabledElement(element).image, pageX: mouseX, pageY: mouseY, imageX: startingCoords.x, imageY: startingCoords.y, keyCode: e.keyCode, which: e.which + element: element, + viewport: cornerstone.getViewport(element), + image: cornerstone.getEnabledElement(element).image, + pageX: mouseX, + pageY: mouseY, + imageX: startingCoords.x, + imageY: startingCoords.y, + keyCode: e.keyCode, + which: e.which }; if (e.type === 'keydown') { @@ -5763,7 +6012,8 @@ if (typeof cornerstoneTools === 'undefined') { // module exports cornerstoneTools.keyboardInput = { - enable: enable, disable: disable + enable: enable, + disable: disable }; })($, cornerstone, cornerstoneTools); @@ -5779,47 +6029,68 @@ if (typeof cornerstoneTools === 'undefined') { // All credit to @kosich // https://gist.github.com/kosich/23188dd86633b6c2efb7 - var antiGhostDelay = 2000; + var antiGhostDelay = 2000, + pointerType = { + mouse: 0, + touch: 1 + }, + lastInteractionType, + lastInteractionTime; - var pointerType = { - mouse: 0, - touch: 1 - }; - - function preventGhostClick(element) { - var lastInteractionType, - lastInteractionTime; - - function handleTap(type, e) { - var now = Date.now(); - if (type !== lastInteractionType) { - if (now - lastInteractionTime <= antiGhostDelay) { - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - return false; - } - - lastInteractionType = type; + function handleTap(type, e) { + var now = Date.now(); + if (type !== lastInteractionType) { + if (now - lastInteractionTime <= antiGhostDelay) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + return false; } - lastInteractionTime = now; + lastInteractionType = type; } - function attachEvents(eventList, interactionType) { - eventList.forEach(function(eventName) { - element.addEventListener(eventName, handleTap.bind(null, interactionType), true); - }); - } - - var mouseEvents = [ 'mousedown', 'mouseup', 'mousemove' ]; - var touchEvents = [ 'touchstart', 'touchend' ]; - - attachEvents(mouseEvents, pointerType.mouse); - attachEvents(touchEvents, pointerType.touch); + lastInteractionTime = now; } - cornerstoneTools.preventGhostClick = preventGhostClick; + // Cacheing the function references + // Necessary because a new function reference is created after .bind() is called + // http://stackoverflow.com/questions/11565471/removing-event-listener-which-was-added-with-bind + var handleTapMouse = handleTap.bind(null, pointerType.mouse); + var handleTapTouch = handleTap.bind(null, pointerType.touch); + + function attachEvents(element, eventList, interactionType) { + var tapHandler = interactionType ? handleTapMouse : handleTapTouch; + eventList.forEach(function(eventName) { + element.addEventListener(eventName, tapHandler, true); + }); + } + + function removeEvents(element, eventList, interactionType) { + var tapHandler = interactionType ? handleTapMouse : handleTapTouch; + eventList.forEach(function(eventName) { + element.removeEventListener(eventName, tapHandler, true); + }); + } + + var mouseEvents = [ 'mousedown', 'mouseup' ]; + var touchEvents = [ 'touchstart', 'touchend' ]; + + function disable(element) { + removeEvents(element, mouseEvents, pointerType.mouse); + removeEvents(element, touchEvents, pointerType.touch); + } + + function enable(element) { + disable(element); + attachEvents(element, mouseEvents, pointerType.mouse); + attachEvents(element, touchEvents, pointerType.touch); + } + + cornerstoneTools.preventGhostClick = { + enable: enable, + disable: disable + }; })(cornerstoneTools); @@ -5870,6 +6141,10 @@ if (typeof cornerstoneTools === 'undefined') { Object.keys(handles).forEach(function(name) { var handle = handles[name]; + if (handle.drawnIndependently === true) { + return; + } + context.beginPath(); if (handle.active) { @@ -5902,20 +6177,27 @@ if (typeof cornerstoneTools === 'undefined') { 'use strict'; - function getHandleNearImagePoint(element, data, coords, distanceSq) { + function getHandleNearImagePoint(element, handles, coords, distanceThreshold) { var nearbyHandle; - if (!data.handles) { + if (!handles) { return; } - Object.keys(data.handles).forEach(function(name) { - var handle = data.handles[name]; - var handleCanvas = cornerstone.pixelToCanvas(element, handle); - var distanceSquared = cornerstoneMath.point.distanceSquared(handleCanvas, coords); - if (distanceSquared < distanceSq) { - nearbyHandle = handle; - return; + Object.keys(handles).forEach(function(name) { + var handle = handles[name]; + if (handle.hasOwnProperty('pointNearHandle')) { + if (handle.pointNearHandle(element, handle, coords)) { + nearbyHandle = handle; + return; + } + } else { + var handleCanvas = cornerstone.pixelToCanvas(element, handle); + var distance = cornerstoneMath.point.distance(handleCanvas, coords); + if (distance <= distanceThreshold) { + nearbyHandle = handle; + return; + } } }); @@ -5934,22 +6216,6 @@ if (typeof cornerstoneTools === 'undefined') { 'use strict'; - function findHandleNear(element, handles, canvasPoint, distanceThreshold) { - var nearbyHandle; - - Object.keys(handles).forEach(function(name) { - var handle = handles[name]; - var handleCanvas = cornerstone.pixelToCanvas(element, handle); - var distance = cornerstoneMath.point.distance(handleCanvas, canvasPoint); - if (distance <= distanceThreshold) { - nearbyHandle = handle; - return; - } - }); - - return nearbyHandle; - } - function getActiveHandle(handles) { var activeHandle; @@ -5970,7 +6236,7 @@ if (typeof cornerstoneTools === 'undefined') { } var activeHandle = getActiveHandle(handles); - var nearbyHandle = findHandleNear(element, handles, canvasPoint, distanceThreshold); + var nearbyHandle = cornerstoneTools.getHandleNearImagePoint(element, handles, canvasPoint, distanceThreshold); if (activeHandle !== nearbyHandle) { if (nearbyHandle !== undefined) { nearbyHandle.active = true; @@ -6006,6 +6272,10 @@ if (typeof cornerstoneTools === 'undefined') { Object.keys(data.handles).forEach(function(name) { var handle = data.handles[name]; + if (handle.movesIndependently === true) { + return; + } + handle.x += eventData.deltaPoints.image.x; handle.y += eventData.deltaPoints.image.y; @@ -6036,7 +6306,10 @@ if (typeof cornerstoneTools === 'undefined') { var image = eventData.image; var handleOutsideImage = false; var rect = { - top: 0, left: 0, width: image.width, height: image.height + top: 0, + left: 0, + width: image.width, + height: image.height }; Object.keys(data.handles).forEach(function(name) { @@ -6065,7 +6338,10 @@ if (typeof cornerstoneTools === 'undefined') { } cornerstone.updateImage(element); - doneMovingCallback(); + + if (typeof doneMovingCallback === 'function') { + doneMovingCallback(); + } } $(element).on('CornerstoneToolsMouseUp', mouseUpCallback); @@ -6073,94 +6349,8 @@ if (typeof cornerstoneTools === 'undefined') { return true; } - //TODO:moveTextHandles-ayselafsar - function moveTextHandles(e, data, toolData, deleteIfHandleOutsideImage, preventHandleOutsideImage) - { - var mouseEventData = e; - var element = mouseEventData.element; - - function mouseDragCallback(e, eventData) - { - for(var property in data.handles) { - var handle = data.linkedTextCoords[property]; - handle.x += eventData.deltaPoints.image.x; - handle.y += eventData.deltaPoints.image.y; - if (preventHandleOutsideImage) - { - if (handle.x < 0) - { - handle.x = 0; - } - if (handle.x > eventData.image.width) - { - handle.x = eventData.image.width; - } - if (handle.y < 0) - { - handle.y = 0; - } - if (handle.y > eventData.image.height) - { - handle.y = eventData.image.height; - } - } - } - cornerstone.updateImage(element); - return false; // false = cases jquery to preventDefault() and stopPropagation() this event - } - - $(element).on("CornerstoneToolsMouseDrag", mouseDragCallback); - - function mouseUpCallback(e, eventData) { - data.moving = false; - - $(element).off('CornerstoneToolsMouseDrag', mouseDragCallback); - $(element).off('CornerstoneToolsMouseUp', mouseUpCallback); - - // If any handle is outside the image, delete the tool data - - if(deleteIfHandleOutsideImage === true) { - var image = eventData.image;//.getEnabledElement(element).image; - var handleOutsideImage = false; - var rect = { - top: 0, - left: 0, - width: image.width, - height: image.height - }; - for(var property in data.handles) { - var handle = data.handles[property]; - if(cornerstoneMath.point.insideRect(handle, rect) === false) - { - handleOutsideImage = true; - } - } - - if(handleOutsideImage) - { - // find this tool data - var indexOfData = -1; - for(var i = 0; i < toolData.data.length; i++) { - if(toolData.data[i] === data) - { - indexOfData = i; - } - } - if(indexOfData !== -1) { - toolData.data.splice(indexOfData, 1); - } - } - } - cornerstone.updateImage(element); - } - $(element).on("CornerstoneToolsMouseUp",mouseUpCallback); - return true; - } - - // module/private exports cornerstoneTools.moveAllHandles = moveAllHandles; - cornerstoneTools.moveTextHandles = moveTextHandles; })($, cornerstone, cornerstoneMath, cornerstoneTools); @@ -6173,11 +6363,16 @@ if (typeof cornerstoneTools === 'undefined') { function moveHandle(mouseEventData, handle, doneMovingCallback, preventHandleOutsideImage) { var element = mouseEventData.element; + var distanceFromTool = { + x: handle.x - mouseEventData.currentPoints.image.x, + y: handle.y - mouseEventData.currentPoints.image.y + }; function mouseDragCallback(e, eventData) { handle.active = true; - handle.x = eventData.currentPoints.image.x; - handle.y = eventData.currentPoints.image.y; + handle.x = eventData.currentPoints.image.x + distanceFromTool.x; + handle.y = eventData.currentPoints.image.y + distanceFromTool.y; + if (preventHandleOutsideImage) { handle.x = Math.max(handle.x, 0); handle.x = Math.min(handle.x, eventData.image.width); @@ -6198,7 +6393,9 @@ if (typeof cornerstoneTools === 'undefined') { $(element).off('CornerstoneToolsMouseClick', mouseUpCallback); cornerstone.updateImage(element); - doneMovingCallback(); + if (typeof doneMovingCallback === 'function') { + doneMovingCallback(); + } } $(element).on('CornerstoneToolsMouseUp', mouseUpCallback); @@ -6217,7 +6414,7 @@ if (typeof cornerstoneTools === 'undefined') { 'use strict'; - function moveNewHandle(mouseEventData, handle, doneMoveCallback, preventHandleOutsideImage) { + function moveNewHandle(mouseEventData, handle, doneMovingCallback, preventHandleOutsideImage) { var element = mouseEventData.element; function moveCallback(e, eventData) { @@ -6261,7 +6458,9 @@ if (typeof cornerstoneTools === 'undefined') { handle.active = false; cornerstone.updateImage(element); - doneMoveCallback(); + if (typeof doneMovingCallback === 'function') { + doneMovingCallback(); + } } } @@ -6277,11 +6476,12 @@ if (typeof cornerstoneTools === 'undefined') { 'use strict'; - function moveNewHandleTouch(eventData, handle, doneMoveCallback, preventHandleOutsideImage) { + function moveNewHandleTouch(eventData, handle, doneMovingCallback, preventHandleOutsideImage) { var element = eventData.element; + var imageCoords = cornerstone.pageToPixel(element, eventData.currentPoints.page.x, eventData.currentPoints.page.y + 50); var distanceFromTouch = { - x: handle.x - eventData.currentPoints.image.x, - y: handle.y - eventData.currentPoints.image.y + x: handle.x - imageCoords.x, + y: handle.y - imageCoords.y }; function moveCallback(e, eventData) { @@ -6302,10 +6502,19 @@ if (typeof cornerstoneTools === 'undefined') { function moveEndCallback(e, eventData) { $(element).off('CornerstoneToolsTouchDrag', moveCallback); + $(element).off('CornerstoneToolsTouchPinch', moveEndCallback); + $(element).off('CornerstoneToolsTouchPress', moveEndCallback); $(element).off('CornerstoneToolsTouchEnd', moveEndCallback); $(element).off('CornerstoneToolsDragEnd', moveEndCallback); $(element).off('CornerstoneToolsTap', moveEndCallback); + if (e.type === 'CornerstoneToolsTouchPinch' || e.type === 'CornerstoneToolsTouchPress') { + handle.active = false; + cornerstone.updateImage(element); + doneMovingCallback(); + return; + } + handle.active = false; handle.x = eventData.currentPoints.image.x + distanceFromTouch.x; handle.y = eventData.currentPoints.image.y + distanceFromTouch.y; @@ -6320,10 +6529,14 @@ if (typeof cornerstoneTools === 'undefined') { cornerstone.updateImage(element); - doneMoveCallback(); + if (typeof doneMovingCallback === 'function') { + doneMovingCallback(); + } } $(element).on('CornerstoneToolsTouchDrag', moveCallback); + $(element).on('CornerstoneToolsTouchPinch', moveEndCallback); + $(element).on('CornerstoneToolsTouchPress', moveEndCallback); $(element).on('CornerstoneToolsTouchEnd', moveEndCallback); $(element).on('CornerstoneToolsDragEnd', moveEndCallback); $(element).on('CornerstoneToolsTap', moveEndCallback); @@ -6342,13 +6555,19 @@ if (typeof cornerstoneTools === 'undefined') { 'use strict'; function touchMoveAllHandles(touchEventData, data, toolData, deleteIfHandleOutsideImage, doneMovingCallback) { + //console.log('touchMoveAllHandles'); var element = touchEventData.element; function touchDragCallback(e, eventData) { + //console.log('touchMoveAllHandles touchDragCallback'); data.active = true; Object.keys(data.handles).forEach(function(name) { var handle = data.handles[name]; + if (handle.movesIndependently === true) { + return; + } + handle.x += eventData.deltaPoints.image.x; handle.y += eventData.deltaPoints.image.y; }); @@ -6359,10 +6578,14 @@ if (typeof cornerstoneTools === 'undefined') { $(element).on('CornerstoneToolsTouchDrag', touchDragCallback); function touchEndCallback(e, eventData) { + //console.log('touchMoveAllHandles touchEndCallback'); data.active = false; data.invalidated = false; $(element).off('CornerstoneToolsTouchDrag', touchDragCallback); + $(element).off('CornerstoneToolsTouchPinch', touchEndCallback); + $(element).off('CornerstoneToolsTouchPress', touchEndCallback); + $(element).off('CornerstoneToolsTouchEnd', touchEndCallback); $(element).off('CornerstoneToolsDragEnd', touchEndCallback); $(element).off('CornerstoneToolsTap', touchEndCallback); @@ -6371,7 +6594,10 @@ if (typeof cornerstoneTools === 'undefined') { var image = eventData.image; var handleOutsideImage = false; var rect = { - top: 0, left: 0, width: image.width, height: image.height + top: 0, + left: 0, + width: image.width, + height: image.height }; Object.keys(data.handles).forEach(function(name) { @@ -6397,10 +6623,16 @@ if (typeof cornerstoneTools === 'undefined') { } } - doneMovingCallback(); + if (typeof doneMovingCallback === 'function') { + doneMovingCallback(); + } + cornerstone.updateImage(element); } + $(element).on('CornerstoneToolsTouchPinch', touchEndCallback); + $(element).on('CornerstoneToolsTouchPress', touchEndCallback); + $(element).on('CornerstoneToolsTouchEnd', touchEndCallback); $(element).on('CornerstoneToolsDragEnd', touchEndCallback); $(element).on('CornerstoneToolsTap', touchEndCallback); return true; @@ -6419,6 +6651,7 @@ if (typeof cornerstoneTools === 'undefined') { 'use strict'; function touchMoveHandle(touchEventData, handle, doneMovingCallback) { + //console.log('touchMoveHandle'); var element = touchEventData.element; var distanceFromTouch = { x: handle.x - touchEventData.currentPoints.image.x, @@ -6438,14 +6671,20 @@ if (typeof cornerstoneTools === 'undefined') { function touchEndCallback() { handle.active = false; $(element).off('CornerstoneToolsTouchDrag', touchDragCallback); + $(element).off('CornerstoneToolsTouchPinch', touchEndCallback); + $(element).off('CornerstoneToolsTouchPress', touchEndCallback); $(element).off('CornerstoneToolsTouchEnd', touchEndCallback); $(element).off('CornerstoneToolsDragEnd', touchEndCallback); $(element).off('CornerstoneToolsTap', touchEndCallback); cornerstone.updateImage(element); - doneMovingCallback(); + if (typeof doneMovingCallback === 'function') { + doneMovingCallback(); + } } + $(element).on('CornerstoneToolsTouchPinch', touchEndCallback); + $(element).on('CornerstoneToolsTouchPress', touchEndCallback); $(element).on('CornerstoneToolsTouchEnd', touchEndCallback); $(element).on('CornerstoneToolsDragEnd', touchEndCallback); $(element).on('CornerstoneToolsTap', touchEndCallback); @@ -6500,7 +6739,8 @@ if (typeof cornerstoneTools === 'undefined') { var index = that.measurements.push(measurement); // fire event var eventDetail = { - index: index, measurement: measurement + index: index, + measurement: measurement }; $(that).trigger('CornerstoneMeasurementAdded', eventDetail); }; @@ -6510,7 +6750,8 @@ if (typeof cornerstoneTools === 'undefined') { that.measurements.splice(index, 1); // fire event var eventDetail = { - index: index, measurement: measurement + index: index, + measurement: measurement }; $(that).trigger('CornerstoneMeasurementRemoved', eventDetail); }; @@ -6705,7 +6946,8 @@ if (typeof cornerstoneTools === 'undefined') { renderer = renderer || cornerstoneTools.referenceLines.renderActiveReferenceLine; cornerstoneTools.addToolState(element, toolType, { - synchronizationContext: synchronizationContext, renderer: renderer + synchronizationContext: synchronizationContext, + renderer: renderer }); $(element).on('CornerstoneImageRendered', onImageRendered); cornerstone.updateImage(element); @@ -6719,7 +6961,8 @@ if (typeof cornerstoneTools === 'undefined') { // module/private exports cornerstoneTools.referenceLines.tool = { - enable: enable, disable: disable + enable: enable, + disable: disable }; @@ -6745,6 +6988,11 @@ if (typeof cornerstoneTools === 'undefined') { var targetImagePlane = cornerstoneTools.metaData.get('imagePlane', targetImage.imageId); var referenceImagePlane = cornerstoneTools.metaData.get('imagePlane', referenceImage.imageId); + // Make sure the target and reference actually have image plane metadata + if (!targetImagePlane || !referenceImagePlane) { + return; + } + // the image planes must be in the same frame of reference if (targetImagePlane.frameOfReferenceUID !== referenceImagePlane.frameOfReferenceUID) { return; @@ -7030,7 +7278,11 @@ if (typeof cornerstoneTools === 'undefined') { if (playClipToolData === undefined || playClipToolData.data.length === 0) { playClipData = { - intervalId: undefined, framesPerSecond: framesPerSecond, lastFrameTimeStamp: undefined, frameRate: 0, loop: true + intervalId: undefined, + framesPerSecond: framesPerSecond, + lastFrameTimeStamp: undefined, + frameRate: 0, + loop: true }; cornerstoneTools.addToolState(element, toolType, playClipData); } else { @@ -7451,6 +7703,7 @@ Display scroll progress bar across bottom of image. } function disable(element) { + clearTimeout(resetPrefetchTimeout); $(element).off('CornerstoneNewImage', onImageUpdated); $(cornerstone).off('CornerstoneImageCacheFull', handleCacheFull); @@ -7565,7 +7818,8 @@ Display scroll progress bar across bottom of image. 'use strict'; var keys = { - UP: 38, DOWN: 40 + UP: 38, + DOWN: 40 }; function keyDownCallback(e, eventData) { @@ -8128,7 +8382,8 @@ Display scroll progress bar across bottom of image. } var imageIdToolStateManager = { - get: getStackSpecificToolState, add: addStackSpecificToolState + get: getStackSpecificToolState, + add: addStackSpecificToolState }; return imageIdToolStateManager; } @@ -8225,7 +8480,8 @@ Display scroll progress bar across bottom of image. } var toolCoords = { - setCoords: setActiveToolCoords, getCoords: getActiveToolCoords + setCoords: setActiveToolCoords, + getCoords: getActiveToolCoords }; return toolCoords; @@ -8364,7 +8620,7 @@ Display scroll progress bar across bottom of image. function touchSettingsManager() { var defaultDistanceFromTouch = { x: 0, - y: -45 + y: -55 }; function setToolDistanceFromTouch(distance){ @@ -8734,8 +8990,16 @@ Display scroll progress bar across bottom of image. sourceElements.forEach(function(sourceElement) { var sourceEnabledElement = cornerstone.getEnabledElement(sourceElement); + if (!sourceEnabledElement || !sourceEnabledElement.image) { + return; + } + var sourceImageId = sourceEnabledElement.image.imageId; var sourceImagePlane = cornerstoneTools.metaData.get('imagePlane', sourceImageId); + if (!sourceImagePlane || !sourceImagePlane.imagePositionPatient) { + return; + } + var sourceImagePosition = sourceImagePlane.imagePositionPatient; if (initialData.hasOwnProperty(sourceEnabledElement)) { @@ -8748,6 +9012,10 @@ Display scroll progress bar across bottom of image. targetElements.forEach(function(targetElement) { var targetEnabledElement = cornerstone.getEnabledElement(targetElement); + if (!targetEnabledElement || !targetEnabledElement.image) { + return; + } + var targetImageId = targetEnabledElement.image.imageId; initialData.imageIds.targetElements.push(targetImageId); @@ -8765,6 +9033,10 @@ Display scroll progress bar across bottom of image. } var targetImagePlane = cornerstoneTools.metaData.get('imagePlane', targetImageId); + if (!targetImagePlane || !targetImagePlane.imagePositionPatient) { + return; + } + var targetImagePosition = targetImagePlane.imagePositionPatient; initialData.distances[sourceImageId][targetImageId] = targetImagePosition.clone().sub(sourceImagePosition); @@ -9050,7 +9322,11 @@ Display scroll progress bar across bottom of image. // create the measurement data for this tool with the end handle activated var measurementData = { - timeSeries: timeSeries, lineSample: new cornerstoneTools.LineSampleMeasurement(), imageIdIndex: timeSeries.stacks[timeSeries.currentStackIndex].currentImageIdIndex, visible: true, handles: { + timeSeries: timeSeries, + lineSample: new cornerstoneTools.LineSampleMeasurement(), + imageIdIndex: timeSeries.stacks[timeSeries.currentStackIndex].currentImageIdIndex, + visible: true, + handles: { end: { x: mouseEventData.currentPoints.image.x, y: mouseEventData.currentPoints.image.y, @@ -9110,7 +9386,9 @@ Display scroll progress bar across bottom of image. // module exports cornerstoneTools.probeTool4D = cornerstoneTools.mouseButtonTool({ - createNewMeasurement: createNewMeasurement, onImageRendered: onImageRendered, toolType: toolType + createNewMeasurement: createNewMeasurement, + onImageRendered: onImageRendered, + toolType: toolType }); })($, cornerstone, cornerstoneTools); @@ -9216,7 +9494,10 @@ Display scroll progress bar across bottom of image. var playClipData; if (playClipToolData === undefined || playClipToolData.data.length === 0) { playClipData = { - intervalId: undefined, framesPerSecond: framesPerSecond, lastFrameTimeStamp: undefined, frameRate: 0 + intervalId: undefined, + framesPerSecond: framesPerSecond, + lastFrameTimeStamp: undefined, + frameRate: 0 }; cornerstoneTools.addToolState(element, toolType, playClipData); } else { @@ -9257,7 +9538,8 @@ Display scroll progress bar across bottom of image. // module/private exports cornerstoneTools.timeSeriesPlayer = { - start: playClip, stop: stopClip + start: playClip, + stop: stopClip }; })($, cornerstone, cornerstoneTools); @@ -9431,7 +9713,10 @@ Display scroll progress bar across bottom of image. var client = cornerstoneMath.point.copy(points.client); var canvas = cornerstoneMath.point.copy(points.canvas); return { - page: page, image: image, client: client, canvas: canvas + page: page, + image: image, + client: client, + canvas: canvas }; } @@ -9479,28 +9764,54 @@ Display scroll progress bar across bottom of image. 'use strict'; - function drawTextBox(context, text, x, y, color) { + function drawTextBox(context, textLines, x, y, color) { + if (Object.prototype.toString.call(textLines) !== '[object Array]') { + textLines = [ textLines ]; + } + var padding = 5, font = cornerstoneTools.textStyle.getFont(), fontSize = cornerstoneTools.textStyle.getFontSize(), backgroundColor = cornerstoneTools.textStyle.getBackgroundColor(); context.save(); - - // Get the text width in the current font context.font = font; - var width = context.measureText(text).width; + context.textBaseline = 'top'; + context.strokeStyle = color; + + // Find the longest text width in the array of text data + var maxWidth = 0; + textLines.forEach(function(text) { + // Get the text width in the current font + var width = context.measureText(text).width; + + // Find the maximum with for all the text rows; + maxWidth = Math.max(maxWidth, width); + }); // Draw the background box with padding - context.textBaseline = 'top'; context.fillStyle = backgroundColor; - context.fillRect(x, y - fontSize, width + (padding * 2), fontSize + (padding * 2)); - - // Draw the text - context.fillStyle = color; - context.fillText(text, x + padding, y - fontSize + padding); - + + // Calculate the bounding box for this text box + var boundingBox = { + left: x, + top: y, + width: maxWidth + (padding * 2), + height: textLines.length * (fontSize + (padding * 2)) + }; + context.fillRect(boundingBox.left, boundingBox.top, boundingBox.width, boundingBox.height); + + // Draw each of the text lines on top of the background box + textLines.forEach(function(text, index) { + context.fillStyle = color; + context.fillText(text, x + padding, y + fontSize * index + padding); + + }); + context.restore(); + + // Return the bounding box so it can be used for pointNearHandle + return boundingBox; } // module exports @@ -9757,7 +10068,8 @@ Display scroll progress bar across bottom of image. var x = imagePlane.rowCosines.dot(point) / imagePlane.columnPixelSpacing; var y = imagePlane.columnCosines.dot(point) / imagePlane.rowPixelSpacing; return { - x: x, y: y + x: x, + y: y }; } @@ -10008,7 +10320,9 @@ Display scroll progress bar across bottom of image. // TODO: actually calculate this? var lineHeight = fontSize / ee.viewport.scale / fontScale; return { - fontSize: scaledFontSize, lineHeight: lineHeight, fontScale: fontScale + fontSize: scaledFontSize, + lineHeight: lineHeight, + fontScale: fontScale }; } diff --git a/Packages/cornerstone/client/dicomParser.js b/Packages/cornerstone/client/dicomParser.js index bf6230245..67f934eb0 100644 --- a/Packages/cornerstone/client/dicomParser.js +++ b/Packages/cornerstone/client/dicomParser.js @@ -1,183 +1,160 @@ -/*! dicom-parser - v1.1.5 - 2015-08-17 | (c) 2014 Chris Hafey | https://github.com/chafey/dicomParser */ +/*! dicom-parser - v1.2.0 - 2015-11-02 | (c) 2014 Chris Hafey | https://github.com/chafey/dicomParser */ (function (root, factory) { - // node.js - if (typeof module !== 'undefined' && module.exports) { - module.exports = factory(); - } - else if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define([], factory); - } else { - // Browser globals - if(typeof cornerstone === 'undefined'){ - dicomParser = {}; - - // meteor - if (typeof Package !== 'undefined') { - root.dicomParser = dicomParser; - } + // node.js + if (typeof module !== 'undefined' && module.exports) { + module.exports = factory(); + } + else if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + } else { + // Browser globals + if(typeof cornerstone === 'undefined'){ + dicomParser = {}; + + // meteor + if (typeof Package !== 'undefined') { + root.dicomParser = dicomParser; + } + } + dicomParser = factory(); } - dicomParser = factory(); - } }(this, function () { - /** - * Parses a DICOM P10 byte array and returns a DataSet object with the parsed elements. If the options - * argument is supplied and it contains the untilTag property, parsing will stop once that - * tag is encoutered. This can be used to parse partial byte streams. - * - * @param byteArray the byte array - * @param options object to control parsing behavior (optional) - * @returns {DataSet} - * @throws error if an error occurs while parsing. The exception object will contain a property dataSet with the - * elements successfully parsed before the error. - */ - var dicomParser = (function(dicomParser) { + /** + * Parses a DICOM P10 byte array and returns a DataSet object with the parsed elements. If the options + * argument is supplied and it contains the untilTag property, parsing will stop once that + * tag is encoutered. This can be used to parse partial byte streams. + * + * @param byteArray the byte array + * @param options object to control parsing behavior (optional) + * @returns {DataSet} + * @throws error if an error occurs while parsing. The exception object will contain a property dataSet with the + * elements successfully parsed before the error. + */ +var dicomParser = (function(dicomParser) { if(dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } dicomParser.parseDicom = function(byteArray, options) { - if(byteArray === undefined) - { - throw "dicomParser.parseDicom: missing required parameter 'byteArray'"; - } - - var littleEndianByteStream = new dicomParser.ByteStream(dicomParser.littleEndianByteArrayParser, byteArray); - - function readPrefix() - { - littleEndianByteStream.seek(128); - var prefix = littleEndianByteStream.readFixedString(4); - if(prefix !== "DICM") + if(byteArray === undefined) { - throw "dicomParser.parseDicom: DICM prefix not found at location 132"; + throw "dicomParser.parseDicom: missing required parameter 'byteArray'"; } - } - function readPart10Header() - { - // Per the DICOM standard, the header is always encoded in Explicit VR Little Endian (see PS3.10, section 7.1) - // so use littleEndianByteStream throughout this method regardless of the transfer syntax - readPrefix(); - - var warnings = []; - var elements = {}; - while(littleEndianByteStream.position < littleEndianByteStream.byteArray.length) { - var position = littleEndianByteStream.position; - var element = dicomParser.readDicomElementExplicit(littleEndianByteStream, warnings); - if(element.tag > 'x0002ffff') { - littleEndianByteStream.position = position; - break; - } - // Cache the littleEndianByteArrayParser for meta header elements, since the rest of the data set may be big endian - // and this parser will be needed later if the meta header values are to be read. - element.parser = dicomParser.littleEndianByteArrayParser; - elements[element.tag] = element; + function readTransferSyntax(metaHeaderDataSet) { + if(metaHeaderDataSet.elements.x00020010 === undefined) { + throw 'dicomParser.parseDicom: missing required meta header attribute 0002,0010'; + } + var transferSyntaxElement = metaHeaderDataSet.elements.x00020010; + return dicomParser.readFixedString(byteArray, transferSyntaxElement.dataOffset, transferSyntaxElement.length); } - var metaHeaderDataSet = new dicomParser.DataSet(littleEndianByteStream.byteArrayParser, littleEndianByteStream.byteArray, elements); - metaHeaderDataSet.warnings = littleEndianByteStream.warnings; - return metaHeaderDataSet; - } - function readTransferSyntax(metaHeaderDataSet) { - if(metaHeaderDataSet.elements.x00020010 === undefined) { - throw 'dicomParser.parseDicom: missing required meta header attribute 0002,0010'; + function isExplicit(transferSyntax) { + if(transferSyntax === '1.2.840.10008.1.2') // implicit little endian + { + return false; + } + // all other transfer syntaxes should be explicit + return true; } - var transferSyntaxElement = metaHeaderDataSet.elements.x00020010; - return dicomParser.readFixedString(littleEndianByteStream.byteArray, transferSyntaxElement.dataOffset, transferSyntaxElement.length); - } - function isExplicit(transferSyntax) { - if(transferSyntax === '1.2.840.10008.1.2') // implicit little endian + function getDataSetByteStream(transferSyntax, position) { + if(transferSyntax === '1.2.840.10008.1.2.1.99') + { + if(typeof(pako) === "undefined") { + throw 'dicomParser.parseDicom: deflated transfer syntax encountered but pako not loaded'; + } + var deflated = byteArray.slice(position); + var inflated = pako.inflateRaw(deflated); + + // create a single byte array with the full header bytes and the inflated bytes + var fullByteArray = new Uint8Array(inflated.length + position); + fullByteArray.set(byteArray.slice(0, position), 0); + fullByteArray.set(inflated, position); + return new dicomParser.ByteStream(dicomParser.littleEndianByteArrayParser, fullByteArray, 0); + } + if(transferSyntax === '1.2.840.10008.1.2.2') // explicit big endian + { + return new dicomParser.ByteStream(dicomParser.bigEndianByteArrayParser, byteArray, position); + } + else + { + // all other transfer syntaxes are little endian; only the pixel encoding differs + // make a new stream so the metaheader warnings don't come along for the ride + return new dicomParser.ByteStream(dicomParser.littleEndianByteArrayParser, byteArray, position); + } + } + + function mergeDataSets(metaHeaderDataSet, instanceDataSet) { - return false; + for (var propertyName in metaHeaderDataSet.elements) + { + if(metaHeaderDataSet.elements.hasOwnProperty(propertyName)) + { + instanceDataSet.elements[propertyName] = metaHeaderDataSet.elements[propertyName]; + } + } + if (metaHeaderDataSet.warnings !== undefined) { + instanceDataSet.warnings = metaHeaderDataSet.warnings.concat(instanceDataSet.warnings); + } + return instanceDataSet; } - // all other transfer syntaxes should be explicit - return true; - } - function getDataSetByteStream(transferSyntax) { - if(transferSyntax === '1.2.840.10008.1.2.2') // explicit big endian + function readDataSet(metaHeaderDataSet) { - return new dicomParser.ByteStream(dicomParser.bigEndianByteArrayParser, byteArray, littleEndianByteStream.position); + var transferSyntax = readTransferSyntax(metaHeaderDataSet); + var explicit = isExplicit(transferSyntax); + var dataSetByteStream = getDataSetByteStream(transferSyntax, metaHeaderDataSet.position); + + var elements = {}; + var dataSet = new dicomParser.DataSet(dataSetByteStream.byteArrayParser, dataSetByteStream.byteArray, elements); + dataSet.warnings = dataSetByteStream.warnings; + + try{ + if(explicit) { + dicomParser.parseDicomDataSetExplicit(dataSet, dataSetByteStream, dataSetByteStream.byteArray.length, options); + } + else + { + dicomParser.parseDicomDataSetImplicit(dataSet, dataSetByteStream, dataSetByteStream.byteArray.length, options); + } + } + catch(e) { + var ex = { + exception: e, + dataSet: dataSet + }; + throw ex; + } + return dataSet; } - else - { - // all other transfer syntaxes are little endian; only the pixel encoding differs - // make a new stream so the metaheader warnings don't come along for the ride - return new dicomParser.ByteStream(dicomParser.littleEndianByteArrayParser, byteArray, littleEndianByteStream.position); + + // main function here + function parseTheByteStream() { + var metaHeaderDataSet = dicomParser.readPart10Header(byteArray, options); + + var dataSet = readDataSet(metaHeaderDataSet); + + return mergeDataSets(metaHeaderDataSet, dataSet); } - } - function mergeDataSets(metaHeaderDataSet, instanceDataSet) - { - for (var propertyName in metaHeaderDataSet.elements) - { - if(metaHeaderDataSet.elements.hasOwnProperty(propertyName)) - { - instanceDataSet.elements[propertyName] = metaHeaderDataSet.elements[propertyName]; - } - } - if (metaHeaderDataSet.warnings !== undefined) { - instanceDataSet.warnings = metaHeaderDataSet.warnings.concat(instanceDataSet.warnings); - } - return instanceDataSet; - } - - function readDataSet(metaHeaderDataSet) - { - var transferSyntax = readTransferSyntax(metaHeaderDataSet); - var explicit = isExplicit(transferSyntax); - var dataSetByteStream = getDataSetByteStream(transferSyntax); - - var elements = {}; - var dataSet = new dicomParser.DataSet(dataSetByteStream.byteArrayParser, dataSetByteStream.byteArray, elements); - dataSet.warnings = dataSetByteStream.warnings; - - try{ - if(explicit) { - dicomParser.parseDicomDataSetExplicit(dataSet, dataSetByteStream, dataSetByteStream.byteArray.length, options); - } - else - { - dicomParser.parseDicomDataSetImplicit(dataSet, dataSetByteStream, dataSetByteStream.byteArray.length, options); - } - } - catch(e) { - var ex = { - exception: e, - dataSet: dataSet - }; - throw ex; - } - return dataSet; - } - - // main function here - function parseTheByteStream() { - var metaHeaderDataSet = readPart10Header(); - - var dataSet = readDataSet(metaHeaderDataSet); - - return mergeDataSets(metaHeaderDataSet, dataSet); - } - - // This is where we actually start parsing - return parseTheByteStream(); + // This is where we actually start parsing + return parseTheByteStream(); }; return dicomParser; - })(dicomParser); +})(dicomParser); - var dicomParser = (function (dicomParser) { +var dicomParser = (function (dicomParser) { "use strict"; if (dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } /** @@ -187,64 +164,64 @@ */ dicomParser.explicitDataSetToJS = function (dataSet, options) { - if(dataSet === undefined) { - throw 'dicomParser.explicitDataSetToJS: missing required parameter dataSet'; - } - - options = options || { - omitPrivateAttibutes: true, // true if private elements should be omitted - maxElementLength : 128 // maximum element length to try and convert to string format - }; - - var result = { - - }; - - for(var tag in dataSet.elements) { - var element = dataSet.elements[tag]; - - // skip this element if it a private element and our options specify that we should - if(options.omitPrivateAttibutes === true && dicomParser.isPrivateTag(tag)) - { - continue; + if(dataSet === undefined) { + throw 'dicomParser.explicitDataSetToJS: missing required parameter dataSet'; } - if(element.items) { - // handle sequences - var sequenceItems = []; - for(var i=0; i < element.items.length; i++) { - sequenceItems.push(dicomParser.explicitDataSetToJS(element.items[i].dataSet, options)); - } - result[tag] = sequenceItems; - } else { - var asString; - asString = undefined; - if(element.length < options.maxElementLength) { - asString = dicomParser.explicitElementToString(dataSet, element); - } + options = options || { + omitPrivateAttibutes: true, // true if private elements should be omitted + maxElementLength : 128 // maximum element length to try and convert to string format + }; - if(asString !== undefined) { - result[tag] = asString; - } else { - result[tag] = { - dataOffset: element.dataOffset, - length : element.length - }; - } + var result = { + + }; + + for(var tag in dataSet.elements) { + var element = dataSet.elements[tag]; + + // skip this element if it a private element and our options specify that we should + if(options.omitPrivateAttibutes === true && dicomParser.isPrivateTag(tag)) + { + continue; + } + + if(element.items) { + // handle sequences + var sequenceItems = []; + for(var i=0; i < element.items.length; i++) { + sequenceItems.push(dicomParser.explicitDataSetToJS(element.items[i].dataSet, options)); + } + result[tag] = sequenceItems; + } else { + var asString; + asString = undefined; + if(element.length < options.maxElementLength) { + asString = dicomParser.explicitElementToString(dataSet, element); + } + + if(asString !== undefined) { + result[tag] = asString; + } else { + result[tag] = { + dataOffset: element.dataOffset, + length : element.length + }; + } + } } - } - return result; + return result; }; return dicomParser; - }(dicomParser)); - var dicomParser = (function (dicomParser) { +}(dicomParser)); +var dicomParser = (function (dicomParser) { "use strict"; if (dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } /** @@ -256,244 +233,244 @@ */ dicomParser.explicitElementToString = function(dataSet, element) { - if(dataSet === undefined || element === undefined) { - throw 'dicomParser.explicitElementToString: missing required parameters'; - } - if(element.vr === undefined) { - throw 'dicomParser.explicitElementToString: cannot convert implicit element to string'; - } - var vr = element.vr; - var tag = element.tag; - - var textResult; - - function multiElementToString(numItems, func) { - var result = ""; - for(var i=0; i < numItems; i++) { - if(i !== 0) { - result += '/'; - } - result += func.call(dataSet, tag).toString(); + if(dataSet === undefined || element === undefined) { + throw 'dicomParser.explicitElementToString: missing required parameters'; } - return result; - } - - if(dicomParser.isStringVr(vr) === true) - { - textResult = dataSet.string(tag); - } - else if (vr == 'AT') { - var num = dataSet.uint32(tag); - if(num === undefined) { - return undefined; + if(element.vr === undefined) { + throw 'dicomParser.explicitElementToString: cannot convert implicit element to string'; } - if (num < 0) + var vr = element.vr; + var tag = element.tag; + + var textResult; + + function multiElementToString(numItems, func) { + var result = ""; + for(var i=0; i < numItems; i++) { + if(i !== 0) { + result += '/'; + } + result += func.call(dataSet, tag).toString(); + } + return result; + } + + if(dicomParser.isStringVr(vr) === true) { - num = 0xFFFFFFFF + num + 1; + textResult = dataSet.string(tag); + } + else if (vr == 'AT') { + var num = dataSet.uint32(tag); + if(num === undefined) { + return undefined; + } + if (num < 0) + { + num = 0xFFFFFFFF + num + 1; + } + + return 'x' + num.toString(16).toUpperCase(); + } + else if (vr == 'US') + { + textResult = multiElementToString(element.length / 2, dataSet.uint16); + } + else if(vr === 'SS') + { + textResult = multiElementToString(element.length / 2, dataSet.int16); + } + else if (vr == 'UL') + { + textResult = multiElementToString(element.length / 4, dataSet.uint32); + } + else if(vr === 'SL') + { + textResult = multiElementToString(element.length / 4, dataSet.int32); + } + else if(vr == 'FD') + { + textResult = multiElementToString(element.length / 8, dataSet.double); + } + else if(vr == 'FL') + { + textResult = multiElementToString(element.length / 4, dataSet.float); } - return 'x' + num.toString(16).toUpperCase(); - } - else if (vr == 'US') - { - textResult = multiElementToString(element.length / 2, dataSet.uint16); - } - else if(vr === 'SS') - { - textResult = multiElementToString(element.length / 2, dataSet.int16); - } - else if (vr == 'UL') - { - textResult = multiElementToString(element.length / 4, dataSet.uint32); - } - else if(vr === 'SL') - { - textResult = multiElementToString(element.length / 4, dataSet.int32); - } - else if(vr == 'FD') - { - textResult = multiElementToString(element.length / 8, dataSet.double); - } - else if(vr == 'FL') - { - textResult = multiElementToString(element.length / 4, dataSet.float); - } - - return textResult; + return textResult; }; return dicomParser; - }(dicomParser)); - /** - * Utility functions for dealing with DICOM - */ +}(dicomParser)); +/** + * Utility functions for dealing with DICOM + */ - var dicomParser = (function (dicomParser) +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) { - "use strict"; + dicomParser = {}; + } - if(dicomParser === undefined) - { - dicomParser = {}; + // algorithm based on http://stackoverflow.com/questions/1433030/validate-number-of-days-in-a-given-month + function daysInMonth(m, y) { // m is 0 indexed: 0-11 + switch (m) { + case 2 : + return (y % 4 == 0 && y % 100) || y % 400 == 0 ? 29 : 28; + case 9 : case 4 : case 6 : case 11 : + return 30; + default : + return 31 } + } - // algorithm based on http://stackoverflow.com/questions/1433030/validate-number-of-days-in-a-given-month - function daysInMonth(m, y) { // m is 0 indexed: 0-11 - switch (m) { - case 2 : - return (y % 4 == 0 && y % 100) || y % 400 == 0 ? 29 : 28; - case 9 : case 4 : case 6 : case 11 : - return 30; - default : - return 31 - } - } - - function isValidDate(d, m, y) { - // make year is a number - if(isNaN(y)) { - return false; - } - return m > 0 && m <= 12 && d > 0 && d <= daysInMonth(m, y); + function isValidDate(d, m, y) { + // make year is a number + if(isNaN(y)) { + return false; } + return m > 0 && m <= 12 && d > 0 && d <= daysInMonth(m, y); + } - /** - * Parses a DA formatted string into a Javascript object - * @param {string} date a string in the DA VR format - * @param {boolean} [validate] - true if an exception should be thrown if the date is invalid - * @returns {*} Javascript object with properties year, month and day or undefined if not present or not 8 bytes long - */ - dicomParser.parseDA = function(date, validate) - { - if(date && date.length === 8) - { - var yyyy = parseInt(date.substring(0, 4), 10); - var mm = parseInt(date.substring(4, 6), 10); - var dd = parseInt(date.substring(6, 8), 10); - - if(validate) { - if (isValidDate(dd, mm, yyyy) !== true) { - throw "invalid DA '" + date + "'"; - } - } - return { - year: yyyy, - month: mm, - day: dd - }; - } - if(validate) { - throw "invalid DA '" + date + "'"; - } - return undefined; - }; - - return dicomParser; - }(dicomParser)); /** - * Utility functions for dealing with DICOM + * Parses a DA formatted string into a Javascript object + * @param {string} date a string in the DA VR format + * @param {boolean} [validate] - true if an exception should be thrown if the date is invalid + * @returns {*} Javascript object with properties year, month and day or undefined if not present or not 8 bytes long */ - - var dicomParser = (function (dicomParser) + dicomParser.parseDA = function(date, validate) { - "use strict"; - - if(dicomParser === undefined) + if(date && date.length === 8) { - dicomParser = {}; - } - - /** - * Parses a TM formatted string into a javascript object with properties for hours, minutes, seconds and fractionalSeconds - * @param {string} time - a string in the TM VR format - * @param {boolean} [validate] - true if an exception should be thrown if the date is invalid - * @returns {*} javascript object with properties for hours, minutes, seconds and fractionalSeconds or undefined if no element or data. Missing fields are set to undefined - */ - dicomParser.parseTM = function(time, validate) { - - if (time.length >= 2) // must at least have HH - { - // 0123456789 - // HHMMSS.FFFFFF - var hh = parseInt(time.substring(0, 2), 10); - var mm = time.length >= 4 ? parseInt(time.substring(2, 4), 10) : undefined; - var ss = time.length >= 6 ? parseInt(time.substring(4, 6), 10) : undefined; - var ffffff = time.length >= 8 ? parseInt(time.substring(7, 13), 10) : undefined; - - if(validate) { - if((isNaN(hh)) || - (mm !== undefined && isNaN(mm)) || - (ss !== undefined && isNaN(ss)) || - (ffffff !== undefined && isNaN(ffffff)) || - (hh < 0 || hh > 23) || - (mm && (mm <0 || mm > 59)) || - (ss && (ss <0 || ss > 59)) || - (ffffff && (ffffff <0 || ffffff > 999999))) - { - throw "invalid TM '" + time + "'"; - } - } - - return { - hours: hh, - minutes: mm, - seconds: ss, - fractionalSeconds: ffffff - }; - } + var yyyy = parseInt(date.substring(0, 4), 10); + var mm = parseInt(date.substring(4, 6), 10); + var dd = parseInt(date.substring(6, 8), 10); if(validate) { - throw "invalid TM '" + time + "'"; + if (isValidDate(dd, mm, yyyy) !== true) { + throw "invalid DA '" + date + "'"; + } + } + return { + year: yyyy, + month: mm, + day: dd + }; + } + if(validate) { + throw "invalid DA '" + date + "'"; + } + return undefined; + }; + + return dicomParser; +}(dicomParser)); +/** + * Utility functions for dealing with DICOM + */ + +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + /** + * Parses a TM formatted string into a javascript object with properties for hours, minutes, seconds and fractionalSeconds + * @param {string} time - a string in the TM VR format + * @param {boolean} [validate] - true if an exception should be thrown if the date is invalid + * @returns {*} javascript object with properties for hours, minutes, seconds and fractionalSeconds or undefined if no element or data. Missing fields are set to undefined + */ + dicomParser.parseTM = function(time, validate) { + + if (time.length >= 2) // must at least have HH + { + // 0123456789 + // HHMMSS.FFFFFF + var hh = parseInt(time.substring(0, 2), 10); + var mm = time.length >= 4 ? parseInt(time.substring(2, 4), 10) : undefined; + var ss = time.length >= 6 ? parseInt(time.substring(4, 6), 10) : undefined; + var ffffff = time.length >= 8 ? parseInt(time.substring(7, 13), 10) : undefined; + + if(validate) { + if((isNaN(hh)) || + (mm !== undefined && isNaN(mm)) || + (ss !== undefined && isNaN(ss)) || + (ffffff !== undefined && isNaN(ffffff)) || + (hh < 0 || hh > 23) || + (mm && (mm <0 || mm > 59)) || + (ss && (ss <0 || ss > 59)) || + (ffffff && (ffffff <0 || ffffff > 999999))) + { + throw "invalid TM '" + time + "'"; + } } - return undefined; - }; + return { + hours: hh, + minutes: mm, + seconds: ss, + fractionalSeconds: ffffff + }; + } - return dicomParser; - }(dicomParser)); - /** - * Utility functions for dealing with DICOM - */ + if(validate) { + throw "invalid TM '" + time + "'"; + } - var dicomParser = (function (dicomParser) - { + return undefined; + }; + + return dicomParser; +}(dicomParser)); +/** + * Utility functions for dealing with DICOM + */ + +var dicomParser = (function (dicomParser) +{ "use strict"; if(dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } var stringVrs = { - AE: true, - AS: true, - AT: false, - CS: true, - DA: true, - DS: true, - DT: true, - FL: false, - FD: false, - IS: true, - LO: true, - LT: true, - OB: false, - OD: false, - OF: false, - OW: false, - PN: true, - SH: true, - SL: false, - SQ: false, - SS: false, - ST: true, - TM: true, - UI: true, - UL: false, - UN: undefined, // dunno - UR: true, - US: false, - UT: true + AE: true, + AS: true, + AT: false, + CS: true, + DA: true, + DS: true, + DT: true, + FL: false, + FD: false, + IS: true, + LO: true, + LT: true, + OB: false, + OD: false, + OF: false, + OW: false, + PN: true, + SH: true, + SL: false, + SQ: false, + SS: false, + ST: true, + TM: true, + UI: true, + UL: false, + UN: undefined, // dunno + UR: true, + US: false, + UT: true }; /** @@ -503,7 +480,7 @@ */ dicomParser.isStringVr = function(vr) { - return stringVrs[vr]; + return stringVrs[vr]; }; /** @@ -513,9 +490,9 @@ */ dicomParser.isPrivateTag = function(tag) { - var lastGroupDigit = parseInt(tag[4]); - var groupIsOdd = (lastGroupDigit % 2) === 1; - return groupIsOdd; + var lastGroupDigit = parseInt(tag[4]); + var groupIsOdd = (lastGroupDigit % 2) === 1; + return groupIsOdd; }; /** @@ -525,109 +502,112 @@ * @returns {*} javascript object with properties for givenName, familyName, middleName, prefix and suffix or undefined if no element or data */ dicomParser.parsePN = function(personName) { - if(personName === undefined) { - return undefined; - } - var stringValues = personName.split('^'); - return { - familyName: stringValues[0], - givenName: stringValues[1], - middleName: stringValues[2], - prefix: stringValues[3], - suffix: stringValues[4] - }; + if(personName === undefined) { + return undefined; + } + var stringValues = personName.split('^'); + return { + familyName: stringValues[0], + givenName: stringValues[1], + middleName: stringValues[2], + prefix: stringValues[3], + suffix: stringValues[4] + }; }; return dicomParser; - }(dicomParser)); - /** - * Functionality for extracting encapsulated pixel data - */ +}(dicomParser)); +/** + * Functionality for extracting encapsulated pixel data + */ - var dicomParser = (function (dicomParser) - { +var dicomParser = (function (dicomParser) +{ "use strict"; if(dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } function getPixelDataFromFragments(byteStream, fragments, bufferSize) { - // if there is only one fragment, return a view on this array to avoid copying - if(fragments.length === 1) { - return new Uint8Array(byteStream.byteArray.buffer, fragments[0].dataOffset, fragments[0].length); - } - - // more than one fragment, combine all of the fragments into one buffer - var pixelData = new Uint8Array(bufferSize); - var pixelDataIndex = 0; - for(var i=0; i < fragments.length; i++) { - var fragmentOffset = fragments[i].dataOffset; - for(var j=0; j < fragments[i].length; j++) { - pixelData[pixelDataIndex++] = byteStream.byteArray[fragmentOffset++]; + // if there is only one fragment, return a view on this array to avoid copying + if(fragments.length === 1) { + return new Uint8Array(byteStream.byteArray.buffer, fragments[0].dataOffset, fragments[0].length); } - } - return pixelData; + // more than one fragment, combine all of the fragments into one buffer + var pixelData = new Uint8Array(bufferSize); + var pixelDataIndex = 0; + for(var i=0; i < fragments.length; i++) { + var fragmentOffset = fragments[i].dataOffset; + for(var j=0; j < fragments[i].length; j++) { + pixelData[pixelDataIndex++] = byteStream.byteArray[fragmentOffset++]; + } + } + + return pixelData; } function readFragmentsUntil(byteStream, endOfFrame) { - // Read fragments until we reach endOfFrame - var fragments = []; - var bufferSize = 0; - while(byteStream.position < endOfFrame && byteStream.position < byteStream.byteArray.length) { - var fragment = dicomParser.readSequenceItem(byteStream); - // NOTE: we only encounter this for the sequence delimiter tag when extracting the last frame - if(fragment.tag === 'xfffee0dd') { - break; + // Read fragments until we reach endOfFrame + var fragments = []; + var bufferSize = 0; + while(byteStream.position < endOfFrame && byteStream.position < byteStream.byteArray.length) { + var fragment = dicomParser.readSequenceItem(byteStream); + // NOTE: we only encounter this for the sequence delimiter tag when extracting the last frame + if(fragment.tag === 'xfffee0dd') { + break; + } + fragments.push(fragment); + byteStream.seek(fragment.length); + bufferSize += fragment.length; } - fragments.push(fragment); - byteStream.seek(fragment.length); - bufferSize += fragment.length; - } - // Convert the fragments into a single pixelData buffer - var pixelData = getPixelDataFromFragments(byteStream, fragments, bufferSize); - return pixelData; + // Convert the fragments into a single pixelData buffer + var pixelData = getPixelDataFromFragments(byteStream, fragments, bufferSize); + return pixelData; } function readEncapsulatedPixelDataWithBasicOffsetTable(pixelDataElement, byteStream, frame) { - // validate that we have an offset for this frame - var numFrames = pixelDataElement.basicOffsetTable.length; - if(frame > numFrames) { - throw "dicomParser.readEncapsulatedPixelData: parameter frame exceeds number of frames in basic offset table"; - } + // validate that we have an offset for this frame + var numFrames = pixelDataElement.basicOffsetTable.length; + if(frame > numFrames) { + throw "dicomParser.readEncapsulatedPixelData: parameter frame exceeds number of frames in basic offset table"; + } - // move to the start of this frame - var frameOffset = pixelDataElement.basicOffsetTable[frame]; - byteStream.seek(frameOffset); + // move to the start of this frame + var frameOffset = pixelDataElement.basicOffsetTable[frame]; + var firstFragment = byteStream.position; + byteStream.seek(frameOffset); - // Find the end of this frame - var endOfFrameOffset = pixelDataElement.basicOffsetTable[frame + 1]; - if(endOfFrameOffset === undefined) { // special case for last frame - endOfFrameOffset = byteStream.position + pixelDataElement.length; - } + // Find the end of this frame + var endOfFrameOffset = pixelDataElement.basicOffsetTable[frame + 1]; + if(endOfFrameOffset === undefined) { // special case for last frame + endOfFrameOffset = byteStream.position + pixelDataElement.length; + } else { + endOfFrameOffset += firstFragment; + } - // read this frame - var pixelData = readFragmentsUntil(byteStream, endOfFrameOffset); - return pixelData; + // read this frame + var pixelData = readFragmentsUntil(byteStream, endOfFrameOffset); + return pixelData; } function readEncapsulatedDataNoBasicOffsetTable(pixelDataElement, byteStream, frame) { - // if the basic offset table is empty, this is a single frame so make sure the requested - // frame is 0 - if(frame !== 0) { - throw 'dicomParser.readEncapsulatedPixelData: non zero frame specified for single frame encapsulated pixel data'; - } + // if the basic offset table is empty, this is a single frame so make sure the requested + // frame is 0 + if(frame !== 0) { + throw 'dicomParser.readEncapsulatedPixelData: non zero frame specified for single frame encapsulated pixel data'; + } - // read this frame - var endOfFrame = byteStream.position + pixelDataElement.length; - var pixelData = readFragmentsUntil(byteStream, endOfFrame); - return pixelData; + // read this frame + var endOfFrame = byteStream.position + pixelDataElement.length; + var pixelData = readFragmentsUntil(byteStream, endOfFrame); + return pixelData; } /** @@ -640,243 +620,243 @@ */ dicomParser.readEncapsulatedPixelData = function(dataSet, pixelDataElement, frame) { - if(dataSet === undefined) { - throw "dicomParser.readEncapsulatedPixelData: missing required parameter 'dataSet'"; - } - if(pixelDataElement === undefined) { - throw "dicomParser.readEncapsulatedPixelData: missing required parameter 'element'"; - } - if(frame === undefined) { - throw "dicomParser.readEncapsulatedPixelData: missing required parameter 'frame'"; - } - if(pixelDataElement.tag !== 'x7fe00010') { - throw "dicomParser.readEncapsulatedPixelData: parameter 'element' refers to non pixel data tag (expected tag = x7fe00010'"; - } - if(pixelDataElement.encapsulatedPixelData !== true) { - throw "dicomParser.readEncapsulatedPixelData: parameter 'element' refers to pixel data element that does not have encapsulated pixel data"; - } - if(pixelDataElement.hadUndefinedLength !== true) { - throw "dicomParser.readEncapsulatedPixelData: parameter 'element' refers to pixel data element that does not have encapsulated pixel data"; - } - if(pixelDataElement.basicOffsetTable === undefined) { - throw "dicomParser.readEncapsulatedPixelData: parameter 'element' refers to pixel data element that does not have encapsulated pixel data"; - } - if(pixelDataElement.fragments === undefined) { - throw "dicomParser.readEncapsulatedPixelData: parameter 'element' refers to pixel data element that does not have encapsulated pixel data"; - } - if(frame < 0) { - throw "dicomParser.readEncapsulatedPixelData: parameter 'frame' must be >= 0"; - } + if(dataSet === undefined) { + throw "dicomParser.readEncapsulatedPixelData: missing required parameter 'dataSet'"; + } + if(pixelDataElement === undefined) { + throw "dicomParser.readEncapsulatedPixelData: missing required parameter 'element'"; + } + if(frame === undefined) { + throw "dicomParser.readEncapsulatedPixelData: missing required parameter 'frame'"; + } + if(pixelDataElement.tag !== 'x7fe00010') { + throw "dicomParser.readEncapsulatedPixelData: parameter 'element' refers to non pixel data tag (expected tag = x7fe00010'"; + } + if(pixelDataElement.encapsulatedPixelData !== true) { + throw "dicomParser.readEncapsulatedPixelData: parameter 'element' refers to pixel data element that does not have encapsulated pixel data"; + } + if(pixelDataElement.hadUndefinedLength !== true) { + throw "dicomParser.readEncapsulatedPixelData: parameter 'element' refers to pixel data element that does not have encapsulated pixel data"; + } + if(pixelDataElement.basicOffsetTable === undefined) { + throw "dicomParser.readEncapsulatedPixelData: parameter 'element' refers to pixel data element that does not have encapsulated pixel data"; + } + if(pixelDataElement.fragments === undefined) { + throw "dicomParser.readEncapsulatedPixelData: parameter 'element' refers to pixel data element that does not have encapsulated pixel data"; + } + if(frame < 0) { + throw "dicomParser.readEncapsulatedPixelData: parameter 'frame' must be >= 0"; + } - // seek past the basic offset table (no need to parse it again since we already have) - var byteStream = new dicomParser.ByteStream(dataSet.byteArrayParser, dataSet.byteArray, pixelDataElement.dataOffset); - var basicOffsetTable = dicomParser.readSequenceItem(byteStream); - if(basicOffsetTable.tag !== 'xfffee000') - { - throw "dicomParser.readEncapsulatedPixelData: missing basic offset table xfffee000"; - } - byteStream.seek(basicOffsetTable.length); + // seek past the basic offset table (no need to parse it again since we already have) + var byteStream = new dicomParser.ByteStream(dataSet.byteArrayParser, dataSet.byteArray, pixelDataElement.dataOffset); + var basicOffsetTable = dicomParser.readSequenceItem(byteStream); + if(basicOffsetTable.tag !== 'xfffee000') + { + throw "dicomParser.readEncapsulatedPixelData: missing basic offset table xfffee000"; + } + byteStream.seek(basicOffsetTable.length); - // If the basic offset table is empty (no entries), it is a single frame. If it is not empty, - // it has at least one frame so use the basic offset table to find the bytes - if(pixelDataElement.basicOffsetTable.length !== 0) - { - return readEncapsulatedPixelDataWithBasicOffsetTable(pixelDataElement, byteStream, frame); - } - else - { - return readEncapsulatedDataNoBasicOffsetTable(pixelDataElement, byteStream, frame); - } + // If the basic offset table is empty (no entries), it is a single frame. If it is not empty, + // it has at least one frame so use the basic offset table to find the bytes + if(pixelDataElement.basicOffsetTable.length !== 0) + { + return readEncapsulatedPixelDataWithBasicOffsetTable(pixelDataElement, byteStream, frame); + } + else + { + return readEncapsulatedDataNoBasicOffsetTable(pixelDataElement, byteStream, frame); + } }; return dicomParser; - }(dicomParser)); +}(dicomParser)); - /** - * Internal helper functions for parsing different types from a big-endian byte array - */ +/** + * Internal helper functions for parsing different types from a big-endian byte array + */ - var dicomParser = (function (dicomParser) - { +var dicomParser = (function (dicomParser) +{ "use strict"; if(dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } dicomParser.bigEndianByteArrayParser = { - /** - * - * Parses an unsigned int 16 from a big-endian byte array - * - * @param byteArray the byte array to read from - * @param position the position in the byte array to read from - * @returns {*} the parsed unsigned int 16 - * @throws error if buffer overread would occur - * @access private - */ - readUint16: function (byteArray, position) { - if (position < 0) { - throw 'bigEndianByteArrayParser.readUint16: position cannot be less than 0'; + /** + * + * Parses an unsigned int 16 from a big-endian byte array + * + * @param byteArray the byte array to read from + * @param position the position in the byte array to read from + * @returns {*} the parsed unsigned int 16 + * @throws error if buffer overread would occur + * @access private + */ + readUint16: function (byteArray, position) { + if (position < 0) { + throw 'bigEndianByteArrayParser.readUint16: position cannot be less than 0'; + } + if (position + 2 > byteArray.length) { + throw 'bigEndianByteArrayParser.readUint16: attempt to read past end of buffer'; + } + return (byteArray[position] << 8) + byteArray[position + 1]; + }, + + /** + * + * Parses a signed int 16 from a big-endian byte array + * + * @param byteArray the byte array to read from + * @param position the position in the byte array to read from + * @returns {*} the parsed signed int 16 + * @throws error if buffer overread would occur + * @access private + */ + readInt16: function (byteArray, position) { + if (position < 0) { + throw 'bigEndianByteArrayParser.readInt16: position cannot be less than 0'; + } + if (position + 2 > byteArray.length) { + throw 'bigEndianByteArrayParser.readInt16: attempt to read past end of buffer'; + } + var int16 = (byteArray[position] << 8) + byteArray[position + 1]; + // fix sign + if (int16 & 0x8000) { + int16 = int16 - 0xFFFF - 1; + } + return int16; + }, + + /** + * Parses an unsigned int 32 from a big-endian byte array + * + * @param byteArray the byte array to read from + * @param position the position in the byte array to read from + * @returns {*} the parsed unsigned int 32 + * @throws error if buffer overread would occur + * @access private + */ + readUint32: function (byteArray, position) { + if (position < 0) { + throw 'bigEndianByteArrayParser.readUint32: position cannot be less than 0'; + } + + if (position + 4 > byteArray.length) { + throw 'bigEndianByteArrayParser.readUint32: attempt to read past end of buffer'; + } + + var uint32 = (256 * (256 * (256 * byteArray[position] + + byteArray[position + 1]) + + byteArray[position + 2]) + + byteArray[position + 3]); + + return uint32; + }, + + /** + * Parses a signed int 32 from a big-endian byte array + * + * @param byteArray the byte array to read from + * @param position the position in the byte array to read from + * @returns {*} the parsed signed int 32 + * @throws error if buffer overread would occur + * @access private + */ + readInt32: function (byteArray, position) { + if (position < 0) { + throw 'bigEndianByteArrayParser.readInt32: position cannot be less than 0'; + } + + if (position + 4 > byteArray.length) { + throw 'bigEndianByteArrayParser.readInt32: attempt to read past end of buffer'; + } + + var int32 = ((byteArray[position] << 24) + + (byteArray[position + 1] << 16) + + (byteArray[position + 2] << 8) + + byteArray[position + 3]); + + return int32; + }, + + /** + * Parses 32-bit float from a big-endian byte array + * + * @param byteArray the byte array to read from + * @param position the position in the byte array to read from + * @returns {*} the parsed 32-bit float + * @throws error if buffer overread would occur + * @access private + */ + readFloat: function (byteArray, position) { + if (position < 0) { + throw 'bigEndianByteArrayParser.readFloat: position cannot be less than 0'; + } + + if (position + 4 > byteArray.length) { + throw 'bigEndianByteArrayParser.readFloat: attempt to read past end of buffer'; + } + + // I am sure there is a better way than this but this should be safe + var byteArrayForParsingFloat = new Uint8Array(4); + byteArrayForParsingFloat[3] = byteArray[position]; + byteArrayForParsingFloat[2] = byteArray[position + 1]; + byteArrayForParsingFloat[1] = byteArray[position + 2]; + byteArrayForParsingFloat[0] = byteArray[position + 3]; + var floatArray = new Float32Array(byteArrayForParsingFloat.buffer); + return floatArray[0]; + }, + + /** + * Parses 64-bit float from a big-endian byte array + * + * @param byteArray the byte array to read from + * @param position the position in the byte array to read from + * @returns {*} the parsed 64-bit float + * @throws error if buffer overread would occur + * @access private + */ + readDouble: function (byteArray, position) { + if (position < 0) { + throw 'bigEndianByteArrayParser.readDouble: position cannot be less than 0'; + } + + if (position + 8 > byteArray.length) { + throw 'bigEndianByteArrayParser.readDouble: attempt to read past end of buffer'; + } + + // I am sure there is a better way than this but this should be safe + var byteArrayForParsingFloat = new Uint8Array(8); + byteArrayForParsingFloat[7] = byteArray[position]; + byteArrayForParsingFloat[6] = byteArray[position + 1]; + byteArrayForParsingFloat[5] = byteArray[position + 2]; + byteArrayForParsingFloat[4] = byteArray[position + 3]; + byteArrayForParsingFloat[3] = byteArray[position + 4]; + byteArrayForParsingFloat[2] = byteArray[position + 5]; + byteArrayForParsingFloat[1] = byteArray[position + 6]; + byteArrayForParsingFloat[0] = byteArray[position + 7]; + var floatArray = new Float64Array(byteArrayForParsingFloat.buffer); + return floatArray[0]; } - if (position + 2 > byteArray.length) { - throw 'bigEndianByteArrayParser.readUint16: attempt to read past end of buffer'; - } - return (byteArray[position] << 8) + byteArray[position + 1]; - }, - - /** - * - * Parses a signed int 16 from a big-endian byte array - * - * @param byteArray the byte array to read from - * @param position the position in the byte array to read from - * @returns {*} the parsed signed int 16 - * @throws error if buffer overread would occur - * @access private - */ - readInt16: function (byteArray, position) { - if (position < 0) { - throw 'bigEndianByteArrayParser.readInt16: position cannot be less than 0'; - } - if (position + 2 > byteArray.length) { - throw 'bigEndianByteArrayParser.readInt16: attempt to read past end of buffer'; - } - var int16 = (byteArray[position] << 8) + byteArray[position + 1]; - // fix sign - if (int16 & 0x8000) { - int16 = int16 - 0xFFFF - 1; - } - return int16; - }, - - /** - * Parses an unsigned int 32 from a big-endian byte array - * - * @param byteArray the byte array to read from - * @param position the position in the byte array to read from - * @returns {*} the parsed unsigned int 32 - * @throws error if buffer overread would occur - * @access private - */ - readUint32: function (byteArray, position) { - if (position < 0) { - throw 'bigEndianByteArrayParser.readUint32: position cannot be less than 0'; - } - - if (position + 4 > byteArray.length) { - throw 'bigEndianByteArrayParser.readUint32: attempt to read past end of buffer'; - } - - var uint32 = (256 * (256 * (256 * byteArray[position] + - byteArray[position + 1]) + - byteArray[position + 2]) + - byteArray[position + 3]); - - return uint32; - }, - - /** - * Parses a signed int 32 from a big-endian byte array - * - * @param byteArray the byte array to read from - * @param position the position in the byte array to read from - * @returns {*} the parsed signed int 32 - * @throws error if buffer overread would occur - * @access private - */ - readInt32: function (byteArray, position) { - if (position < 0) { - throw 'bigEndianByteArrayParser.readInt32: position cannot be less than 0'; - } - - if (position + 4 > byteArray.length) { - throw 'bigEndianByteArrayParser.readInt32: attempt to read past end of buffer'; - } - - var int32 = ((byteArray[position] << 24) + - (byteArray[position + 1] << 16) + - (byteArray[position + 2] << 8) + - byteArray[position + 3]); - - return int32; - }, - - /** - * Parses 32-bit float from a big-endian byte array - * - * @param byteArray the byte array to read from - * @param position the position in the byte array to read from - * @returns {*} the parsed 32-bit float - * @throws error if buffer overread would occur - * @access private - */ - readFloat: function (byteArray, position) { - if (position < 0) { - throw 'bigEndianByteArrayParser.readFloat: position cannot be less than 0'; - } - - if (position + 4 > byteArray.length) { - throw 'bigEndianByteArrayParser.readFloat: attempt to read past end of buffer'; - } - - // I am sure there is a better way than this but this should be safe - var byteArrayForParsingFloat = new Uint8Array(4); - byteArrayForParsingFloat[3] = byteArray[position]; - byteArrayForParsingFloat[2] = byteArray[position + 1]; - byteArrayForParsingFloat[1] = byteArray[position + 2]; - byteArrayForParsingFloat[0] = byteArray[position + 3]; - var floatArray = new Float32Array(byteArrayForParsingFloat.buffer); - return floatArray[0]; - }, - - /** - * Parses 64-bit float from a big-endian byte array - * - * @param byteArray the byte array to read from - * @param position the position in the byte array to read from - * @returns {*} the parsed 64-bit float - * @throws error if buffer overread would occur - * @access private - */ - readDouble: function (byteArray, position) { - if (position < 0) { - throw 'bigEndianByteArrayParser.readDouble: position cannot be less than 0'; - } - - if (position + 8 > byteArray.length) { - throw 'bigEndianByteArrayParser.readDouble: attempt to read past end of buffer'; - } - - // I am sure there is a better way than this but this should be safe - var byteArrayForParsingFloat = new Uint8Array(8); - byteArrayForParsingFloat[7] = byteArray[position]; - byteArrayForParsingFloat[6] = byteArray[position + 1]; - byteArrayForParsingFloat[5] = byteArray[position + 2]; - byteArrayForParsingFloat[4] = byteArray[position + 3]; - byteArrayForParsingFloat[3] = byteArray[position + 4]; - byteArrayForParsingFloat[2] = byteArray[position + 5]; - byteArrayForParsingFloat[1] = byteArray[position + 6]; - byteArrayForParsingFloat[0] = byteArray[position + 7]; - var floatArray = new Float64Array(byteArrayForParsingFloat.buffer); - return floatArray[0]; - } }; return dicomParser; - }(dicomParser)); - /** - * Internal helper functions common to parsing byte arrays of any type - */ +}(dicomParser)); +/** + * Internal helper functions common to parsing byte arrays of any type + */ - var dicomParser = (function (dicomParser) - { +var dicomParser = (function (dicomParser) +{ "use strict"; if(dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } /** @@ -893,48 +873,48 @@ */ dicomParser.readFixedString = function(byteArray, position, length) { - if(length < 0) - { - throw 'readFixedString - length cannot be less than 0'; - } - - if(position + length > byteArray.length) { - throw 'dicomParser.readFixedString: attempt to read past end of buffer'; - } - - var result = ""; - for(var i=0; i < length; i++) - { - var byte = byteArray[position + i]; - if(byte === 0) { - position += length; - return result; + if(length < 0) + { + throw 'readFixedString - length cannot be less than 0'; } - result += String.fromCharCode(byte); - } - return result; + if(position + length > byteArray.length) { + throw 'dicomParser.readFixedString: attempt to read past end of buffer'; + } + + var result = ""; + for(var i=0; i < length; i++) + { + var byte = byteArray[position + i]; + if(byte === 0) { + position += length; + return result; + } + result += String.fromCharCode(byte); + } + + return result; }; return dicomParser; - }(dicomParser)); - /** - * - * Internal helper class to assist with parsing. Supports reading from a byte - * stream contained in a Uint8Array. Example usage: - * - * var byteArray = new Uint8Array(32); - * var byteStream = new dicomParser.ByteStream(dicomParser.littleEndianByteArrayParser, byteArray); - * - * */ - var dicomParser = (function (dicomParser) - { +}(dicomParser)); +/** + * + * Internal helper class to assist with parsing. Supports reading from a byte + * stream contained in a Uint8Array. Example usage: + * + * var byteArray = new Uint8Array(32); + * var byteStream = new dicomParser.ByteStream(dicomParser.littleEndianByteArrayParser, byteArray); + * + * */ +var dicomParser = (function (dicomParser) +{ "use strict"; if(dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } /** @@ -948,30 +928,30 @@ * @throws will throw an error if the position parameter is not inside the byte array */ dicomParser.ByteStream = function(byteArrayParser, byteArray, position) { - if(byteArrayParser === undefined) - { - throw "dicomParser.ByteStream: missing required parameter 'byteArrayParser'"; - } - if(byteArray === undefined) - { - throw "dicomParser.ByteStream: missing required parameter 'byteArray'"; - } - if((byteArray instanceof Uint8Array) === false) { - throw 'dicomParser.ByteStream: parameter byteArray is not of type Uint8Array'; - } - if(position < 0) - { - throw "dicomParser.ByteStream: parameter 'position' cannot be less than 0"; - } - if(position >= byteArray.length) - { - throw "dicomParser.ByteStream: parameter 'position' cannot be greater than or equal to 'byteArray' length"; + if(byteArrayParser === undefined) + { + throw "dicomParser.ByteStream: missing required parameter 'byteArrayParser'"; + } + if(byteArray === undefined) + { + throw "dicomParser.ByteStream: missing required parameter 'byteArray'"; + } + if((byteArray instanceof Uint8Array) === false) { + throw 'dicomParser.ByteStream: parameter byteArray is not of type Uint8Array'; + } + if(position < 0) + { + throw "dicomParser.ByteStream: parameter 'position' cannot be less than 0"; + } + if(position >= byteArray.length) + { + throw "dicomParser.ByteStream: parameter 'position' cannot be greater than or equal to 'byteArray' length"; - } - this.byteArrayParser = byteArrayParser; - this.byteArray = byteArray; - this.position = position ? position : 0; - this.warnings = []; // array of string warnings encountered while parsing + } + this.byteArrayParser = byteArrayParser; + this.byteArray = byteArray; + this.position = position ? position : 0; + this.warnings = []; // array of string warnings encountered while parsing }; /** @@ -982,11 +962,11 @@ */ dicomParser.ByteStream.prototype.seek = function(offset) { - if(this.position + offset < 0) - { - throw "cannot seek to position < 0"; - } - this.position += offset; + if(this.position + offset < 0) + { + throw "cannot seek to position < 0"; + } + this.position += offset; }; /** @@ -997,12 +977,12 @@ */ dicomParser.ByteStream.prototype.readByteStream = function(numBytes) { - if(this.position + numBytes > this.byteArray.length) { - throw 'readByteStream - buffer overread'; - } - var byteArrayView = new Uint8Array(this.byteArray.buffer, this.position, numBytes); - this.position += numBytes; - return new dicomParser.ByteStream(this.byteArrayParser, byteArrayView); + if(this.position + numBytes > this.byteArray.length) { + throw 'readByteStream - buffer overread'; + } + var byteArrayView = new Uint8Array(this.byteArray.buffer, this.position, numBytes); + this.position += numBytes; + return new dicomParser.ByteStream(this.byteArrayParser, byteArrayView); }; /** @@ -1015,9 +995,9 @@ */ dicomParser.ByteStream.prototype.readUint16 = function() { - var result = this.byteArrayParser.readUint16(this.byteArray, this.position); - this.position += 2; - return result; + var result = this.byteArrayParser.readUint16(this.byteArray, this.position); + this.position += 2; + return result; }; /** @@ -1029,9 +1009,9 @@ */ dicomParser.ByteStream.prototype.readUint32 = function() { - var result = this.byteArrayParser.readUint32(this.byteArray, this.position); - this.position += 4; - return result; + var result = this.byteArrayParser.readUint32(this.byteArray, this.position); + this.position += 4; + return result; }; /** @@ -1044,45 +1024,45 @@ */ dicomParser.ByteStream.prototype.readFixedString = function(length) { - var result = dicomParser.readFixedString(this.byteArray, this.position, length); - this.position += length; - return result; + var result = dicomParser.readFixedString(this.byteArray, this.position, length); + this.position += length; + return result; }; return dicomParser; - }(dicomParser)); - /** - * - * The DataSet class encapsulates a collection of DICOM Elements and provides various functions - * to access the data in those elements - * - * Rules for handling padded spaces: - * DS = Strip leading and trailing spaces - * DT = Strip trailing spaces - * IS = Strip leading and trailing spaces - * PN = Strip trailing spaces - * TM = Strip trailing spaces - * AE = Strip leading and trailing spaces - * CS = Strip leading and trailing spaces - * SH = Strip leading and trailing spaces - * LO = Strip leading and trailing spaces - * LT = Strip trailing spaces - * ST = Strip trailing spaces - * UT = Strip trailing spaces - * - */ - var dicomParser = (function (dicomParser) - { +}(dicomParser)); +/** + * + * The DataSet class encapsulates a collection of DICOM Elements and provides various functions + * to access the data in those elements + * + * Rules for handling padded spaces: + * DS = Strip leading and trailing spaces + * DT = Strip trailing spaces + * IS = Strip leading and trailing spaces + * PN = Strip trailing spaces + * TM = Strip trailing spaces + * AE = Strip leading and trailing spaces + * CS = Strip leading and trailing spaces + * SH = Strip leading and trailing spaces + * LO = Strip leading and trailing spaces + * LT = Strip trailing spaces + * ST = Strip trailing spaces + * UT = Strip trailing spaces + * + */ +var dicomParser = (function (dicomParser) +{ "use strict"; if(dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } function getByteArrayParser(element, defaultParser) { - return (element.parser !== undefined ? element.parser : defaultParser); + return (element.parser !== undefined ? element.parser : defaultParser); } /** @@ -1094,9 +1074,9 @@ */ dicomParser.DataSet = function(byteArrayParser, byteArray, elements) { - this.byteArrayParser = byteArrayParser; - this.byteArray = byteArray; - this.elements = elements; + this.byteArrayParser = byteArrayParser; + this.byteArray = byteArray; + this.elements = elements; }; /** @@ -1107,13 +1087,13 @@ */ dicomParser.DataSet.prototype.uint16 = function(tag, index) { - var element = this.elements[tag]; - index = (index !== undefined) ? index : 0; - if(element && element.length !== 0) - { - return getByteArrayParser(element, this.byteArrayParser).readUint16(this.byteArray, element.dataOffset + (index *2)); - } - return undefined; + var element = this.elements[tag]; + index = (index !== undefined) ? index : 0; + if(element && element.length !== 0) + { + return getByteArrayParser(element, this.byteArrayParser).readUint16(this.byteArray, element.dataOffset + (index *2)); + } + return undefined; }; /** @@ -1124,13 +1104,13 @@ */ dicomParser.DataSet.prototype.int16 = function(tag, index) { - var element = this.elements[tag]; - index = (index !== undefined) ? index : 0; - if(element && element.length !== 0) - { - return getByteArrayParser(element, this.byteArrayParser).readInt16(this.byteArray, element.dataOffset + (index * 2)); - } - return undefined; + var element = this.elements[tag]; + index = (index !== undefined) ? index : 0; + if(element && element.length !== 0) + { + return getByteArrayParser(element, this.byteArrayParser).readInt16(this.byteArray, element.dataOffset + (index * 2)); + } + return undefined; }; /** @@ -1141,13 +1121,13 @@ */ dicomParser.DataSet.prototype.uint32 = function(tag, index) { - var element = this.elements[tag]; - index = (index !== undefined) ? index : 0; - if(element && element.length !== 0) - { - return getByteArrayParser(element, this.byteArrayParser).readUint32(this.byteArray, element.dataOffset + (index * 4)); - } - return undefined; + var element = this.elements[tag]; + index = (index !== undefined) ? index : 0; + if(element && element.length !== 0) + { + return getByteArrayParser(element, this.byteArrayParser).readUint32(this.byteArray, element.dataOffset + (index * 4)); + } + return undefined; }; /** @@ -1158,13 +1138,13 @@ */ dicomParser.DataSet.prototype.int32 = function(tag, index) { - var element = this.elements[tag]; - index = (index !== undefined) ? index : 0; - if(element && element.length !== 0) - { - return getByteArrayParser(element, this.byteArrayParser).readInt32(this.byteArray, element.dataOffset + (index * 4)); - } - return undefined; + var element = this.elements[tag]; + index = (index !== undefined) ? index : 0; + if(element && element.length !== 0) + { + return getByteArrayParser(element, this.byteArrayParser).readInt32(this.byteArray, element.dataOffset + (index * 4)); + } + return undefined; }; /** @@ -1175,13 +1155,13 @@ */ dicomParser.DataSet.prototype.float = function(tag, index) { - var element = this.elements[tag]; - index = (index !== undefined) ? index : 0; - if(element && element.length !== 0) - { - return getByteArrayParser(element, this.byteArrayParser).readFloat(this.byteArray, element.dataOffset + (index * 4)); - } - return undefined; + var element = this.elements[tag]; + index = (index !== undefined) ? index : 0; + if(element && element.length !== 0) + { + return getByteArrayParser(element, this.byteArrayParser).readFloat(this.byteArray, element.dataOffset + (index * 4)); + } + return undefined; }; /** @@ -1192,13 +1172,13 @@ */ dicomParser.DataSet.prototype.double = function(tag, index) { - var element = this.elements[tag]; - index = (index !== undefined) ? index : 0; - if(element && element.length !== 0) - { - return getByteArrayParser(element, this.byteArrayParser).readDouble(this.byteArray, element.dataOffset + (index * 8)); - } - return undefined; + var element = this.elements[tag]; + index = (index !== undefined) ? index : 0; + if(element && element.length !== 0) + { + return getByteArrayParser(element, this.byteArrayParser).readDouble(this.byteArray, element.dataOffset + (index * 8)); + } + return undefined; }; /** @@ -1208,18 +1188,18 @@ */ dicomParser.DataSet.prototype.numStringValues = function(tag) { - var element = this.elements[tag]; - if(element && element.length > 0) - { - var fixedString = dicomParser.readFixedString(this.byteArray, element.dataOffset, element.length); - var numMatching = fixedString.match(/\\/g); - if(numMatching === null) + var element = this.elements[tag]; + if(element && element.length > 0) { - return 1; + var fixedString = dicomParser.readFixedString(this.byteArray, element.dataOffset, element.length); + var numMatching = fixedString.match(/\\/g); + if(numMatching === null) + { + return 1; + } + return numMatching.length + 1; } - return numMatching.length + 1; - } - return undefined; + return undefined; }; /** @@ -1235,23 +1215,23 @@ */ dicomParser.DataSet.prototype.string = function(tag, index) { - var element = this.elements[tag]; - if(element && element.length > 0) - { - var fixedString = dicomParser.readFixedString(this.byteArray, element.dataOffset, element.length); - if(index >= 0) + var element = this.elements[tag]; + if(element && element.length > 0) { - var values = fixedString.split('\\'); - // trim trailing spaces - return values[index].trim(); + var fixedString = dicomParser.readFixedString(this.byteArray, element.dataOffset, element.length); + if(index >= 0) + { + var values = fixedString.split('\\'); + // trim trailing spaces + return values[index].trim(); + } + else + { + // trim trailing spaces + return fixedString.trim(); + } } - else - { - // trim trailing spaces - return fixedString.trim(); - } - } - return undefined; + return undefined; }; /** @@ -1265,21 +1245,21 @@ */ dicomParser.DataSet.prototype.text = function(tag, index) { - var element = this.elements[tag]; - if(element && element.length > 0) - { - var fixedString = dicomParser.readFixedString(this.byteArray, element.dataOffset, element.length); - if(index >= 0) + var element = this.elements[tag]; + if(element && element.length > 0) { - var values = fixedString.split('\\'); - return values[index].replace(/ +$/, ''); + var fixedString = dicomParser.readFixedString(this.byteArray, element.dataOffset, element.length); + if(index >= 0) + { + var values = fixedString.split('\\'); + return values[index].replace(/ +$/, ''); + } + else + { + return fixedString.replace(/ +$/, ''); + } } - else - { - return fixedString.replace(/ +$/, ''); - } - } - return undefined; + return undefined; }; /** @@ -1291,16 +1271,16 @@ */ dicomParser.DataSet.prototype.floatString = function(tag, index) { - var element = this.elements[tag]; - if(element && element.length > 0) - { - index = (index !== undefined) ? index : 0; - var value = this.string(tag, index); - if(value !== undefined) { - return parseFloat(value); + var element = this.elements[tag]; + if(element && element.length > 0) + { + index = (index !== undefined) ? index : 0; + var value = this.string(tag, index); + if(value !== undefined) { + return parseFloat(value); + } } - } - return undefined; + return undefined; }; /** @@ -1312,32 +1292,32 @@ */ dicomParser.DataSet.prototype.intString = function(tag, index) { - var element = this.elements[tag]; - if(element && element.length > 0) { - index = (index !== undefined) ? index : 0; - var value = this.string(tag, index); - if(value !== undefined) { - return parseInt(value); + var element = this.elements[tag]; + if(element && element.length > 0) { + index = (index !== undefined) ? index : 0; + var value = this.string(tag, index); + if(value !== undefined) { + return parseInt(value); + } } - } - return undefined; + return undefined; }; //dicomParser.DataSet = DataSet; return dicomParser; - }(dicomParser)); - /** - * Internal helper functions for parsing DICOM elements - */ +}(dicomParser)); +/** + * Internal helper functions for parsing DICOM elements + */ - var dicomParser = (function (dicomParser) - { +var dicomParser = (function (dicomParser) +{ "use strict"; if(dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } /** @@ -1349,89 +1329,89 @@ */ dicomParser.findEndOfEncapsulatedElement = function(byteStream, element, warnings) { - if(byteStream === undefined) - { - throw "dicomParser.findEndOfEncapsulatedElement: missing required parameter 'byteStream'"; - } - if(element === undefined) - { - throw "dicomParser.findEndOfEncapsulatedElement: missing required parameter 'element'"; - } - - element.encapsulatedPixelData = true; - element.basicOffsetTable = []; - element.fragments = []; - var basicOffsetTableItemTag = dicomParser.readTag(byteStream); - if(basicOffsetTableItemTag !== 'xfffee000') { - throw "dicomParser.findEndOfEncapsulatedElement: basic offset table not found"; - } - var basicOffsetTableItemlength = byteStream.readUint32(); - var numFragments = basicOffsetTableItemlength / 4; - for(var i =0; i < numFragments; i++) { - var offset = byteStream.readUint32(); - element.basicOffsetTable.push(offset); - } - var baseOffset = byteStream.position; - - while(byteStream.position < byteStream.byteArray.length) - { - var tag = dicomParser.readTag(byteStream); - var length = byteStream.readUint32(); - if(tag === 'xfffee0dd') + if(byteStream === undefined) { - byteStream.seek(length); - element.length = byteStream.position - element.dataOffset; - return; + throw "dicomParser.findEndOfEncapsulatedElement: missing required parameter 'byteStream'"; } - else if(tag === 'xfffee000') + if(element === undefined) { - element.fragments.push({ - offset: byteStream.position - baseOffset - 8, - position : byteStream.position, - length : length - }); - } - else { - if(warnings) { - warnings.push('unexpected tag ' + tag + ' while searching for end of pixel data element with undefined length'); - } - if(length > byteStream.byteArray.length - byteStream.position) - { - // fix length - length = byteStream.byteArray.length - byteStream.position; - } - element.fragments.push({ - offset: byteStream.position - baseOffset - 8, - position : byteStream.position, - length : length - }); - byteStream.seek(length); - element.length = byteStream.position - element.dataOffset; - return; + throw "dicomParser.findEndOfEncapsulatedElement: missing required parameter 'element'"; } - byteStream.seek(length); - } + element.encapsulatedPixelData = true; + element.basicOffsetTable = []; + element.fragments = []; + var basicOffsetTableItemTag = dicomParser.readTag(byteStream); + if(basicOffsetTableItemTag !== 'xfffee000') { + throw "dicomParser.findEndOfEncapsulatedElement: basic offset table not found"; + } + var basicOffsetTableItemlength = byteStream.readUint32(); + var numFragments = basicOffsetTableItemlength / 4; + for(var i =0; i < numFragments; i++) { + var offset = byteStream.readUint32(); + element.basicOffsetTable.push(offset); + } + var baseOffset = byteStream.position; - if(warnings) { - warnings.push("pixel data element " + element.tag + " missing sequence delimiter tag xfffee0dd"); - } + while(byteStream.position < byteStream.byteArray.length) + { + var tag = dicomParser.readTag(byteStream); + var length = byteStream.readUint32(); + if(tag === 'xfffee0dd') + { + byteStream.seek(length); + element.length = byteStream.position - element.dataOffset; + return; + } + else if(tag === 'xfffee000') + { + element.fragments.push({ + offset: byteStream.position - baseOffset - 8, + position : byteStream.position, + length : length + }); + } + else { + if(warnings) { + warnings.push('unexpected tag ' + tag + ' while searching for end of pixel data element with undefined length'); + } + if(length > byteStream.byteArray.length - byteStream.position) + { + // fix length + length = byteStream.byteArray.length - byteStream.position; + } + element.fragments.push({ + offset: byteStream.position - baseOffset - 8, + position : byteStream.position, + length : length + }); + byteStream.seek(length); + element.length = byteStream.position - element.dataOffset; + return; + } + + byteStream.seek(length); + } + + if(warnings) { + warnings.push("pixel data element " + element.tag + " missing sequence delimiter tag xfffee0dd"); + } }; return dicomParser; - }(dicomParser)); - /** - * Internal helper functions for parsing DICOM elements - */ +}(dicomParser)); +/** + * Internal helper functions for parsing DICOM elements + */ - var dicomParser = (function (dicomParser) - { +var dicomParser = (function (dicomParser) +{ "use strict"; if(dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } /** @@ -1442,228 +1422,228 @@ */ dicomParser.findItemDelimitationItemAndSetElementLength = function(byteStream, element) { - if(byteStream === undefined) - { - throw "dicomParser.readDicomElementImplicit: missing required parameter 'byteStream'"; - } - - var itemDelimitationItemLength = 8; // group, element, length - var maxPosition = byteStream.byteArray.length - itemDelimitationItemLength; - while(byteStream.position <= maxPosition) - { - var groupNumber = byteStream.readUint16(); - if(groupNumber === 0xfffe) + if(byteStream === undefined) { - var elementNumber = byteStream.readUint16(); - if(elementNumber === 0xe00d) - { - // NOTE: It would be better to also check for the length to be 0 as part of the check above - // but we will just log a warning for now - var itemDelimiterLength = byteStream.readUint32(); // the length - if(itemDelimiterLength !== 0) { - byteStream.warnings('encountered non zero length following item delimiter at position' + byteStream.position - 4 + " while reading element of undefined length with tag ' + element.tag"); - } - element.length = byteStream.position - element.dataOffset; - return; - } + throw "dicomParser.readDicomElementImplicit: missing required parameter 'byteStream'"; } - } - // No item delimitation item - silently set the length to the end of the buffer and set the position past the end of the buffer - element.length = byteStream.byteArray.length - element.dataOffset; - byteStream.seek(byteStream.byteArray.length - byteStream.position); + var itemDelimitationItemLength = 8; // group, element, length + var maxPosition = byteStream.byteArray.length - itemDelimitationItemLength; + while(byteStream.position <= maxPosition) + { + var groupNumber = byteStream.readUint16(); + if(groupNumber === 0xfffe) + { + var elementNumber = byteStream.readUint16(); + if(elementNumber === 0xe00d) + { + // NOTE: It would be better to also check for the length to be 0 as part of the check above + // but we will just log a warning for now + var itemDelimiterLength = byteStream.readUint32(); // the length + if(itemDelimiterLength !== 0) { + byteStream.warnings('encountered non zero length following item delimiter at position' + byteStream.position - 4 + " while reading element of undefined length with tag ' + element.tag"); + } + element.length = byteStream.position - element.dataOffset; + return; + } + } + } + + // No item delimitation item - silently set the length to the end of the buffer and set the position past the end of the buffer + element.length = byteStream.byteArray.length - element.dataOffset; + byteStream.seek(byteStream.byteArray.length - byteStream.position); }; return dicomParser; - }(dicomParser)); - /** - * Internal helper functions for parsing different types from a little-endian byte array - */ +}(dicomParser)); +/** + * Internal helper functions for parsing different types from a little-endian byte array + */ - var dicomParser = (function (dicomParser) - { +var dicomParser = (function (dicomParser) +{ "use strict"; if(dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } dicomParser.littleEndianByteArrayParser = { - /** - * - * Parses an unsigned int 16 from a little-endian byte array - * - * @param byteArray the byte array to read from - * @param position the position in the byte array to read from - * @returns {*} the parsed unsigned int 16 - * @throws error if buffer overread would occur - * @access private - */ - readUint16: function (byteArray, position) { - if (position < 0) { - throw 'littleEndianByteArrayParser.readUint16: position cannot be less than 0'; + /** + * + * Parses an unsigned int 16 from a little-endian byte array + * + * @param byteArray the byte array to read from + * @param position the position in the byte array to read from + * @returns {*} the parsed unsigned int 16 + * @throws error if buffer overread would occur + * @access private + */ + readUint16: function (byteArray, position) { + if (position < 0) { + throw 'littleEndianByteArrayParser.readUint16: position cannot be less than 0'; + } + if (position + 2 > byteArray.length) { + throw 'littleEndianByteArrayParser.readUint16: attempt to read past end of buffer'; + } + return byteArray[position] + (byteArray[position + 1] * 256); + }, + + /** + * + * Parses a signed int 16 from a little-endian byte array + * + * @param byteArray the byte array to read from + * @param position the position in the byte array to read from + * @returns {*} the parsed signed int 16 + * @throws error if buffer overread would occur + * @access private + */ + readInt16: function (byteArray, position) { + if (position < 0) { + throw 'littleEndianByteArrayParser.readInt16: position cannot be less than 0'; + } + if (position + 2 > byteArray.length) { + throw 'littleEndianByteArrayParser.readInt16: attempt to read past end of buffer'; + } + var int16 = byteArray[position] + (byteArray[position + 1] << 8); + // fix sign + if (int16 & 0x8000) { + int16 = int16 - 0xFFFF - 1; + } + return int16; + }, + + + /** + * Parses an unsigned int 32 from a little-endian byte array + * + * @param byteArray the byte array to read from + * @param position the position in the byte array to read from + * @returns {*} the parsed unsigned int 32 + * @throws error if buffer overread would occur + * @access private + */ + readUint32: function (byteArray, position) { + if (position < 0) { + throw 'littleEndianByteArrayParser.readUint32: position cannot be less than 0'; + } + + if (position + 4 > byteArray.length) { + throw 'littleEndianByteArrayParser.readUint32: attempt to read past end of buffer'; + } + + var uint32 = (byteArray[position] + + (byteArray[position + 1] * 256) + + (byteArray[position + 2] * 256 * 256) + + (byteArray[position + 3] * 256 * 256 * 256 )); + + return uint32; + }, + + /** + * Parses a signed int 32 from a little-endian byte array + * + * @param byteArray the byte array to read from + * @param position the position in the byte array to read from + * @returns {*} the parsed unsigned int 32 + * @throws error if buffer overread would occur + * @access private + */ + readInt32: function (byteArray, position) { + if (position < 0) { + throw 'littleEndianByteArrayParser.readInt32: position cannot be less than 0'; + } + + if (position + 4 > byteArray.length) { + throw 'littleEndianByteArrayParser.readInt32: attempt to read past end of buffer'; + } + + var int32 = (byteArray[position] + + (byteArray[position + 1] << 8) + + (byteArray[position + 2] << 16) + + (byteArray[position + 3] << 24)); + + return int32; + + }, + + /** + * Parses 32-bit float from a little-endian byte array + * + * @param byteArray the byte array to read from + * @param position the position in the byte array to read from + * @returns {*} the parsed 32-bit float + * @throws error if buffer overread would occur + * @access private + */ + readFloat: function (byteArray, position) { + if (position < 0) { + throw 'littleEndianByteArrayParser.readFloat: position cannot be less than 0'; + } + + if (position + 4 > byteArray.length) { + throw 'littleEndianByteArrayParser.readFloat: attempt to read past end of buffer'; + } + + // I am sure there is a better way than this but this should be safe + var byteArrayForParsingFloat = new Uint8Array(4); + byteArrayForParsingFloat[0] = byteArray[position]; + byteArrayForParsingFloat[1] = byteArray[position + 1]; + byteArrayForParsingFloat[2] = byteArray[position + 2]; + byteArrayForParsingFloat[3] = byteArray[position + 3]; + var floatArray = new Float32Array(byteArrayForParsingFloat.buffer); + return floatArray[0]; + }, + + /** + * Parses 64-bit float from a little-endian byte array + * + * @param byteArray the byte array to read from + * @param position the position in the byte array to read from + * @returns {*} the parsed 64-bit float + * @throws error if buffer overread would occur + * @access private + */ + readDouble: function (byteArray, position) { + if (position < 0) { + throw 'littleEndianByteArrayParser.readDouble: position cannot be less than 0'; + } + + if (position + 8 > byteArray.length) { + throw 'littleEndianByteArrayParser.readDouble: attempt to read past end of buffer'; + } + + // I am sure there is a better way than this but this should be safe + var byteArrayForParsingFloat = new Uint8Array(8); + byteArrayForParsingFloat[0] = byteArray[position]; + byteArrayForParsingFloat[1] = byteArray[position + 1]; + byteArrayForParsingFloat[2] = byteArray[position + 2]; + byteArrayForParsingFloat[3] = byteArray[position + 3]; + byteArrayForParsingFloat[4] = byteArray[position + 4]; + byteArrayForParsingFloat[5] = byteArray[position + 5]; + byteArrayForParsingFloat[6] = byteArray[position + 6]; + byteArrayForParsingFloat[7] = byteArray[position + 7]; + var floatArray = new Float64Array(byteArrayForParsingFloat.buffer); + return floatArray[0]; } - if (position + 2 > byteArray.length) { - throw 'littleEndianByteArrayParser.readUint16: attempt to read past end of buffer'; - } - return byteArray[position] + (byteArray[position + 1] * 256); - }, - - /** - * - * Parses a signed int 16 from a little-endian byte array - * - * @param byteArray the byte array to read from - * @param position the position in the byte array to read from - * @returns {*} the parsed signed int 16 - * @throws error if buffer overread would occur - * @access private - */ - readInt16: function (byteArray, position) { - if (position < 0) { - throw 'littleEndianByteArrayParser.readInt16: position cannot be less than 0'; - } - if (position + 2 > byteArray.length) { - throw 'littleEndianByteArrayParser.readInt16: attempt to read past end of buffer'; - } - var int16 = byteArray[position] + (byteArray[position + 1] << 8); - // fix sign - if (int16 & 0x8000) { - int16 = int16 - 0xFFFF - 1; - } - return int16; - }, - - - /** - * Parses an unsigned int 32 from a little-endian byte array - * - * @param byteArray the byte array to read from - * @param position the position in the byte array to read from - * @returns {*} the parsed unsigned int 32 - * @throws error if buffer overread would occur - * @access private - */ - readUint32: function (byteArray, position) { - if (position < 0) { - throw 'littleEndianByteArrayParser.readUint32: position cannot be less than 0'; - } - - if (position + 4 > byteArray.length) { - throw 'littleEndianByteArrayParser.readUint32: attempt to read past end of buffer'; - } - - var uint32 = (byteArray[position] + - (byteArray[position + 1] * 256) + - (byteArray[position + 2] * 256 * 256) + - (byteArray[position + 3] * 256 * 256 * 256 )); - - return uint32; - }, - - /** - * Parses a signed int 32 from a little-endian byte array - * - * @param byteArray the byte array to read from - * @param position the position in the byte array to read from - * @returns {*} the parsed unsigned int 32 - * @throws error if buffer overread would occur - * @access private - */ - readInt32: function (byteArray, position) { - if (position < 0) { - throw 'littleEndianByteArrayParser.readInt32: position cannot be less than 0'; - } - - if (position + 4 > byteArray.length) { - throw 'littleEndianByteArrayParser.readInt32: attempt to read past end of buffer'; - } - - var int32 = (byteArray[position] + - (byteArray[position + 1] << 8) + - (byteArray[position + 2] << 16) + - (byteArray[position + 3] << 24)); - - return int32; - - }, - - /** - * Parses 32-bit float from a little-endian byte array - * - * @param byteArray the byte array to read from - * @param position the position in the byte array to read from - * @returns {*} the parsed 32-bit float - * @throws error if buffer overread would occur - * @access private - */ - readFloat: function (byteArray, position) { - if (position < 0) { - throw 'littleEndianByteArrayParser.readFloat: position cannot be less than 0'; - } - - if (position + 4 > byteArray.length) { - throw 'littleEndianByteArrayParser.readFloat: attempt to read past end of buffer'; - } - - // I am sure there is a better way than this but this should be safe - var byteArrayForParsingFloat = new Uint8Array(4); - byteArrayForParsingFloat[0] = byteArray[position]; - byteArrayForParsingFloat[1] = byteArray[position + 1]; - byteArrayForParsingFloat[2] = byteArray[position + 2]; - byteArrayForParsingFloat[3] = byteArray[position + 3]; - var floatArray = new Float32Array(byteArrayForParsingFloat.buffer); - return floatArray[0]; - }, - - /** - * Parses 64-bit float from a little-endian byte array - * - * @param byteArray the byte array to read from - * @param position the position in the byte array to read from - * @returns {*} the parsed 64-bit float - * @throws error if buffer overread would occur - * @access private - */ - readDouble: function (byteArray, position) { - if (position < 0) { - throw 'littleEndianByteArrayParser.readDouble: position cannot be less than 0'; - } - - if (position + 8 > byteArray.length) { - throw 'littleEndianByteArrayParser.readDouble: attempt to read past end of buffer'; - } - - // I am sure there is a better way than this but this should be safe - var byteArrayForParsingFloat = new Uint8Array(8); - byteArrayForParsingFloat[0] = byteArray[position]; - byteArrayForParsingFloat[1] = byteArray[position + 1]; - byteArrayForParsingFloat[2] = byteArray[position + 2]; - byteArrayForParsingFloat[3] = byteArray[position + 3]; - byteArrayForParsingFloat[4] = byteArray[position + 4]; - byteArrayForParsingFloat[5] = byteArray[position + 5]; - byteArrayForParsingFloat[6] = byteArray[position + 6]; - byteArrayForParsingFloat[7] = byteArray[position + 7]; - var floatArray = new Float64Array(byteArrayForParsingFloat.buffer); - return floatArray[0]; - } }; return dicomParser; - }(dicomParser)); - /** - * Internal helper functions for parsing implicit and explicit DICOM data sets - */ +}(dicomParser)); +/** + * Internal helper functions for parsing implicit and explicit DICOM data sets + */ - var dicomParser = (function (dicomParser) - { +var dicomParser = (function (dicomParser) +{ "use strict"; if(dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } /** @@ -1673,30 +1653,30 @@ */ dicomParser.parseDicomDataSetExplicit = function (dataSet, byteStream, maxPosition, options) { - maxPosition = (maxPosition === undefined) ? byteStream.byteArray.length : maxPosition ; - options = options || {}; + maxPosition = (maxPosition === undefined) ? byteStream.byteArray.length : maxPosition ; + options = options || {}; - if(byteStream === undefined) - { - throw "dicomParser.parseDicomDataSetExplicit: missing required parameter 'byteStream'"; - } - if(maxPosition < byteStream.position || maxPosition > byteStream.byteArray.length) - { - throw "dicomParser.parseDicomDataSetExplicit: invalid value for parameter 'maxPosition'"; - } - var elements = dataSet.elements; - - while(byteStream.position < maxPosition) - { - var element = dicomParser.readDicomElementExplicit(byteStream, dataSet.warnings, options.untilTag); - elements[element.tag] = element; - if(element.tag === options.untilTag) { - return; + if(byteStream === undefined) + { + throw "dicomParser.parseDicomDataSetExplicit: missing required parameter 'byteStream'"; + } + if(maxPosition < byteStream.position || maxPosition > byteStream.byteArray.length) + { + throw "dicomParser.parseDicomDataSetExplicit: invalid value for parameter 'maxPosition'"; + } + var elements = dataSet.elements; + + while(byteStream.position < maxPosition) + { + var element = dicomParser.readDicomElementExplicit(byteStream, dataSet.warnings, options.untilTag); + elements[element.tag] = element; + if(element.tag === options.untilTag) { + return; + } + } + if(byteStream.position > maxPosition) { + throw "dicomParser:parseDicomDataSetExplicit: buffer overrun"; } - } - if(byteStream.position > maxPosition) { - throw "dicomParser:parseDicomDataSetExplicit: buffer overrun"; - } }; /** @@ -1706,385 +1686,454 @@ */ dicomParser.parseDicomDataSetImplicit = function(dataSet, byteStream, maxPosition, options) { - maxPosition = (maxPosition === undefined) ? dataSet.byteArray.length : maxPosition ; - options = options || {}; + maxPosition = (maxPosition === undefined) ? dataSet.byteArray.length : maxPosition ; + options = options || {}; - if(byteStream === undefined) - { - throw "dicomParser.parseDicomDataSetImplicit: missing required parameter 'byteStream'"; - } - if(maxPosition < byteStream.position || maxPosition > byteStream.byteArray.length) - { - throw "dicomParser.parseDicomDataSetImplicit: invalid value for parameter 'maxPosition'"; - } - - var elements = dataSet.elements; - - while(byteStream.position < maxPosition) - { - var element = dicomParser.readDicomElementImplicit(byteStream, options.untilTag); - elements[element.tag] = element; - if(element.tag === options.untilTag) { - return; + if(byteStream === undefined) + { + throw "dicomParser.parseDicomDataSetImplicit: missing required parameter 'byteStream'"; + } + if(maxPosition < byteStream.position || maxPosition > byteStream.byteArray.length) + { + throw "dicomParser.parseDicomDataSetImplicit: invalid value for parameter 'maxPosition'"; + } + + var elements = dataSet.elements; + + while(byteStream.position < maxPosition) + { + var element = dicomParser.readDicomElementImplicit(byteStream, options.untilTag); + elements[element.tag] = element; + if(element.tag === options.untilTag) { + return; + } } - } }; return dicomParser; - }(dicomParser)); +}(dicomParser)); - /** - * Internal helper functions for for parsing DICOM elements - */ +/** + * Internal helper functions for for parsing DICOM elements + */ - var dicomParser = (function (dicomParser) - { +var dicomParser = (function (dicomParser) +{ "use strict"; if(dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } function getDataLengthSizeInBytesForVR(vr) { - if( vr === 'OB' || - vr === 'OW' || - vr === 'SQ' || - vr === 'OF' || - vr === 'UT' || - vr === 'UN') - { - return 4; - } - else - { - return 2; - } + if( vr === 'OB' || + vr === 'OW' || + vr === 'SQ' || + vr === 'OF' || + vr === 'UT' || + vr === 'UN') + { + return 4; + } + else + { + return 2; + } } dicomParser.readDicomElementExplicit = function(byteStream, warnings, untilTag) { - if(byteStream === undefined) - { - throw "dicomParser.readDicomElementExplicit: missing required parameter 'byteStream'"; - } - - var element = { - tag : dicomParser.readTag(byteStream), - vr : byteStream.readFixedString(2) - // length set below based on VR - // dataOffset set below based on VR and size of length - }; - - var dataLengthSizeBytes = getDataLengthSizeInBytesForVR(element.vr); - if(dataLengthSizeBytes === 2) - { - element.length = byteStream.readUint16(); - element.dataOffset = byteStream.position; - } - else - { - byteStream.seek(2); - element.length = byteStream.readUint32(); - element.dataOffset = byteStream.position; - } - - if(element.length === 4294967295) - { - element.hadUndefinedLength = true; - } - - if(element.tag === untilTag) { - return element; - } - - // if VR is SQ, parse the sequence items - if(element.vr === 'SQ') - { - dicomParser.readSequenceItemsExplicit(byteStream, element, warnings); - return element; - } - if(element.length === 4294967295) - { - if(element.tag === 'x7fe00010') { - dicomParser.findEndOfEncapsulatedElement(byteStream, element, warnings); - return element; - } else { - dicomParser.findItemDelimitationItemAndSetElementLength(byteStream, element); - return element; + if(byteStream === undefined) + { + throw "dicomParser.readDicomElementExplicit: missing required parameter 'byteStream'"; } - } - byteStream.seek(element.length); - return element; + var element = { + tag : dicomParser.readTag(byteStream), + vr : byteStream.readFixedString(2) + // length set below based on VR + // dataOffset set below based on VR and size of length + }; + + var dataLengthSizeBytes = getDataLengthSizeInBytesForVR(element.vr); + if(dataLengthSizeBytes === 2) + { + element.length = byteStream.readUint16(); + element.dataOffset = byteStream.position; + } + else + { + byteStream.seek(2); + element.length = byteStream.readUint32(); + element.dataOffset = byteStream.position; + } + + if(element.length === 4294967295) + { + element.hadUndefinedLength = true; + } + + if(element.tag === untilTag) { + return element; + } + + // if VR is SQ, parse the sequence items + if(element.vr === 'SQ') + { + dicomParser.readSequenceItemsExplicit(byteStream, element, warnings); + return element; + } + if(element.length === 4294967295) + { + if(element.tag === 'x7fe00010') { + dicomParser.findEndOfEncapsulatedElement(byteStream, element, warnings); + return element; + } else { + dicomParser.findItemDelimitationItemAndSetElementLength(byteStream, element); + return element; + } + } + + byteStream.seek(element.length); + return element; }; return dicomParser; - }(dicomParser)); - /** - * Internal helper functions for for parsing DICOM elements - */ +}(dicomParser)); +/** + * Internal helper functions for for parsing DICOM elements + */ - var dicomParser = (function (dicomParser) - { +var dicomParser = (function (dicomParser) +{ "use strict"; if(dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } dicomParser.readDicomElementImplicit = function(byteStream, untilTag) { - if(byteStream === undefined) - { - throw "dicomParser.readDicomElementImplicit: missing required parameter 'byteStream'"; - } - - var element = { - tag : dicomParser.readTag(byteStream), - length: byteStream.readUint32(), - dataOffset : byteStream.position - }; - - if(element.length === 4294967295) - { - element.hadUndefinedLength = true; - } - - if(element.tag === untilTag) { - return element; - } - - // peek ahead at the next tag to see if it looks like a sequence. This is not 100% - // safe because a non sequence item could have data that has these bytes, but this - // is how to do it without a data dictionary. - if ((byteStream.position + 4) <= byteStream.byteArray.length) { - var nextTag = dicomParser.readTag(byteStream); - byteStream.seek(-4); - - // zero length sequence - if (element.hadUndefinedLength && nextTag === 'xfffee0dd') { - element.length = 0; - byteStream.seek(8); - return element; + if(byteStream === undefined) + { + throw "dicomParser.readDicomElementImplicit: missing required parameter 'byteStream'"; } - if (nextTag === 'xfffee000') { - // parse the sequence - dicomParser.readSequenceItemsImplicit(byteStream, element); - //element.length = byteStream.byteArray.length - element.dataOffset; - return element; + var element = { + tag : dicomParser.readTag(byteStream), + length: byteStream.readUint32(), + dataOffset : byteStream.position + }; + + if(element.length === 4294967295) + { + element.hadUndefinedLength = true; } - } - // if element is not a sequence and has undefined length, we have to - // scan the data for a magic number to figure out when it ends. - if(element.length === 4294967295) - { - dicomParser.findItemDelimitationItemAndSetElementLength(byteStream, element); + if(element.tag === untilTag) { + return element; + } + + // peek ahead at the next tag to see if it looks like a sequence. This is not 100% + // safe because a non sequence item could have data that has these bytes, but this + // is how to do it without a data dictionary. + if ((byteStream.position + 4) <= byteStream.byteArray.length) { + var nextTag = dicomParser.readTag(byteStream); + byteStream.seek(-4); + + // zero length sequence + if (element.hadUndefinedLength && nextTag === 'xfffee0dd') { + element.length = 0; + byteStream.seek(8); + return element; + } + + if (nextTag === 'xfffee000') { + // parse the sequence + dicomParser.readSequenceItemsImplicit(byteStream, element); + //element.length = byteStream.byteArray.length - element.dataOffset; + return element; + } + } + + // if element is not a sequence and has undefined length, we have to + // scan the data for a magic number to figure out when it ends. + if(element.length === 4294967295) + { + dicomParser.findItemDelimitationItemAndSetElementLength(byteStream, element); + return element; + } + + // non sequence element with known length, skip over the data part + byteStream.seek(element.length); return element; - } - - // non sequence element with known length, skip over the data part - byteStream.seek(element.length); - return element; }; return dicomParser; - }(dicomParser)); - /** - * Internal helper functions for parsing DICOM elements - */ - - var dicomParser = (function (dicomParser) +}(dicomParser)); +/** + * Parses a DICOM P10 byte array and returns a DataSet object with the parsed elements. If the options + * argument is supplied and it contains the untilTag property, parsing will stop once that + * tag is encoutered. This can be used to parse partial byte streams. + * + * @param byteArray the byte array + * @param options object to control parsing behavior (optional) + * @returns {DataSet} + * @throws error if an error occurs while parsing. The exception object will contain a property dataSet with the + * elements successfully parsed before the error. + */ +var dicomParser = (function(dicomParser) { + if(dicomParser === undefined) { + dicomParser = {}; + } + + dicomParser.readPart10Header = function(byteArray, options) { + + if(byteArray === undefined) + { + throw "dicomParser.readPart10Header: missing required parameter 'byteArray'"; + } + + var littleEndianByteStream = new dicomParser.ByteStream(dicomParser.littleEndianByteArrayParser, byteArray); + + function readPrefix() + { + littleEndianByteStream.seek(128); + var prefix = littleEndianByteStream.readFixedString(4); + if(prefix !== "DICM") + { + throw "dicomParser.readPart10Header: DICM prefix not found at location 132"; + } + } + + // main function here + function readTheHeader() { + // Per the DICOM standard, the header is always encoded in Explicit VR Little Endian (see PS3.10, section 7.1) + // so use littleEndianByteStream throughout this method regardless of the transfer syntax + readPrefix(); + + var warnings = []; + var elements = {}; + while(littleEndianByteStream.position < littleEndianByteStream.byteArray.length) { + var position = littleEndianByteStream.position; + var element = dicomParser.readDicomElementExplicit(littleEndianByteStream, warnings); + if(element.tag > 'x0002ffff') { + littleEndianByteStream.position = position; + break; + } + // Cache the littleEndianByteArrayParser for meta header elements, since the rest of the data set may be big endian + // and this parser will be needed later if the meta header values are to be read. + element.parser = dicomParser.littleEndianByteArrayParser; + elements[element.tag] = element; + } + var metaHeaderDataSet = new dicomParser.DataSet(littleEndianByteStream.byteArrayParser, littleEndianByteStream.byteArray, elements); + metaHeaderDataSet.warnings = littleEndianByteStream.warnings; + metaHeaderDataSet.position = littleEndianByteStream.position; + return metaHeaderDataSet; + } + + // This is where we actually start parsing + return readTheHeader(); + }; + + return dicomParser; +})(dicomParser); + +/** + * Internal helper functions for parsing DICOM elements + */ + +var dicomParser = (function (dicomParser) +{ "use strict"; if(dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } function readDicomDataSetExplicitUndefinedLength(byteStream, warnings) { - var elements = {}; + var elements = {}; - while(byteStream.position < byteStream.byteArray.length) - { - var element = dicomParser.readDicomElementExplicit(byteStream, warnings); - elements[element.tag] = element; - - // we hit an item delimiter tag, return the current offset to mark - // the end of this sequence item - if(element.tag === 'xfffee00d') + while(byteStream.position < byteStream.byteArray.length) { - return new dicomParser.DataSet(byteStream.byteArrayParser, byteStream.byteArray, elements); + var element = dicomParser.readDicomElementExplicit(byteStream, warnings); + elements[element.tag] = element; + + // we hit an item delimiter tag, return the current offset to mark + // the end of this sequence item + if(element.tag === 'xfffee00d') + { + return new dicomParser.DataSet(byteStream.byteArrayParser, byteStream.byteArray, elements); + } + } - } - - // eof encountered - log a warning and return what we have for the element - byteStream.warnings.push('eof encountered before finding sequence delimitation item while reading sequence item of undefined length'); - return new dicomParser.DataSet(byteStream.byteArrayParser, byteStream.byteArray, elements); + // eof encountered - log a warning and return what we have for the element + byteStream.warnings.push('eof encountered before finding sequence delimitation item while reading sequence item of undefined length'); + return new dicomParser.DataSet(byteStream.byteArrayParser, byteStream.byteArray, elements); } function readSequenceItemExplicit(byteStream, warnings) { - var item = dicomParser.readSequenceItem(byteStream); + var item = dicomParser.readSequenceItem(byteStream); - if(item.length === 4294967295) - { - item.hadUndefinedLength = true; - item.dataSet = readDicomDataSetExplicitUndefinedLength(byteStream, warnings); - item.length = byteStream.position - item.dataOffset; - } - else - { - item.dataSet = new dicomParser.DataSet(byteStream.byteArrayParser, byteStream.byteArray, {}); - dicomParser.parseDicomDataSetExplicit(item.dataSet, byteStream, byteStream.position + item.length); - } - return item; + if(item.length === 4294967295) + { + item.hadUndefinedLength = true; + item.dataSet = readDicomDataSetExplicitUndefinedLength(byteStream, warnings); + item.length = byteStream.position - item.dataOffset; + } + else + { + item.dataSet = new dicomParser.DataSet(byteStream.byteArrayParser, byteStream.byteArray, {}); + dicomParser.parseDicomDataSetExplicit(item.dataSet, byteStream, byteStream.position + item.length); + } + return item; } function readSQElementUndefinedLengthExplicit(byteStream, element, warnings) { - while(byteStream.position < byteStream.byteArray.length) - { - // end reading this sequence if the next tag is the sequence delimitation item - var nextTag = dicomParser.readTag(byteStream); - byteStream.seek(-4); - if (nextTag === 'xfffee0dd') { - // set the correct length - element.length = byteStream.position - element.dataOffset; - byteStream.seek(8); - return element; - } + while(byteStream.position < byteStream.byteArray.length) + { + // end reading this sequence if the next tag is the sequence delimitation item + var nextTag = dicomParser.readTag(byteStream); + byteStream.seek(-4); + if (nextTag === 'xfffee0dd') { + // set the correct length + element.length = byteStream.position - element.dataOffset; + byteStream.seek(8); + return element; + } - var item = readSequenceItemExplicit(byteStream, warnings); - element.items.push(item); - } - element.length = byteStream.position - element.dataOffset; + var item = readSequenceItemExplicit(byteStream, warnings); + element.items.push(item); + } + element.length = byteStream.position - element.dataOffset; } function readSQElementKnownLengthExplicit(byteStream, element, warnings) { - var maxPosition = element.dataOffset + element.length; - while(byteStream.position < maxPosition) - { - var item = readSequenceItemExplicit(byteStream, warnings); - element.items.push(item); - } + var maxPosition = element.dataOffset + element.length; + while(byteStream.position < maxPosition) + { + var item = readSequenceItemExplicit(byteStream, warnings); + element.items.push(item); + } } dicomParser.readSequenceItemsExplicit = function(byteStream, element, warnings) { - if(byteStream === undefined) - { - throw "dicomParser.readSequenceItemsExplicit: missing required parameter 'byteStream'"; - } - if(element === undefined) - { - throw "dicomParser.readSequenceItemsExplicit: missing required parameter 'element'"; - } + if(byteStream === undefined) + { + throw "dicomParser.readSequenceItemsExplicit: missing required parameter 'byteStream'"; + } + if(element === undefined) + { + throw "dicomParser.readSequenceItemsExplicit: missing required parameter 'element'"; + } - element.items = []; + element.items = []; - if(element.length === 4294967295) - { - readSQElementUndefinedLengthExplicit(byteStream, element); - } - else - { - readSQElementKnownLengthExplicit(byteStream, element, warnings); - } + if(element.length === 4294967295) + { + readSQElementUndefinedLengthExplicit(byteStream, element); + } + else + { + readSQElementKnownLengthExplicit(byteStream, element, warnings); + } }; return dicomParser; - }(dicomParser)); - /** - * Internal helper functions for parsing DICOM elements - */ +}(dicomParser)); +/** + * Internal helper functions for parsing DICOM elements + */ - var dicomParser = (function (dicomParser) - { +var dicomParser = (function (dicomParser) +{ "use strict"; if(dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } function readDicomDataSetImplicitUndefinedLength(byteStream) { - var elements = {}; + var elements = {}; - while(byteStream.position < byteStream.byteArray.length) - { - var element = dicomParser.readDicomElementImplicit(byteStream); - elements[element.tag] = element; - - // we hit an item delimiter tag, return the current offset to mark - // the end of this sequence item - if(element.tag === 'xfffee00d') + while(byteStream.position < byteStream.byteArray.length) { - return new dicomParser.DataSet(byteStream.byteArrayParser, byteStream.byteArray, elements); + var element = dicomParser.readDicomElementImplicit(byteStream); + elements[element.tag] = element; + + // we hit an item delimiter tag, return the current offset to mark + // the end of this sequence item + if(element.tag === 'xfffee00d') + { + return new dicomParser.DataSet(byteStream.byteArrayParser, byteStream.byteArray, elements); + } } - } - // eof encountered - log a warning and return what we have for the element - byteStream.warnings.push('eof encountered before finding sequence item delimiter in sequence item of undefined length'); - return new dicomParser.DataSet(byteStream.byteArrayParser, byteStream.byteArray, elements); + // eof encountered - log a warning and return what we have for the element + byteStream.warnings.push('eof encountered before finding sequence item delimiter in sequence item of undefined length'); + return new dicomParser.DataSet(byteStream.byteArrayParser, byteStream.byteArray, elements); } function readSequenceItemImplicit(byteStream) { - var item = dicomParser.readSequenceItem(byteStream); + var item = dicomParser.readSequenceItem(byteStream); - if(item.length === 4294967295) - { - item.hadUndefinedLength = true; - item.dataSet = readDicomDataSetImplicitUndefinedLength(byteStream); - item.length = byteStream.position - item.dataOffset; - } - else - { - item.dataSet = new dicomParser.DataSet(byteStream.byteArrayParser, byteStream.byteArray, {}); - dicomParser.parseDicomDataSetImplicit(item.dataSet, byteStream, byteStream.position + item.length); - } - return item; + if(item.length === 4294967295) + { + item.hadUndefinedLength = true; + item.dataSet = readDicomDataSetImplicitUndefinedLength(byteStream); + item.length = byteStream.position - item.dataOffset; + } + else + { + item.dataSet = new dicomParser.DataSet(byteStream.byteArrayParser, byteStream.byteArray, {}); + dicomParser.parseDicomDataSetImplicit(item.dataSet, byteStream, byteStream.position + item.length); + } + return item; } function readSQElementUndefinedLengthImplicit(byteStream, element) { - while(byteStream.position < byteStream.byteArray.length) - { - // end reading this sequence if the next tag is the sequence delimitation item - var nextTag = dicomParser.readTag(byteStream); - byteStream.seek(-4); - if (nextTag === 'xfffee0dd') { - // set the correct length - element.length = byteStream.position - element.dataOffset; - byteStream.seek(8); - return element; - } + while(byteStream.position < byteStream.byteArray.length) + { + // end reading this sequence if the next tag is the sequence delimitation item + var nextTag = dicomParser.readTag(byteStream); + byteStream.seek(-4); + if (nextTag === 'xfffee0dd') { + // set the correct length + element.length = byteStream.position - element.dataOffset; + byteStream.seek(8); + return element; + } - var item = readSequenceItemImplicit(byteStream); - element.items.push(item); - } - element.length = byteStream.byteArray.length - element.dataOffset; + var item = readSequenceItemImplicit(byteStream); + element.items.push(item); + } + element.length = byteStream.byteArray.length - element.dataOffset; } function readSQElementKnownLengthImplicit(byteStream, element) { - var maxPosition = element.dataOffset + element.length; - while(byteStream.position < maxPosition) - { - var item = readSequenceItemImplicit(byteStream); - element.items.push(item); - } + var maxPosition = element.dataOffset + element.length; + while(byteStream.position < maxPosition) + { + var item = readSequenceItemImplicit(byteStream); + element.items.push(item); + } } /** @@ -2094,40 +2143,40 @@ */ dicomParser.readSequenceItemsImplicit = function(byteStream, element) { - if(byteStream === undefined) - { - throw "dicomParser.readSequenceItemsImplicit: missing required parameter 'byteStream'"; - } - if(element === undefined) - { - throw "dicomParser.readSequenceItemsImplicit: missing required parameter 'element'"; - } + if(byteStream === undefined) + { + throw "dicomParser.readSequenceItemsImplicit: missing required parameter 'byteStream'"; + } + if(element === undefined) + { + throw "dicomParser.readSequenceItemsImplicit: missing required parameter 'element'"; + } - element.items = []; + element.items = []; - if(element.length === 4294967295) - { - readSQElementUndefinedLengthImplicit(byteStream, element); - } - else - { - readSQElementKnownLengthImplicit(byteStream, element); - } + if(element.length === 4294967295) + { + readSQElementUndefinedLengthImplicit(byteStream, element); + } + else + { + readSQElementKnownLengthImplicit(byteStream, element); + } }; return dicomParser; - }(dicomParser)); - /** - * Internal helper functions for parsing DICOM elements - */ +}(dicomParser)); +/** + * Internal helper functions for parsing DICOM elements + */ - var dicomParser = (function (dicomParser) - { +var dicomParser = (function (dicomParser) +{ "use strict"; if(dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } /** @@ -2140,34 +2189,34 @@ */ dicomParser.readSequenceItem = function(byteStream) { - if(byteStream === undefined) - { - throw "dicomParser.readSequenceItem: missing required parameter 'byteStream'"; - } + if(byteStream === undefined) + { + throw "dicomParser.readSequenceItem: missing required parameter 'byteStream'"; + } - var element = { - tag : dicomParser.readTag(byteStream), - length : byteStream.readUint32(), - dataOffset : byteStream.position - }; + var element = { + tag : dicomParser.readTag(byteStream), + length : byteStream.readUint32(), + dataOffset : byteStream.position + }; - return element; + return element; }; return dicomParser; - }(dicomParser)); - /** - * Internal helper functions for parsing DICOM elements - */ +}(dicomParser)); +/** + * Internal helper functions for parsing DICOM elements + */ - var dicomParser = (function (dicomParser) - { +var dicomParser = (function (dicomParser) +{ "use strict"; if(dicomParser === undefined) { - dicomParser = {}; + dicomParser = {}; } /** @@ -2178,35 +2227,35 @@ */ dicomParser.readTag = function(byteStream) { - if(byteStream === undefined) - { - throw "dicomParser.readTag: missing required parameter 'byteStream'"; - } + if(byteStream === undefined) + { + throw "dicomParser.readTag: missing required parameter 'byteStream'"; + } - var groupNumber = byteStream.readUint16() * 256 * 256; - var elementNumber = byteStream.readUint16(); - var tag = "x" + ('00000000' + (groupNumber + elementNumber).toString(16)).substr(-8); - return tag; + var groupNumber = byteStream.readUint16() * 256 * 256; + var elementNumber = byteStream.readUint16(); + var tag = "x" + ('00000000' + (groupNumber + elementNumber).toString(16)).substr(-8); + return tag; }; return dicomParser; - }(dicomParser)); - /** - * Version - */ +}(dicomParser)); +/** + * Version + */ - var dicomParser = (function (dicomParser) +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) { - "use strict"; + dicomParser = {}; + } - if(dicomParser === undefined) - { - dicomParser = {}; - } + dicomParser.version = "1.2.0"; - dicomParser.version = "1.1.5"; - - return dicomParser; - }(dicomParser)); return dicomParser; -})); \ No newline at end of file +}(dicomParser)); + return dicomParser; +})); diff --git a/Packages/cornerstone/client/hangingProtocol.js b/Packages/cornerstone/client/hangingProtocol.js index 591481964..6f5195c87 100644 --- a/Packages/cornerstone/client/hangingProtocol.js +++ b/Packages/cornerstone/client/hangingProtocol.js @@ -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 diff --git a/Packages/dicomweb/lib/findIndexOfString.js b/Packages/dicomweb/lib/findIndexOfString.js new file mode 100644 index 000000000..37eaf61f3 --- /dev/null +++ b/Packages/dicomweb/lib/findIndexOfString.js @@ -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 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; +}; \ No newline at end of file diff --git a/Packages/dicomweb/lib/uint8ArrayToString.js b/Packages/dicomweb/lib/uint8ArrayToString.js new file mode 100644 index 000000000..35b85655d --- /dev/null +++ b/Packages/dicomweb/lib/uint8ArrayToString.js @@ -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; +}; diff --git a/Packages/dicomweb/package.js b/Packages/dicomweb/package.js index a96d9fea4..eb8b4f101 100644 --- a/Packages/dicomweb/package.js +++ b/Packages/dicomweb/package.js @@ -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'); }); diff --git a/Packages/lesiontracker/compatibility/lesionTool.js b/Packages/lesiontracker/compatibility/lesionTool.js index 8b6b3b62b..fe7e91e63 100644 --- a/Packages/lesiontracker/compatibility/lesionTool.js +++ b/Packages/lesiontracker/compatibility/lesionTool.js @@ -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)); \ No newline at end of file diff --git a/Packages/lesiontracker/compatibility/measurementManagerDAL.js b/Packages/lesiontracker/compatibility/measurementManagerDAL.js new file mode 100644 index 000000000..b438bbf21 --- /dev/null +++ b/Packages/lesiontracker/compatibility/measurementManagerDAL.js @@ -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 + }; +})(); \ No newline at end of file diff --git a/Packages/lesiontracker/compatibility/nonTargetTool.js b/Packages/lesiontracker/compatibility/nonTargetTool.js new file mode 100644 index 000000000..b609b6a22 --- /dev/null +++ b/Packages/lesiontracker/compatibility/nonTargetTool.js @@ -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); diff --git a/Packages/lesiontracker/components/lesionDialog/lesionDialog.html b/Packages/lesiontracker/components/lesionDialog/lesionDialog.html deleted file mode 100644 index 369c7cd5a..000000000 --- a/Packages/lesiontracker/components/lesionDialog/lesionDialog.html +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/Packages/lesiontracker/components/lesionDialog/lesionDialog.js b/Packages/lesiontracker/components/lesionDialog/lesionDialog.js deleted file mode 100644 index 0d16ac861..000000000 --- a/Packages/lesiontracker/components/lesionDialog/lesionDialog.js +++ /dev/null @@ -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); - } - });*/ diff --git a/Packages/lesiontracker/components/lesionLocationDialog/lesionLocationDialog.html b/Packages/lesiontracker/components/lesionLocationDialog/lesionLocationDialog.html index 1f25b7950..b71e636f9 100644 --- a/Packages/lesiontracker/components/lesionLocationDialog/lesionLocationDialog.html +++ b/Packages/lesiontracker/components/lesionLocationDialog/lesionLocationDialog.html @@ -1,19 +1,17 @@