diff --git a/LesionTracker/.meteor/.finished-upgraders b/LesionTracker/.meteor/.finished-upgraders
new file mode 100644
index 000000000..61ee31323
--- /dev/null
+++ b/LesionTracker/.meteor/.finished-upgraders
@@ -0,0 +1,12 @@
+# This file contains information which helps Meteor properly upgrade your
+# app when you run 'meteor update'. You should check it into version control
+# with your project.
+
+notices-for-0.9.0
+notices-for-0.9.1
+0.9.4-platform-file
+notices-for-facebook-graph-api-2
+1.2.0-standard-minifiers-package
+1.2.0-meteor-platform-split
+1.2.0-cordova-changes
+1.2.0-breaking-changes
diff --git a/LesionTracker/.meteor/.gitignore b/LesionTracker/.meteor/.gitignore
new file mode 100644
index 000000000..408303742
--- /dev/null
+++ b/LesionTracker/.meteor/.gitignore
@@ -0,0 +1 @@
+local
diff --git a/LesionTracker/.meteor/.id b/LesionTracker/.meteor/.id
new file mode 100644
index 000000000..f2d5540e8
--- /dev/null
+++ b/LesionTracker/.meteor/.id
@@ -0,0 +1,7 @@
+# This file contains a token that is unique to your project.
+# Check it into your repository along with the rest of this directory.
+# It can be used for purposes such as:
+# - ensuring you don't accidentally deploy one app on top of another
+# - providing package authors with aggregated statistics
+
+1q3vtk41m2yk4k1rqj08l
diff --git a/LesionTracker/.meteor/packages b/LesionTracker/.meteor/packages
new file mode 100644
index 000000000..779a670d2
--- /dev/null
+++ b/LesionTracker/.meteor/packages
@@ -0,0 +1,34 @@
+# Meteor packages used by this project, one per line.
+# Check this file (and the other files in this directory) into your repository.
+#
+# 'meteor add' and 'meteor remove' will edit this file for you,
+# but you can also edit it by hand.
+
+meteor-base # Packages every Meteor app needs to have
+mobile-experience # Packages for a great mobile UX
+mongo # The database Meteor supports right now
+blaze-html-templates # Compile .html files into Meteor Blaze views
+session # Client-side reactive dictionary for your app
+jquery # Helpful client-side library
+tracker # Meteor's client-side reactive programming library
+
+standard-minifiers # JS/CSS minifiers run for production mode
+es5-shim # ECMAScript 5 compatibility for older browsers.
+ecmascript # Enable ECMAScript2015+ syntax in app code
+
+autopublish # Publish all data to the clients (for prototyping)
+insecure # Allow all DB writes from clients (for prototyping)
+cornerstone
+worklist
+viewerbase
+dicomweb
+accounts-password
+
+ian:accounts-ui-bootstrap-3
+iron:router
+twbs:bootstrap
+http
+arsnebula:reactive-promise
+promise
+stylus
+fortawesome:fontawesome
diff --git a/LesionTracker/.meteor/platforms b/LesionTracker/.meteor/platforms
new file mode 100644
index 000000000..efeba1b50
--- /dev/null
+++ b/LesionTracker/.meteor/platforms
@@ -0,0 +1,2 @@
+server
+browser
diff --git a/LesionTracker/.meteor/release b/LesionTracker/.meteor/release
new file mode 100644
index 000000000..5684262a8
--- /dev/null
+++ b/LesionTracker/.meteor/release
@@ -0,0 +1 @@
+METEOR@1.2.0.2
diff --git a/LesionTracker/.meteor/versions b/LesionTracker/.meteor/versions
new file mode 100644
index 000000000..ad190b90b
--- /dev/null
+++ b/LesionTracker/.meteor/versions
@@ -0,0 +1,96 @@
+accounts-base@1.2.1
+accounts-password@1.1.3
+anti:i18n@0.4.3
+arsnebula:reactive-promise@0.9.1
+autopublish@1.0.4
+autoupdate@1.2.3
+babel-compiler@5.8.24_1
+babel-runtime@0.1.4
+base64@1.0.4
+binary-heap@1.0.4
+blaze@2.1.3
+blaze-html-templates@1.0.1
+blaze-tools@1.0.4
+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
+cornerstone@0.0.1
+ddp@1.2.2
+ddp-client@1.2.1
+ddp-common@1.2.1
+ddp-rate-limiter@1.0.0
+ddp-server@1.2.1
+deps@1.0.9
+dicomweb@0.0.1
+diff-sequence@1.0.1
+ecmascript@0.1.5
+ecmascript-collections@0.1.6
+ejson@1.0.7
+email@1.0.7
+es5-shim@4.1.13
+fastclick@1.0.7
+fortawesome:fontawesome@4.4.0
+geojson-utils@1.0.4
+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
+id-map@1.0.4
+insecure@1.0.4
+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-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
+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
+rate-limit@1.0.0
+reactive-dict@1.1.2
+reactive-var@1.0.6
+reload@1.1.4
+retry@1.0.4
+routepolicy@1.0.6
+service-configuration@1.0.5
+session@1.1.1
+sha@1.0.4
+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
+templating-tools@1.0.0
+tracker@1.0.9
+twbs:bootstrap@3.3.5
+ui@1.0.8
+underscore@1.0.4
+url@1.0.5
+viewerbase@0.0.1
+webapp@1.2.2
+webapp-hashing@1.0.5
+worklist@0.0.1
diff --git a/LesionTracker/bin/localhostOrthanc.sh b/LesionTracker/bin/localhostOrthanc.sh
new file mode 100755
index 000000000..c7d0ff737
--- /dev/null
+++ b/LesionTracker/bin/localhostOrthanc.sh
@@ -0,0 +1,2 @@
+echo "Starting Meteor server..."
+PACKAGE_DIRS="../Packages" meteor --settings ../config/localhostOrthanc.json
\ No newline at end of file
diff --git a/LesionTracker/bin/medicalConnections.sh b/LesionTracker/bin/medicalConnections.sh
new file mode 100755
index 000000000..1cad10db8
--- /dev/null
+++ b/LesionTracker/bin/medicalConnections.sh
@@ -0,0 +1 @@
+PACKAGE_DIRS="../Packages" meteor --settings ../config/medicalConnections.json
diff --git a/LesionTracker/bin/medkenOrthanc.sh b/LesionTracker/bin/medkenOrthanc.sh
new file mode 100755
index 000000000..4f99ef8f3
--- /dev/null
+++ b/LesionTracker/bin/medkenOrthanc.sh
@@ -0,0 +1 @@
+PACKAGE_DIRS="../Packages" meteor --settings ../config/medkenOrthanc.json
diff --git a/LesionTracker/bin/siimDCM4CHEE.sh b/LesionTracker/bin/siimDCM4CHEE.sh
new file mode 100755
index 000000000..be0397a85
--- /dev/null
+++ b/LesionTracker/bin/siimDCM4CHEE.sh
@@ -0,0 +1 @@
+PACKAGE_DIRS="../Packages" meteor --settings ../config/siimDCM4CHEE.json
diff --git a/LesionTracker/client/compatibility/docking-container.js b/LesionTracker/client/compatibility/docking-container.js
new file mode 100644
index 000000000..d515d6c88
--- /dev/null
+++ b/LesionTracker/client/compatibility/docking-container.js
@@ -0,0 +1,33 @@
+//Collapse the container
+function collapseContainer ( container ) {
+ container.toggleClass("collapseHorizontal");
+}
+
+//function add collapse button
+function addButtonCollapse ( container ) {
+ var btnCollapse = document.createElement("button");
+ btnCollapse.innerHTML = ">";
+ btnCollapse.classList.add('btnCollapse');
+
+ btnCollapse.onclick = function() { // Note this is a function
+ collapseContainer(container);
+ };
+ container.append(btnCollapse);
+}
+
+$.fn.dockingContainer = function(options) {
+ var self = this;
+ var opts = $.extend( {}, $.fn.dockingContainer.defaults, options);
+
+ //Set container class
+ this.addClass("dockingContainer");
+
+ //Add btnCollapse
+ addButtonCollapse(this);
+ return this;
+};
+
+//Defaults
+$.fn.dockingContainer.defaults = {
+ background: "black"
+};
\ No newline at end of file
diff --git a/LesionTracker/client/compatibility/lesionTool.js b/LesionTracker/client/compatibility/lesionTool.js
new file mode 100644
index 000000000..389091db9
--- /dev/null
+++ b/LesionTracker/client/compatibility/lesionTool.js
@@ -0,0 +1,285 @@
+var lineIndex = 0; //This holds drawn line index
+var cornerstoneTools = (function ($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ "use strict";
+
+ if(cornerstoneTools === undefined) {
+ cornerstoneTools = {};
+ }
+
+ var toolType = "lesion";
+
+ ///////// BEGIN ACTIVE TOOL ///////
+ function createNewMeasurement(mouseEventData)
+ {
+ var element = mouseEventData.element;
+ var lesionCounter = "";
+
+ //setting name of lesion
+ $(element).on("LesionNameSet", function(e,counter){
+ lesionCounter = counter;
+ });
+
+ //Listens LesionToolModified event and calls measurementModified function when lesion measurement is changed or updated.
+ $(element).on("LesionToolModified", measurementModified);
+ $(element).trigger("LesionMeasurementCreated");
+
+ // 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: 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,
+ isDeleted: false,
+ lineNumber:"" //Indicates line number on image
+ };
+ lineIndex++;
+ 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);
+ 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);
+
+ }
+
+
+ ///////// BEGIN IMAGE RENDERING ///////
+ function onImageRendered(e, eventData) {
+
+ // 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;
+ }
+
+ // 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();
+ }
+
+ }
+ ///////// 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) {
+ 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);
+ }
+ } else {
+ 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.active = true;
+ } else{
+ data.active = false;
+ }
+ }
+ }
+
+
+ enabledElement.image.render(enabledElement, true);
+
+ 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,
+ lineIndex : lineIndex,
+ type : type //Holds image will be deleted or active
+ };
+
+ enabledElement.invalid = false;
+
+ onImageRendered(e, eventData);
+ }
+
+ // module exports
+ cornerstoneTools.lesion = cornerstoneTools.mouseButtonTool({
+ createNewMeasurement : createNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool : pointNearTool,
+ pointNearToolForText: pointNearToolForText,
+ toolType : toolType
+ });
+ cornerstoneTools.lesionTouch = cornerstoneTools.touchTool({
+ createNewMeasurement: createNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ pointNearToolForText: pointNearToolForText,
+ toolType: toolType
+ });
+ return cornerstoneTools;
+}($, cornerstone, cornerstoneMath, cornerstoneTools));
\ No newline at end of file
diff --git a/LesionTracker/client/compatibility/tabHelper.js b/LesionTracker/client/compatibility/tabHelper.js
new file mode 100644
index 000000000..ca9ea3bab
--- /dev/null
+++ b/LesionTracker/client/compatibility/tabHelper.js
@@ -0,0 +1,15 @@
+//Holds functions to help managing tabs
+
+//Returns activeTabId
+function activeTabId() {
+ var activeTab = Session.get("activeTabId");
+ return activeTab;
+}
+
+//Returns activeTabContentId
+function activeTabContentId () {
+ var activeTabId = activeTabId();
+ var splitTabId = activeTabId.split("tab");
+ var contentId = "content"+splitTabId[1];
+ return contentId;
+}
\ No newline at end of file
diff --git a/LesionTracker/client/components/hidingPanel/hidingPanel.html b/LesionTracker/client/components/hidingPanel/hidingPanel.html
new file mode 100644
index 000000000..30dc3777a
--- /dev/null
+++ b/LesionTracker/client/components/hidingPanel/hidingPanel.html
@@ -0,0 +1,5 @@
+
+
+ {{> studyBrowser}}
+
+
\ No newline at end of file
diff --git a/LesionTracker/client/components/hidingPanel/hidingPanel.styl b/LesionTracker/client/components/hidingPanel/hidingPanel.styl
new file mode 100644
index 000000000..bff3dc975
--- /dev/null
+++ b/LesionTracker/client/components/hidingPanel/hidingPanel.styl
@@ -0,0 +1,9 @@
+.hidingPanel
+ width: 12%
+ height: 100%
+ margin-top: 35px
+ position: relative
+ float: left
+ overflow: auto
+ background-color: blue
+ color: black
\ No newline at end of file
diff --git a/LesionTracker/client/components/toolbar/toolbar.css b/LesionTracker/client/components/toolbar/toolbar.css
new file mode 100644
index 000000000..21cbff5c8
--- /dev/null
+++ b/LesionTracker/client/components/toolbar/toolbar.css
@@ -0,0 +1,48 @@
+.toolbar {
+ width: 100%;
+ height: 35px;
+ text-align: center;
+}
+
+#toolbar-group button {
+ text-align: center;
+ margin-top: 2px;
+ margin-right: 3px;
+ margin-left: 3px;
+ color: #ffffff;
+ background-color: black;
+ width: 30px;
+ height: 30px;
+ border: 1px solid white;
+}
+
+#toolbar-group button > span {
+ width: 12px;
+ height: 12px;
+ position: absolute;
+ top: 9px;
+ left: 9px;
+ right: 9px;
+ bottom: 9px;
+}
+
+
+#toolbar-group .btn:hover {
+ border-width: 3px;
+ background-color: transparent;
+ color: white;
+}
+#toolbar-group .btn:focus, .btn:active {
+ outline: 0 !important;
+ border-width: 5px;
+ background-color: transparent;
+ color:white;
+ text-align: center;
+}
+
+#toolbar-group .btn:focus > span{
+ top: 6px;
+ left: 6px;
+ right: 6px;
+ bottom: 6px;
+}
\ No newline at end of file
diff --git a/LesionTracker/client/components/toolbar/toolbar.html b/LesionTracker/client/components/toolbar/toolbar.html
new file mode 100644
index 000000000..e40c066f7
--- /dev/null
+++ b/LesionTracker/client/components/toolbar/toolbar.html
@@ -0,0 +1,36 @@
+
+
+
\ No newline at end of file
diff --git a/LesionTracker/client/components/toolbar/toolbar.js b/LesionTracker/client/components/toolbar/toolbar.js
new file mode 100644
index 000000000..2aeb7a4cb
--- /dev/null
+++ b/LesionTracker/client/components/toolbar/toolbar.js
@@ -0,0 +1,34 @@
+Template.toolbar.events({
+ 'click .imageViewerTool': function(e) {
+ var tool = e.currentTarget.id;
+ console.log('Setting active tool to: ' + tool);
+ toolManager.setActiveTool(tool);
+ },
+ 'click .imageViewerCommand': function(e) {
+ var command = e.currentTarget.id;
+ if (!OHIF.viewer.functionList.hasOwnProperty(command)) {
+ return;
+ }
+
+ // TODO = Add support for active viewports
+ //var element = getActiveViewportElement();
+ $('.imageViewer').each(function() {
+ var element = this;
+ OHIF.viewer.functionList[command](element);
+ });
+ }
+});
+
+Template.toolbar.onRendered(function() {
+ var tooltipButtons = $('[data-toggle="tooltip"]');
+ tooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
+
+ // Enable tooltips for the layout button
+ var extraTooltipButtons = $('[rel="tooltip"]');
+ extraTooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
+
+ toolManager.addTool('lesion', {
+ mouse: cornerstoneTools.lesion,
+ touch: cornerstoneTools.lesionTouch
+ });
+});
\ No newline at end of file
diff --git a/LesionTracker/client/components/viewer.css b/LesionTracker/client/components/viewer.css
new file mode 100644
index 000000000..3dd2e3f77
--- /dev/null
+++ b/LesionTracker/client/components/viewer.css
@@ -0,0 +1,7 @@
+.viewer{
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ background-color: black;
+}
diff --git a/LesionTracker/client/components/viewer.html b/LesionTracker/client/components/viewer.html
new file mode 100644
index 000000000..7f6bd47ad
--- /dev/null
+++ b/LesionTracker/client/components/viewer.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+ {{ >hidingPanel }}
+ {{ >viewerMain }}
+
+
\ No newline at end of file
diff --git a/LesionTracker/client/components/viewer.js b/LesionTracker/client/components/viewer.js
new file mode 100644
index 000000000..d7b3e0584
--- /dev/null
+++ b/LesionTracker/client/components/viewer.js
@@ -0,0 +1,89 @@
+OHIF = {
+ viewer: {}
+};
+
+OHIF.viewer.imageViewerLoadedSeriesDictionary = {};
+OHIF.viewer.imageViewerCurrentImageIdIndexDictionary = {};
+OHIF.viewer.loadIndicatorDelay = 3000;
+OHIF.viewer.defaultTool = 'wwwc';
+OHIF.viewer.refLinesEnabled = true;
+OHIF.viewer.isPlaying = {};
+
+Meteor.startup(function() {
+ OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer("CornerstoneNewImage", cornerstoneTools.updateImageSynchronizer);
+ OHIF.viewer.functionList = {
+ invert: function(element) {
+ var viewport = cornerstone.getViewport(element);
+ viewport.invert = !viewport.invert;
+ cornerstone.setViewport(element, viewport);
+ },
+ playClip: function(element) {
+ var viewportIndex = $('.imageViewerViewport').index(element);
+ var isPlaying = OHIF.viewer.isPlaying[viewportIndex] || false;
+ if (isPlaying === true) {
+ cornerstoneTools.stopClip(element);
+ } else {
+ cornerstoneTools.playClip(element);
+ }
+ OHIF.viewer.isPlaying[viewportIndex] = !OHIF.viewer.isPlaying[viewportIndex];
+ Session.set('UpdateCINE', Random.id());
+ }
+ };
+
+
+ if (isTouchDevice()) {
+ OHIF.viewer.tooltipConfig = {
+ trigger: 'manual'
+ };
+ } else {
+ OHIF.viewer.tooltipConfig = {
+ trigger: 'hover'
+ };
+ }
+});
+
+function 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
+ viewportResizeTimer = setTimeout(function() {
+ var elements = $('.imageViewerViewport');
+ elements.each(function(index) {
+ var element = this;
+ if (!element) {
+ return;
+ }
+ cornerstone.resize(element, true);
+ });
+ }, 1);
+}
+
+Session.setDefault('ActiveViewport', 0);
+Session.setDefault('viewportRows', 1);
+Session.setDefault('viewportColumns', 1);
+
+// Avoid doing DOM manipulation during the resize handler
+// because it is fired very often.
+// Resizing is therefore performed 100 ms after the resize event stops.
+var resizeTimer;
+$(window).on('resize', function() {
+ clearTimeout(resizeTimer);
+ resizeTimer = setTimeout(function() {
+ resizeViewports();
+ }, 100);
+});
+
+Template.viewer.onRendered(function() {
+ var imageViewer = $("#viewer");
+ if (imageViewer) {
+ $('.navbar-default').css({
+ 'background-color': '#000000',
+ 'border-color': '#101010'
+ });
+ document.body.style.overflow = "hidden";
+ document.body.style.height = '100%';
+ document.body.style.width = '100%';
+ document.body.style.minWidth = 0;
+ document.body.style.position = 'fixed'; // Prevent overscroll on mobile devices
+ }
+});
\ No newline at end of file
diff --git a/LesionTracker/client/components/viewerMain/viewerMain.css b/LesionTracker/client/components/viewerMain/viewerMain.css
new file mode 100644
index 000000000..fe6a741cb
--- /dev/null
+++ b/LesionTracker/client/components/viewerMain/viewerMain.css
@@ -0,0 +1,5 @@
+.viewerMain {
+ width: 88%;
+ height: 100%;
+ margin-left: 12%;
+}
\ No newline at end of file
diff --git a/LesionTracker/client/components/viewerMain/viewerMain.html b/LesionTracker/client/components/viewerMain/viewerMain.html
new file mode 100644
index 000000000..3113df387
--- /dev/null
+++ b/LesionTracker/client/components/viewerMain/viewerMain.html
@@ -0,0 +1,7 @@
+
+
+ {{> toolbar}}
+ {{> imageViewerViewports}}
+
+
+
\ No newline at end of file
diff --git a/LesionTracker/client/head.html b/LesionTracker/client/head.html
new file mode 100644
index 000000000..ed329b9b0
--- /dev/null
+++ b/LesionTracker/client/head.html
@@ -0,0 +1,12 @@
+
+
+ Lesion Tracker
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LesionTracker/client/images/logo.png b/LesionTracker/client/images/logo.png
new file mode 100644
index 000000000..bcb42b3d7
Binary files /dev/null and b/LesionTracker/client/images/logo.png differ
diff --git a/LesionTracker/client/route.js b/LesionTracker/client/route.js
new file mode 100644
index 000000000..eca66daf3
--- /dev/null
+++ b/LesionTracker/client/route.js
@@ -0,0 +1,34 @@
+Router.configure({
+ layoutTemplate: 'layout',
+ //loadingTemplate: '',
+ notFoundTemplate: 'notFound'
+});
+
+
+
+Router.route('/', function () {
+ this.render('worklist', {
+ });
+});
+
+ViewerStudies = new Mongo.Collection(null);
+
+Router.route('/viewer/:_id', {
+ layoutTemplate: 'layout',
+ name: 'viewer',
+ onBeforeAction: function() {
+ var self = this;
+ //Session.set('openNewTabEvent', self);
+
+ Meteor.call('GetStudyMetadata', this.params._id, function(error, study) {
+ //console.log(study);
+ sortStudy(study);
+
+ var studies = [study];
+ Session.set('studies', studies);
+ //Session.set(activeTabId, studies);
+ //Session.set('showContentInTab', true);
+ self.render('viewer');
+ });
+ }
+});
\ No newline at end of file
diff --git a/LesionTracker/client/routeHooks.js b/LesionTracker/client/routeHooks.js
new file mode 100644
index 000000000..085979ea0
--- /dev/null
+++ b/LesionTracker/client/routeHooks.js
@@ -0,0 +1,5 @@
+Router.onBeforeAction(function () {
+
+ // User is logged in, go ahead and route them
+ this.next();
+});
diff --git a/LesionTracker/client/stylesheets/docking-container.css b/LesionTracker/client/stylesheets/docking-container.css
new file mode 100644
index 000000000..265bd5f69
--- /dev/null
+++ b/LesionTracker/client/stylesheets/docking-container.css
@@ -0,0 +1,52 @@
+/*****************************/
+/* Toggle */
+/*****************************/
+* {
+ box-sizing: border-box;
+}
+.dockingContainer, .collapseVertical, .collapseHorizontal {
+ -moz-transition: all 0.3s ease-out;
+ -webkit-transition: all 0.3s ease-out;
+ transition: all 0.3s ease-out;
+}
+
+.dockingContainer {
+ -moz-transition: all 0.3s ease-out;
+ -webkit-transition: all 0.3s ease-out;
+ transition: all 0.3s ease-out;
+}
+
+.collapseVertical {
+ opacity: 0;
+ height: 0;
+ border: none;
+}
+
+.collapseHorizontal {
+ width: 30px;
+}
+
+.btnCollapse{
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: block;
+ width: 25px;
+ height: 25px;
+ line-height: 25px;
+ padding: 0;
+ border: 0;
+ border-top: 1px solid #e6e6e6;
+ background: #f6f6f6;
+ color: #666;
+ font-size: 0.875rem;
+ text-align: center;
+ cursor: pointer;
+ z-index: 100;
+}
+
+.btnCollapseIcon-collapse{
+ -moz-transform: rotate(180deg);
+ -webkit-transform: rotate(180deg);
+ transform: rotate(180deg);
+}
diff --git a/OHIFViewer/.gitignore b/OHIFViewer/.gitignore
new file mode 100644
index 000000000..b0746064f
--- /dev/null
+++ b/OHIFViewer/.gitignore
@@ -0,0 +1,3 @@
+.meteor/local
+.meteor/meteorite
+node_modules
\ No newline at end of file
diff --git a/OHIFViewer/.meteor/.finished-upgraders b/OHIFViewer/.meteor/.finished-upgraders
new file mode 100644
index 000000000..61ee31323
--- /dev/null
+++ b/OHIFViewer/.meteor/.finished-upgraders
@@ -0,0 +1,12 @@
+# This file contains information which helps Meteor properly upgrade your
+# app when you run 'meteor update'. You should check it into version control
+# with your project.
+
+notices-for-0.9.0
+notices-for-0.9.1
+0.9.4-platform-file
+notices-for-facebook-graph-api-2
+1.2.0-standard-minifiers-package
+1.2.0-meteor-platform-split
+1.2.0-cordova-changes
+1.2.0-breaking-changes
diff --git a/OHIFViewer/.meteor/.gitignore b/OHIFViewer/.meteor/.gitignore
new file mode 100644
index 000000000..408303742
--- /dev/null
+++ b/OHIFViewer/.meteor/.gitignore
@@ -0,0 +1 @@
+local
diff --git a/OHIFViewer/.meteor/.id b/OHIFViewer/.meteor/.id
new file mode 100644
index 000000000..c1706e8d0
--- /dev/null
+++ b/OHIFViewer/.meteor/.id
@@ -0,0 +1,7 @@
+# This file contains a token that is unique to your project.
+# Check it into your repository along with the rest of this directory.
+# It can be used for purposes such as:
+# - ensuring you don't accidentally deploy one app on top of another
+# - providing package authors with aggregated statistics
+
+1jfkhvr1ljfqodmpn32m
diff --git a/OHIFViewer/.meteor/packages b/OHIFViewer/.meteor/packages
new file mode 100644
index 000000000..ac5deffb8
--- /dev/null
+++ b/OHIFViewer/.meteor/packages
@@ -0,0 +1,34 @@
+# Meteor packages used by this project, one per line.
+# Check this file (and the other files in this directory) into your repository.
+#
+# 'meteor add' and 'meteor remove' will edit this file for you,
+# but you can also edit it by hand.
+
+accounts-password
+ian:accounts-ui-bootstrap-3
+iron:router
+twbs:bootstrap
+http
+arsnebula:reactive-promise
+promise
+stylus
+fortawesome:fontawesome
+mrt:moment
+standard-minifiers
+meteor-base
+mobile-experience
+mongo
+blaze-html-templates
+session
+jquery
+tracker
+logging
+reload
+random
+ejson
+spacebars
+check
+cornerstone
+dicomweb
+viewerbase
+worklist
diff --git a/OHIFViewer/.meteor/platforms b/OHIFViewer/.meteor/platforms
new file mode 100644
index 000000000..efeba1b50
--- /dev/null
+++ b/OHIFViewer/.meteor/platforms
@@ -0,0 +1,2 @@
+server
+browser
diff --git a/OHIFViewer/.meteor/release b/OHIFViewer/.meteor/release
new file mode 100644
index 000000000..5684262a8
--- /dev/null
+++ b/OHIFViewer/.meteor/release
@@ -0,0 +1 @@
+METEOR@1.2.0.2
diff --git a/OHIFViewer/.meteor/versions b/OHIFViewer/.meteor/versions
new file mode 100644
index 000000000..1c2361305
--- /dev/null
+++ b/OHIFViewer/.meteor/versions
@@ -0,0 +1,94 @@
+accounts-base@1.2.1
+accounts-password@1.1.3
+anti:i18n@0.4.3
+arsnebula:reactive-promise@0.9.1
+autoupdate@1.2.3
+babel-compiler@5.8.24_1
+babel-runtime@0.1.4
+base64@1.0.4
+binary-heap@1.0.4
+blaze@2.1.3
+blaze-html-templates@1.0.1
+blaze-tools@1.0.4
+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
+cornerstone@0.0.1
+ddp@1.2.2
+ddp-client@1.2.1
+ddp-common@1.2.1
+ddp-rate-limiter@1.0.0
+ddp-server@1.2.1
+deps@1.0.9
+dicomweb@0.0.1
+diff-sequence@1.0.1
+ecmascript@0.1.5
+ecmascript-collections@0.1.6
+ejson@1.0.7
+email@1.0.7
+fastclick@1.0.7
+fortawesome:fontawesome@4.4.0
+geojson-utils@1.0.4
+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
+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
+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-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
+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
+rate-limit@1.0.0
+reactive-dict@1.1.2
+reactive-var@1.0.6
+reload@1.1.4
+retry@1.0.4
+routepolicy@1.0.6
+service-configuration@1.0.5
+session@1.1.1
+sha@1.0.4
+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
+templating-tools@1.0.0
+tracker@1.0.9
+twbs:bootstrap@3.3.5
+ui@1.0.8
+underscore@1.0.4
+url@1.0.5
+viewerbase@0.0.1
+webapp@1.2.2
+webapp-hashing@1.0.5
+worklist@0.0.1
diff --git a/OHIFViewer/bin/localhostOrthanc.sh b/OHIFViewer/bin/localhostOrthanc.sh
new file mode 100755
index 000000000..c7d0ff737
--- /dev/null
+++ b/OHIFViewer/bin/localhostOrthanc.sh
@@ -0,0 +1,2 @@
+echo "Starting Meteor server..."
+PACKAGE_DIRS="../Packages" meteor --settings ../config/localhostOrthanc.json
\ No newline at end of file
diff --git a/OHIFViewer/bin/medicalConnections.sh b/OHIFViewer/bin/medicalConnections.sh
new file mode 100755
index 000000000..1cad10db8
--- /dev/null
+++ b/OHIFViewer/bin/medicalConnections.sh
@@ -0,0 +1 @@
+PACKAGE_DIRS="../Packages" meteor --settings ../config/medicalConnections.json
diff --git a/OHIFViewer/bin/medkenOrthanc.sh b/OHIFViewer/bin/medkenOrthanc.sh
new file mode 100755
index 000000000..4f99ef8f3
--- /dev/null
+++ b/OHIFViewer/bin/medkenOrthanc.sh
@@ -0,0 +1 @@
+PACKAGE_DIRS="../Packages" meteor --settings ../config/medkenOrthanc.json
diff --git a/OHIFViewer/bin/siimDCM4CHEE.sh b/OHIFViewer/bin/siimDCM4CHEE.sh
new file mode 100755
index 000000000..be0397a85
--- /dev/null
+++ b/OHIFViewer/bin/siimDCM4CHEE.sh
@@ -0,0 +1 @@
+PACKAGE_DIRS="../Packages" meteor --settings ../config/siimDCM4CHEE.json
diff --git a/OHIFViewer/client/components/viewer/toolRow/toolbar.html b/OHIFViewer/client/components/viewer/toolRow/toolbar.html
new file mode 100644
index 000000000..822124459
--- /dev/null
+++ b/OHIFViewer/client/components/viewer/toolRow/toolbar.html
@@ -0,0 +1,36 @@
+
+
+
\ No newline at end of file
diff --git a/OHIFViewer/client/components/viewer/toolRow/toolbar.js b/OHIFViewer/client/components/viewer/toolRow/toolbar.js
new file mode 100644
index 000000000..3e6dd6ab7
--- /dev/null
+++ b/OHIFViewer/client/components/viewer/toolRow/toolbar.js
@@ -0,0 +1,40 @@
+Template.toolbar.helpers({
+ studies : function() {
+ var studies = Session.get('studies');
+ return studies;
+ }
+});
+
+Template.toolbar.events({
+ 'click .imageViewerTool': function(e) {
+ var tool = e.currentTarget.id;
+ console.log('Setting active tool to: ' + tool);
+ toolManager.setActiveTool(tool);
+ },
+ 'click .imageViewerCommand': function(e) {
+ var command = e.currentTarget.id;
+ if (!OHIF.viewer.functionList.hasOwnProperty(command)) {
+ return;
+ }
+ var viewportIndex = Session.get('ActiveViewport');
+ var element = $('.imageViewerViewport').get(viewportIndex);
+ OHIF.viewer.functionList[command](element);
+ }
+});
+
+Template.toolbar.onRendered(function() {
+ var tooltipButtons = $('[data-toggle="tooltip"]');
+ tooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
+
+ // Enable tooltips for the layout button
+ var extraTooltipButtons = $('[rel="tooltip"]');
+ extraTooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
+});
+
+Template.toolbar.helpers({
+ 'isPlaying': function() {
+ Session.get('UpdateCINE');
+ var viewportIndex = Session.get('ActiveViewport');
+ return !!OHIF.viewer.isPlaying[viewportIndex];
+ }
+});
\ No newline at end of file
diff --git a/OHIFViewer/client/components/viewer/toolRow/toolbar.styl b/OHIFViewer/client/components/viewer/toolRow/toolbar.styl
new file mode 100644
index 000000000..774a71141
--- /dev/null
+++ b/OHIFViewer/client/components/viewer/toolRow/toolbar.styl
@@ -0,0 +1,15 @@
+#toolbar
+ background-color: black
+ text-align: center
+
+ .btn-group
+ display: inline-block
+
+ button
+ text-align: center
+ color: #ffffff
+ background-color: #424242
+ border-color: #424242
+
+ &.active, &:active
+ background-color: #3B678E
\ No newline at end of file
diff --git a/OHIFViewer/client/components/viewer/viewer.html b/OHIFViewer/client/components/viewer/viewer.html
new file mode 100644
index 000000000..d56a5d23d
--- /dev/null
+++ b/OHIFViewer/client/components/viewer/viewer.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+ {{> studyBrowser}}
+ {{> viewerMain}}
+
+
\ No newline at end of file
diff --git a/OHIFViewer/client/components/viewer/viewer.js b/OHIFViewer/client/components/viewer/viewer.js
new file mode 100644
index 000000000..d7b3e0584
--- /dev/null
+++ b/OHIFViewer/client/components/viewer/viewer.js
@@ -0,0 +1,89 @@
+OHIF = {
+ viewer: {}
+};
+
+OHIF.viewer.imageViewerLoadedSeriesDictionary = {};
+OHIF.viewer.imageViewerCurrentImageIdIndexDictionary = {};
+OHIF.viewer.loadIndicatorDelay = 3000;
+OHIF.viewer.defaultTool = 'wwwc';
+OHIF.viewer.refLinesEnabled = true;
+OHIF.viewer.isPlaying = {};
+
+Meteor.startup(function() {
+ OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer("CornerstoneNewImage", cornerstoneTools.updateImageSynchronizer);
+ OHIF.viewer.functionList = {
+ invert: function(element) {
+ var viewport = cornerstone.getViewport(element);
+ viewport.invert = !viewport.invert;
+ cornerstone.setViewport(element, viewport);
+ },
+ playClip: function(element) {
+ var viewportIndex = $('.imageViewerViewport').index(element);
+ var isPlaying = OHIF.viewer.isPlaying[viewportIndex] || false;
+ if (isPlaying === true) {
+ cornerstoneTools.stopClip(element);
+ } else {
+ cornerstoneTools.playClip(element);
+ }
+ OHIF.viewer.isPlaying[viewportIndex] = !OHIF.viewer.isPlaying[viewportIndex];
+ Session.set('UpdateCINE', Random.id());
+ }
+ };
+
+
+ if (isTouchDevice()) {
+ OHIF.viewer.tooltipConfig = {
+ trigger: 'manual'
+ };
+ } else {
+ OHIF.viewer.tooltipConfig = {
+ trigger: 'hover'
+ };
+ }
+});
+
+function 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
+ viewportResizeTimer = setTimeout(function() {
+ var elements = $('.imageViewerViewport');
+ elements.each(function(index) {
+ var element = this;
+ if (!element) {
+ return;
+ }
+ cornerstone.resize(element, true);
+ });
+ }, 1);
+}
+
+Session.setDefault('ActiveViewport', 0);
+Session.setDefault('viewportRows', 1);
+Session.setDefault('viewportColumns', 1);
+
+// Avoid doing DOM manipulation during the resize handler
+// because it is fired very often.
+// Resizing is therefore performed 100 ms after the resize event stops.
+var resizeTimer;
+$(window).on('resize', function() {
+ clearTimeout(resizeTimer);
+ resizeTimer = setTimeout(function() {
+ resizeViewports();
+ }, 100);
+});
+
+Template.viewer.onRendered(function() {
+ var imageViewer = $("#viewer");
+ if (imageViewer) {
+ $('.navbar-default').css({
+ 'background-color': '#000000',
+ 'border-color': '#101010'
+ });
+ document.body.style.overflow = "hidden";
+ document.body.style.height = '100%';
+ document.body.style.width = '100%';
+ document.body.style.minWidth = 0;
+ document.body.style.position = 'fixed'; // Prevent overscroll on mobile devices
+ }
+});
\ No newline at end of file
diff --git a/OHIFViewer/client/components/viewer/viewer.styl b/OHIFViewer/client/components/viewer/viewer.styl
new file mode 100644
index 000000000..c72e28c25
--- /dev/null
+++ b/OHIFViewer/client/components/viewer/viewer.styl
@@ -0,0 +1,12 @@
+#viewer
+ width: 100%
+ height: calc(100% - 53px)
+ border-top: 1px solid #525252
+
+.noselect
+ -webkit-touch-callout: none
+ -webkit-user-select: none
+ -khtml-user-select: none
+ -moz-user-select: none
+ -ms-user-select: none
+ user-select: none
\ No newline at end of file
diff --git a/OHIFViewer/client/components/viewer/viewerMain/viewerMain.html b/OHIFViewer/client/components/viewer/viewerMain/viewerMain.html
new file mode 100644
index 000000000..58184fa05
--- /dev/null
+++ b/OHIFViewer/client/components/viewer/viewerMain/viewerMain.html
@@ -0,0 +1,6 @@
+
+
+ {{ > toolbar }}
+ {{ > imageViewerViewports }}
+
+
\ No newline at end of file
diff --git a/OHIFViewer/client/components/viewer/viewerMain/viewerMain.js b/OHIFViewer/client/components/viewer/viewerMain/viewerMain.js
new file mode 100644
index 000000000..018768993
--- /dev/null
+++ b/OHIFViewer/client/components/viewer/viewerMain/viewerMain.js
@@ -0,0 +1,6 @@
+Template.viewerMain.helpers({
+ studies : function() {
+ var studies = Session.get('studies');
+ return 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
new file mode 100644
index 000000000..7b269b4cd
--- /dev/null
+++ b/OHIFViewer/client/components/viewer/viewerMain/viewerMain.styl
@@ -0,0 +1,5 @@
+.viewerMain
+ float: left
+ background-color: gray
+ height: 100%
+ width: calc(100% - 120px)
\ No newline at end of file
diff --git a/OHIFViewer/client/routeHooks.js b/OHIFViewer/client/routeHooks.js
new file mode 100644
index 000000000..085979ea0
--- /dev/null
+++ b/OHIFViewer/client/routeHooks.js
@@ -0,0 +1,5 @@
+Router.onBeforeAction(function () {
+
+ // User is logged in, go ahead and route them
+ this.next();
+});
diff --git a/OHIFViewer/client/routes.js b/OHIFViewer/client/routes.js
new file mode 100644
index 000000000..df1d39187
--- /dev/null
+++ b/OHIFViewer/client/routes.js
@@ -0,0 +1,33 @@
+Router.configure({
+ layoutTemplate: 'layout',
+ //loadingTemplate: '',
+ notFoundTemplate: 'notFound'
+});
+
+
+
+Router.route('/', function () {
+ this.render('worklist', {
+ });
+});
+
+ViewerStudies = new Mongo.Collection(null);
+
+Router.route('/viewer/:_id', {
+ layoutTemplate: 'layout',
+ name: 'viewer',
+ onBeforeAction: function() {
+ var self = this;
+ Meteor.call('GetStudyMetadata', this.params._id, function(error, study) {
+ //console.log(study);
+ sortStudy(study);
+
+ var studies = [study];
+ Session.set('studies', studies);
+
+ self.render('viewer');
+ });
+ }
+});
+
+
diff --git a/Packages/cornerstone/client/cornerstone.js b/Packages/cornerstone/client/cornerstone.js
new file mode 100644
index 000000000..b68821d71
--- /dev/null
+++ b/Packages/cornerstone/client/cornerstone.js
@@ -0,0 +1,3045 @@
+/*! cornerstone - v0.8.4 - 2015-10-09 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstone */
+if(typeof cornerstone === 'undefined'){
+ cornerstone = {
+ internal : {},
+ rendering: {}
+ };
+}
+
+(function (cornerstone) {
+
+ "use strict";
+
+ function disable(element) {
+ if(element === undefined) {
+ throw "disable: element element must not be undefined";
+ }
+
+ // Search for this element in this list of enabled elements
+ var enabledElements = cornerstone.getEnabledElements();
+ for(var i=0; i < enabledElements.length; i++) {
+ if(enabledElements[i].element === element) {
+ // We found it!
+
+ // Fire an event so dependencies can cleanup
+ var eventData = {
+ element : element
+ };
+ $(element).trigger("CornerstoneElementDisabled", eventData);
+
+ // remove the child dom elements that we created (e.g.canvas)
+ enabledElements[i].element.removeChild(enabledElements[i].canvas);
+
+ // remove this element from the list of enabled elements
+ enabledElements.splice(i, 1);
+ return;
+ }
+ }
+ }
+
+ // module/private exports
+ cornerstone.disable = disable;
+
+}(cornerstone));
+/**
+ * This module is responsible for enabling an element to display images with cornerstone
+ */
+(function ($, cornerstone) {
+
+ "use strict";
+
+ /**
+ * sets a new image object for a given element
+ * @param element
+ * @param image
+ */
+ function displayImage(element, image, viewport) {
+ if(element === undefined) {
+ throw "displayImage: parameter element cannot be undefined";
+ }
+ if(image === undefined) {
+ throw "displayImage: parameter image cannot be undefined";
+ }
+
+ var enabledElement = cornerstone.getEnabledElement(element);
+
+ enabledElement.image = image;
+
+ if(enabledElement.viewport === undefined) {
+ enabledElement.viewport = cornerstone.internal.getDefaultViewport(enabledElement.canvas, image);
+ }
+
+ // merge viewport
+ if(viewport) {
+ for(var attrname in viewport)
+ {
+ if(viewport[attrname] !== null) {
+ enabledElement.viewport[attrname] = viewport[attrname];
+ }
+ }
+ }
+
+ var now = new Date();
+ var frameRate;
+ if(enabledElement.lastImageTimeStamp !== undefined) {
+ var timeSinceLastImage = now.getTime() - enabledElement.lastImageTimeStamp;
+ frameRate = (1000 / timeSinceLastImage).toFixed();
+ } else {
+ }
+ enabledElement.lastImageTimeStamp = now.getTime();
+
+ var newImageEventData = {
+ viewport : enabledElement.viewport,
+ element : enabledElement.element,
+ image : enabledElement.image,
+ enabledElement : enabledElement,
+ frameRate : frameRate
+ };
+
+ $(enabledElement.element).trigger("CornerstoneNewImage", newImageEventData);
+
+ cornerstone.updateImage(element);
+ }
+
+ // module/private exports
+ cornerstone.displayImage = displayImage;
+}($, cornerstone));
+/**
+ * This module is responsible for immediately drawing an enabled element
+ */
+
+(function ($, cornerstone) {
+
+ "use strict";
+
+ /**
+ * Immediately draws the enabled element
+ *
+ * @param element
+ */
+ function draw(element) {
+ var enabledElement = cornerstone.getEnabledElement(element);
+
+ if(enabledElement.image === undefined) {
+ throw "draw: image has not been loaded yet";
+ }
+
+ cornerstone.drawImage(enabledElement);
+ }
+
+ // Module exports
+ cornerstone.draw = draw;
+
+}($, cornerstone));
+/**
+ * This module is responsible for drawing invalidated enabled elements
+ */
+
+(function ($, cornerstone) {
+
+ "use strict";
+
+ /**
+ * Draws all invalidated enabled elements and clears the invalid flag after drawing it
+ */
+ function drawInvalidated()
+ {
+ var enabledElements = cornerstone.getEnabledElements();
+ for(var i=0;i < enabledElements.length; i++) {
+ var ee = enabledElements[i];
+ if(ee.invalid === true) {
+ cornerstone.drawImage(ee);
+ }
+ }
+ }
+
+ // Module exports
+ cornerstone.drawInvalidated = drawInvalidated;
+}($, cornerstone));
+/**
+ * This module is responsible for enabling an element to display images with cornerstone
+ */
+(function (cornerstone) {
+
+ "use strict";
+
+ function enable(element, renderer) {
+ if(element === undefined) {
+ throw "enable: parameter element cannot be undefined";
+ }
+
+ var canvas = document.createElement('canvas');
+ element.appendChild(canvas);
+
+ if (typeof renderer === 'string' && renderer.toLowerCase() === 'webgl') {
+ renderer = cornerstone.webGL.renderer.render;
+ }
+
+ if (renderer === cornerstone.webGL.renderer.render) {
+ if (!cornerstone.webGL.renderer.isWebGLAvailable()) {
+ console.error('WebGL not available, falling back to Canvas renderer');
+ renderer = undefined;
+ } else {
+ cornerstone.webGL.renderer.initRenderer();
+ }
+ }
+
+ var el = {
+ element: element,
+ canvas: canvas,
+ image : undefined, // will be set once image is loaded
+ invalid: false, // true if image needs to be drawn, false if not
+ render: renderer,
+ data : {}
+ };
+ cornerstone.addEnabledElement(el);
+
+ cornerstone.resize(element, true);
+
+ return element;
+ }
+
+ // module/private exports
+ cornerstone.enable = enable;
+}(cornerstone));
+(function (cornerstone) {
+
+ "use strict";
+
+ function getElementData(el, dataType) {
+ var ee = cornerstone.getEnabledElement(el);
+ if(ee.data.hasOwnProperty(dataType) === false)
+ {
+ ee.data[dataType] = {};
+ }
+ return ee.data[dataType];
+ }
+
+ function removeElementData(el, dataType) {
+ var ee = cornerstone.getEnabledElement(el);
+ delete ee.data[dataType];
+ }
+
+ // module/private exports
+ cornerstone.getElementData = getElementData;
+ cornerstone.removeElementData = removeElementData;
+
+}(cornerstone));
+(function (cornerstone) {
+
+ "use strict";
+
+ var enabledElements = [];
+
+ function getEnabledElement(element) {
+ if(element === undefined) {
+ throw "getEnabledElement: parameter element must not be undefined";
+ }
+ for(var i=0; i < enabledElements.length; i++) {
+ if(enabledElements[i].element == element) {
+ return enabledElements[i];
+ }
+ }
+
+ throw "element not enabled";
+ }
+
+ function addEnabledElement(enabledElement) {
+ if(enabledElement === undefined) {
+ throw "getEnabledElement: enabledElement element must not be undefined";
+ }
+
+ enabledElements.push(enabledElement);
+ }
+
+ function getEnabledElementsByImageId(imageId) {
+ var ees = [];
+ enabledElements.forEach(function(enabledElement) {
+ if(enabledElement.image && enabledElement.image.imageId === imageId) {
+ ees.push(enabledElement);
+ }
+ });
+ return ees;
+ }
+
+ function getEnabledElements() {
+ return enabledElements;
+ }
+
+ // module/private exports
+ cornerstone.getEnabledElement = getEnabledElement;
+ cornerstone.addEnabledElement = addEnabledElement;
+ cornerstone.getEnabledElementsByImageId = getEnabledElementsByImageId;
+ cornerstone.getEnabledElements = getEnabledElements;
+}(cornerstone));
+/**
+ * This module will fit an image to fit inside the canvas displaying it such that all pixels
+ * in the image are viewable
+ */
+(function (cornerstone) {
+
+ "use strict";
+
+ function getImageSize(enabledElement) {
+ if(enabledElement.viewport.rotation === 0 ||enabledElement.viewport.rotation === 180) {
+ return {
+ width: enabledElement.image.width,
+ height: enabledElement.image.height
+ };
+ } else {
+ return {
+ width: enabledElement.image.height,
+ height: enabledElement.image.width
+ };
+ }
+ }
+
+ /**
+ * Adjusts an images scale and center so the image is centered and completely visible
+ * @param element
+ */
+ function fitToWindow(element)
+ {
+ var enabledElement = cornerstone.getEnabledElement(element);
+ var imageSize = getImageSize(enabledElement);
+
+ var verticalScale = enabledElement.canvas.height / imageSize.height;
+ var horizontalScale= enabledElement.canvas.width / imageSize.width;
+ if(horizontalScale < verticalScale) {
+ enabledElement.viewport.scale = horizontalScale;
+ }
+ else
+ {
+ enabledElement.viewport.scale = verticalScale;
+ }
+ enabledElement.viewport.translation.x = 0;
+ enabledElement.viewport.translation.y = 0;
+ cornerstone.updateImage(element);
+ }
+
+ cornerstone.fitToWindow = fitToWindow;
+}(cornerstone));
+
+/**
+ * This file is responsible for returning the default viewport for an image
+ */
+
+(function ($, cornerstone) {
+
+ "use strict";
+
+ /**
+ * returns a default viewport for display the specified image on the specified
+ * enabled element. The default viewport is fit to window
+ *
+ * @param element
+ * @param image
+ */
+ function getDefaultViewportForImage(element, image) {
+ var enabledElement = cornerstone.getEnabledElement(element);
+ var viewport = cornerstone.internal.getDefaultViewport(enabledElement.canvas, image);
+ return viewport;
+ }
+
+ // Module exports
+ cornerstone.getDefaultViewportForImage = getDefaultViewportForImage;
+}($, cornerstone));
+/**
+ * This module is responsible for returning the currently displayed image for an element
+ */
+
+(function ($, cornerstone) {
+
+ "use strict";
+
+ /**
+ * returns the currently displayed image for an element or undefined if no image has
+ * been displayed yet
+ *
+ * @param element
+ */
+ function getImage(element) {
+ var enabledElement = cornerstone.getEnabledElement(element);
+ return enabledElement.image;
+ }
+
+ // Module exports
+ cornerstone.getImage = getImage;
+}($, cornerstone));
+/**
+ * This module returns a subset of the stored pixels of an image
+ */
+(function (cornerstone) {
+
+ "use strict";
+
+ /**
+ * Returns array of pixels with modality LUT transformation applied
+ */
+ function getPixels(element, x, y, width, height) {
+
+ var storedPixels = cornerstone.getStoredPixels(element, x, y, width, height);
+ var ee = cornerstone.getEnabledElement(element);
+
+ var mlutfn = cornerstone.internal.getModalityLUT(ee.image.slope, ee.image.intercept, ee.viewport.modalityLUT);
+
+ var modalityPixels = storedPixels.map(mlutfn);
+
+ return modalityPixels;
+ }
+
+ // module exports
+ cornerstone.getPixels = getPixels;
+}(cornerstone));
+/**
+ * This module returns a subset of the stored pixels of an image
+ */
+(function (cornerstone) {
+
+ "use strict";
+
+ /**
+ * Returns an array of stored pixels given a rectangle in the image
+ * @param element
+ * @param x
+ * @param y
+ * @param width
+ * @param height
+ * @returns {Array}
+ */
+ function getStoredPixels(element, x, y, width, height) {
+ if(element === undefined) {
+ throw "getStoredPixels: parameter element must not be undefined";
+ }
+
+ x = Math.round(x);
+ y = Math.round(y);
+ var ee = cornerstone.getEnabledElement(element);
+ var storedPixels = [];
+ var index = 0;
+ var pixelData = ee.image.getPixelData();
+ for(var row=0; row < height; row++) {
+ for(var column=0; column < width; column++) {
+ var spIndex = ((row + y) * ee.image.columns) + (column + x);
+ storedPixels[index++] = pixelData[spIndex];
+ }
+ }
+ return storedPixels;
+ }
+
+ // module exports
+ cornerstone.getStoredPixels = getStoredPixels;
+}(cornerstone));
+/**
+ * This module contains functions to deal with getting and setting the viewport for an enabled element
+ */
+(function (cornerstone) {
+
+ "use strict";
+
+ /**
+ * Returns the viewport for the specified enabled element
+ * @param element
+ * @returns {*}
+ */
+ function getViewport(element) {
+ var enabledElement = cornerstone.getEnabledElement(element);
+
+ var viewport = enabledElement.viewport;
+ if(viewport === undefined) {
+ return undefined;
+ }
+ return {
+ scale : viewport.scale,
+ translation : {
+ x : viewport.translation.x,
+ y : viewport.translation.y
+ },
+ voi : {
+ windowWidth: viewport.voi.windowWidth,
+ windowCenter : viewport.voi.windowCenter
+ },
+ invert : viewport.invert,
+ pixelReplication: viewport.pixelReplication,
+ rotation: viewport.rotation,
+ hflip: viewport.hflip,
+ vflip: viewport.vflip,
+ modalityLUT: viewport.modalityLUT,
+ voiLUT: viewport.voiLUT
+ };
+ }
+
+ // module/private exports
+ cornerstone.getViewport = getViewport;
+
+}(cornerstone));
+
+/**
+ * This module deals with caching images
+ */
+
+(function (cornerstone) {
+
+ "use strict";
+
+ var imageCache = {};
+
+ var cachedImages = [];
+
+ var maximumSizeInBytes = 1024 * 1024 * 1024; // 1 GB
+ var cacheSizeInBytes = 0;
+
+ function setMaximumSizeBytes(numBytes) {
+ if (numBytes === undefined) {
+ throw "setMaximumSizeBytes: parameter numBytes must not be undefined";
+ }
+ if (numBytes.toFixed === undefined) {
+ throw "setMaximumSizeBytes: parameter numBytes must be a number";
+ }
+
+ maximumSizeInBytes = numBytes;
+ purgeCacheIfNecessary();
+ }
+
+ function purgeCacheIfNecessary() {
+ // if max cache size has not been exceeded, do nothing
+ if (cacheSizeInBytes <= maximumSizeInBytes) {
+ return;
+ }
+
+ // cache size has been exceeded, create list of images sorted by timeStamp
+ // so we can purge the least recently used image
+ function compare(a,b) {
+ if (a.timeStamp > b.timeStamp) {
+ return -1;
+ }
+ if (a.timeStamp < b.timeStamp) {
+ return 1;
+ }
+ return 0;
+ }
+ cachedImages.sort(compare);
+
+ // remove images as necessary
+ while(cacheSizeInBytes > maximumSizeInBytes) {
+ var lastCachedImage = cachedImages[cachedImages.length - 1];
+ cacheSizeInBytes -= lastCachedImage.sizeInBytes;
+ delete imageCache[lastCachedImage.imageId];
+ lastCachedImage.imagePromise.reject();
+ cachedImages.pop();
+ $(cornerstone).trigger('CornerstoneImageCachePromiseRemoved', {imageId: lastCachedImage.imageId});
+ }
+
+ var cacheInfo = cornerstone.imageCache.getCacheInfo();
+ $(cornerstone).trigger('CornerstoneImageCacheFull', cacheInfo);
+ }
+
+ function putImagePromise(imageId, imagePromise) {
+ if (imageId === undefined) {
+ throw "getImagePromise: imageId must not be undefined";
+ }
+ if (imagePromise === undefined) {
+ throw "getImagePromise: imagePromise must not be undefined";
+ }
+
+ if (imageCache.hasOwnProperty(imageId) === true) {
+ throw "putImagePromise: imageId already in cache";
+ }
+
+ var cachedImage = {
+ loaded : false,
+ imageId : imageId,
+ imagePromise : imagePromise,
+ timeStamp : new Date(),
+ sizeInBytes: 0
+ };
+
+ imageCache[imageId] = cachedImage;
+ cachedImages.push(cachedImage);
+
+ imagePromise.then(function(image) {
+ cachedImage.loaded = true;
+
+ if (image.sizeInBytes === undefined) {
+ throw "putImagePromise: image does not have sizeInBytes property or";
+ }
+ if (image.sizeInBytes.toFixed === undefined) {
+ throw "putImagePromise: image.sizeInBytes is not a number";
+ }
+ cachedImage.sizeInBytes = image.sizeInBytes;
+ cacheSizeInBytes += cachedImage.sizeInBytes;
+ purgeCacheIfNecessary();
+ });
+ }
+
+ function getImagePromise(imageId) {
+ if (imageId === undefined) {
+ throw "getImagePromise: imageId must not be undefined";
+ }
+ var cachedImage = imageCache[imageId];
+ if (cachedImage === undefined) {
+ return undefined;
+ }
+
+ // bump time stamp for cached image
+ cachedImage.timeStamp = new Date();
+ return cachedImage.imagePromise;
+ }
+
+ function removeImagePromise(imageId) {
+ if (imageId === undefined) {
+ throw "removeImagePromise: imageId must not be undefined";
+ }
+ var cachedImage = imageCache[imageId];
+ if (cachedImage === undefined) {
+ throw "removeImagePromise: imageId must not be undefined";
+ }
+ cachedImages.splice( cachedImages.indexOf(cachedImage), 1);
+ cacheSizeInBytes -= cachedImage.sizeInBytes;
+ delete imageCache[imageId];
+
+ return cachedImage.imagePromise;
+ }
+
+ function getCacheInfo() {
+ return {
+ maximumSizeInBytes : maximumSizeInBytes,
+ cacheSizeInBytes : cacheSizeInBytes,
+ numberOfImagesCached: cachedImages.length
+ };
+ }
+
+ function purgeCache() {
+ while (cachedImages.length > 0) {
+ var removedCachedImage = cachedImages.pop();
+ delete imageCache[removedCachedImage.imageId];
+ removedCachedImage.imagePromise.reject();
+ }
+ cacheSizeInBytes = 0;
+ }
+
+ // module exports
+ cornerstone.imageCache = {
+ putImagePromise : putImagePromise,
+ getImagePromise: getImagePromise,
+ removeImagePromise: removeImagePromise,
+ setMaximumSizeBytes: setMaximumSizeBytes,
+ getCacheInfo : getCacheInfo,
+ purgeCache: purgeCache,
+ cachedImages: cachedImages
+ };
+
+}(cornerstone));
+
+/**
+ * This module deals with ImageLoaders, loading images and caching images
+ */
+
+(function ($, cornerstone) {
+
+ "use strict";
+
+ var imageLoaders = {};
+
+ var unknownImageLoader;
+
+ function loadImageFromImageLoader(imageId) {
+ var colonIndex = imageId.indexOf(":");
+ var scheme = imageId.substring(0, colonIndex);
+ var loader = imageLoaders[scheme];
+ var imagePromise;
+ if(loader === undefined || loader === null) {
+ if(unknownImageLoader !== undefined) {
+ imagePromise = unknownImageLoader(imageId);
+ return imagePromise;
+ }
+ else {
+ return undefined;
+ }
+ }
+ imagePromise = loader(imageId);
+
+ // broadcast an image loaded event once the image is loaded
+ // This is based on the idea here: http://stackoverflow.com/questions/3279809/global-custom-events-in-jquery
+ imagePromise.then(function(image) {
+ $(cornerstone).trigger('CornerstoneImageLoaded', {image: image});
+ });
+
+ return imagePromise;
+ }
+
+ // Loads an image given an imageId and returns a promise which will resolve
+ // to the loaded image object or fail if an error occurred. The loaded image
+ // is not stored in the cache
+ function loadImage(imageId) {
+ if(imageId === undefined) {
+ throw "loadImage: parameter imageId must not be undefined";
+ }
+
+ var imagePromise = cornerstone.imageCache.getImagePromise(imageId);
+ if(imagePromise !== undefined) {
+ return imagePromise;
+ }
+
+ imagePromise = loadImageFromImageLoader(imageId);
+ if(imagePromise === undefined) {
+ throw "loadImage: no image loader for imageId";
+ }
+
+ return imagePromise;
+ }
+
+ // Loads an image given an imageId and returns a promise which will resolve
+ // to the loaded image object or fail if an error occurred. The image is
+ // stored in the cache
+ function loadAndCacheImage(imageId) {
+ if(imageId === undefined) {
+ throw "loadAndCacheImage: parameter imageId must not be undefined";
+ }
+
+ var imagePromise = cornerstone.imageCache.getImagePromise(imageId);
+ if(imagePromise !== undefined) {
+ return imagePromise;
+ }
+
+ imagePromise = loadImageFromImageLoader(imageId);
+ if(imagePromise === undefined) {
+ throw "loadAndCacheImage: no image loader for imageId";
+ }
+
+ cornerstone.imageCache.putImagePromise(imageId, imagePromise);
+
+ return imagePromise;
+ }
+
+
+ // registers an imageLoader plugin with cornerstone for the specified scheme
+ function registerImageLoader(scheme, imageLoader) {
+ imageLoaders[scheme] = imageLoader;
+ }
+
+ // Registers a new unknownImageLoader and returns the previous one (if it exists)
+ function registerUnknownImageLoader(imageLoader) {
+ var oldImageLoader = unknownImageLoader;
+ unknownImageLoader = imageLoader;
+ return oldImageLoader;
+ }
+
+ // module exports
+
+ cornerstone.loadImage = loadImage;
+ cornerstone.loadAndCacheImage = loadAndCacheImage;
+ cornerstone.registerImageLoader = registerImageLoader;
+ cornerstone.registerUnknownImageLoader = registerUnknownImageLoader;
+
+}($, cornerstone));
+
+(function (cornerstone) {
+
+ "use strict";
+
+ function calculateTransform(enabledElement, scale) {
+
+ var transform = new cornerstone.internal.Transform();
+ transform.translate(enabledElement.canvas.width/2, enabledElement.canvas.height / 2);
+
+ //Apply the rotation before scaling for non square pixels
+ var angle = enabledElement.viewport.rotation;
+ if(angle!==0) {
+ transform.rotate(angle*Math.PI/180);
+ }
+
+ // apply the scale
+ var widthScale = enabledElement.viewport.scale;
+ var heightScale = enabledElement.viewport.scale;
+ if(enabledElement.image.rowPixelSpacing < enabledElement.image.columnPixelSpacing) {
+ widthScale = widthScale * (enabledElement.image.columnPixelSpacing / enabledElement.image.rowPixelSpacing);
+ }
+ else if(enabledElement.image.columnPixelSpacing < enabledElement.image.rowPixelSpacing) {
+ heightScale = heightScale * (enabledElement.image.rowPixelSpacing / enabledElement.image.columnPixelSpacing);
+ }
+ transform.scale(widthScale, heightScale);
+
+ // unrotate to so we can translate unrotated
+ if(angle!==0) {
+ transform.rotate(-angle*Math.PI/180);
+ }
+
+ // apply the pan offset
+ transform.translate(enabledElement.viewport.translation.x, enabledElement.viewport.translation.y);
+
+ // rotate again so we can apply general scale
+ if(angle!==0) {
+ transform.rotate(angle*Math.PI/180);
+ }
+
+ if(scale !== undefined) {
+ // apply the font scale
+ transform.scale(scale, scale);
+ }
+
+ //Apply Flip if required
+ if(enabledElement.viewport.hflip) {
+ transform.scale(-1,1);
+ }
+
+ if(enabledElement.viewport.vflip) {
+ transform.scale(1,-1);
+ }
+
+ // translate the origin back to the corner of the image so the event handlers can draw in image coordinate system
+ transform.translate(-enabledElement.image.width / 2 , -enabledElement.image.height/ 2);
+ return transform;
+ }
+
+ // Module exports
+ cornerstone.internal.calculateTransform = calculateTransform;
+}(cornerstone));
+/**
+ * This module is responsible for drawing an image to an enabled elements canvas element
+ */
+
+(function ($, cornerstone) {
+
+ "use strict";
+
+ /**
+ * Internal API function to draw an image to a given enabled element
+ * @param enabledElement
+ * @param invalidated - true if pixel data has been invalidated and cached rendering should not be used
+ */
+ function drawImage(enabledElement, invalidated) {
+
+ var start = new Date();
+
+ if (enabledElement.render) {
+ enabledElement.render(enabledElement, invalidated);
+ } else {
+ enabledElement.image.render(enabledElement, invalidated);
+ }
+
+ 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,
+ renderTimeInMs : diff
+ };
+
+ $(enabledElement.element).trigger("CornerstoneImageRendered", eventData);
+ enabledElement.invalid = false;
+ }
+
+ // Module exports
+ cornerstone.internal.drawImage = drawImage;
+ cornerstone.drawImage = drawImage;
+
+}($, cornerstone));
+/**
+ * This module generates a lut for an image
+ */
+
+(function (cornerstone) {
+
+ "use strict";
+
+ function generateLutNew(image, windowWidth, windowCenter, invert, modalityLUT, voiLUT)
+ {
+ if(image.lut === undefined) {
+ image.lut = new Int16Array(image.maxPixelValue - Math.min(image.minPixelValue,0)+1);
+ }
+ var lut = image.lut;
+ var maxPixelValue = image.maxPixelValue;
+ var minPixelValue = image.minPixelValue;
+
+ var mlutfn = cornerstone.internal.getModalityLUT(image.slope, image.intercept, modalityLUT);
+ var vlutfn = cornerstone.internal.getVOILUT(windowWidth, windowCenter, voiLUT);
+
+ var offset = 0;
+ if(minPixelValue < 0) {
+ offset = minPixelValue;
+ }
+ var storedValue;
+ var modalityLutValue;
+ var voiLutValue;
+ var clampedValue;
+
+ for(storedValue = image.minPixelValue; storedValue <= maxPixelValue; storedValue++)
+ {
+ modalityLutValue = mlutfn(storedValue);
+ voiLutValue = vlutfn(modalityLutValue);
+ clampedValue = Math.min(Math.max(voiLutValue, 0), 255);
+ if(!invert) {
+ lut[storedValue+ (-offset)] = Math.round(clampedValue);
+ } else {
+ lut[storedValue + (-offset)] = Math.round(255 - clampedValue);
+ }
+ }
+ return lut;
+ }
+
+
+
+ /**
+ * Creates a LUT used while rendering to convert stored pixel values to
+ * display pixels
+ *
+ * @param image
+ * @returns {Array}
+ */
+ function generateLut(image, windowWidth, windowCenter, invert, modalityLUT, voiLUT)
+ {
+ if(modalityLUT || voiLUT) {
+ return generateLutNew(image, windowWidth, windowCenter, invert, modalityLUT, voiLUT);
+ }
+
+ if(image.lut === undefined) {
+ image.lut = new Int16Array(image.maxPixelValue - Math.min(image.minPixelValue,0)+1);
+ }
+ var lut = image.lut;
+
+ var maxPixelValue = image.maxPixelValue;
+ var minPixelValue = image.minPixelValue;
+ var slope = image.slope;
+ var intercept = image.intercept;
+ var localWindowWidth = windowWidth;
+ var localWindowCenter = windowCenter;
+ var modalityLutValue;
+ var voiLutValue;
+ var clampedValue;
+ var storedValue;
+
+ // NOTE: As of Nov 2014, most javascript engines have lower performance when indexing negative indexes.
+ // We improve performance by offsetting the pixel values for signed data to avoid negative indexes
+ // when generating the lut and then undo it in storedPixelDataToCanvasImagedata. Thanks to @jpambrun
+ // for this contribution!
+
+ var offset = 0;
+ if(minPixelValue < 0) {
+ offset = minPixelValue;
+ }
+
+ if(invert === true) {
+ for(storedValue = image.minPixelValue; storedValue <= maxPixelValue; storedValue++)
+ {
+ modalityLutValue = storedValue * slope + intercept;
+ voiLutValue = (((modalityLutValue - (localWindowCenter)) / (localWindowWidth) + 0.5) * 255.0);
+ clampedValue = Math.min(Math.max(voiLutValue, 0), 255);
+ lut[storedValue + (-offset)] = Math.round(255 - clampedValue);
+ }
+ }
+ else {
+ for(storedValue = image.minPixelValue; storedValue <= maxPixelValue; storedValue++)
+ {
+ modalityLutValue = storedValue * slope + intercept;
+ voiLutValue = (((modalityLutValue - (localWindowCenter)) / (localWindowWidth) + 0.5) * 255.0);
+ clampedValue = Math.min(Math.max(voiLutValue, 0), 255);
+ lut[storedValue+ (-offset)] = Math.round(clampedValue);
+ }
+ }
+ }
+
+
+ // Module exports
+ cornerstone.internal.generateLutNew = generateLutNew;
+ cornerstone.internal.generateLut = generateLut;
+ cornerstone.generateLutNew = generateLutNew;
+ cornerstone.generateLut = generateLut;
+}(cornerstone));
+
+/**
+ * This module contains a function to get a default viewport for an image given
+ * a canvas element to display it in
+ *
+ */
+(function (cornerstone) {
+
+ "use strict";
+
+ /**
+ * Creates a new viewport object containing default values for the image and canvas
+ * @param canvas
+ * @param image
+ * @returns viewport object
+ */
+ function getDefaultViewport(canvas, image) {
+ if(canvas === undefined) {
+ throw "getDefaultViewport: parameter canvas must not be undefined";
+ }
+ if(image === undefined) {
+ throw "getDefaultViewport: parameter image must not be undefined";
+ }
+ var viewport = {
+ scale : 1.0,
+ translation : {
+ x : 0,
+ y : 0
+ },
+ voi : {
+ windowWidth: image.windowWidth,
+ windowCenter: image.windowCenter,
+ },
+ invert: image.invert,
+ pixelReplication: false,
+ rotation: 0,
+ hflip: false,
+ vflip: false,
+ modalityLUT: image.modalityLUT,
+ voiLUT: image.voiLUT
+ };
+
+ // fit image to window
+ var verticalScale = canvas.height / image.rows;
+ var horizontalScale= canvas.width / image.columns;
+ if(horizontalScale < verticalScale) {
+ viewport.scale = horizontalScale;
+ }
+ else {
+ viewport.scale = verticalScale;
+ }
+ return viewport;
+ }
+
+ // module/private exports
+ cornerstone.internal.getDefaultViewport = getDefaultViewport;
+ cornerstone.getDefaultViewport = getDefaultViewport;
+}(cornerstone));
+
+(function (cornerstone) {
+
+ "use strict";
+
+ function getTransform(enabledElement)
+ {
+ // For now we will calculate it every time it is requested. In the future, we may want to cache
+ // it in the enabled element to speed things up
+ var transform = cornerstone.internal.calculateTransform(enabledElement);
+ return transform;
+ }
+
+ // Module exports
+ cornerstone.internal.getTransform = getTransform;
+
+}(cornerstone));
+/**
+ * This module is responsible for drawing an image to an enabled elements canvas element
+ */
+
+(function ($, cornerstone) {
+
+ "use strict";
+
+ cornerstone.drawImage = cornerstone.internal.drawImage;
+ cornerstone.generateLut = cornerstone.internal.generateLut;
+ cornerstone.storedPixelDataToCanvasImageData = cornerstone.internal.storedPixelDataToCanvasImageData;
+ cornerstone.storedColorPixelDataToCanvasImageData = cornerstone.internal.storedColorPixelDataToCanvasImageData;
+
+}($, cornerstone));
+/**
+ * This module generates a Modality LUT
+ */
+
+(function (cornerstone) {
+
+ "use strict";
+
+
+ function generateLinearModalityLUT(slope, intercept) {
+ var localSlope = slope;
+ var localIntercept = intercept;
+ return function(sp) {
+ return sp * localSlope + localIntercept;
+ }
+ }
+
+ function generateNonLinearModalityLUT(modalityLUT) {
+ var minValue = modalityLUT.lut[0];
+ var maxValue = modalityLUT.lut[modalityLUT.lut.length -1];
+ var maxValueMapped = modalityLUT.firstValueMapped + modalityLUT.lut.length;
+ return function(sp) {
+ if(sp < modalityLUT.firstValueMapped) {
+ return minValue;
+ }
+ else if(sp >= maxValueMapped)
+ {
+ return maxValue;
+ }
+ else
+ {
+ return modalityLUT.lut[sp];
+ }
+ }
+ }
+
+ function getModalityLUT(slope, intercept, modalityLUT) {
+ if (modalityLUT) {
+ return generateNonLinearModalityLUT(modalityLUT);
+ } else {
+ return generateLinearModalityLUT(slope, intercept);
+ }
+ }
+
+ // Module exports
+ cornerstone.internal.getModalityLUT = getModalityLUT;
+
+}(cornerstone));
+
+/**
+ * This module contains a function to convert stored pixel values to display pixel values using a LUT
+ */
+(function (cornerstone) {
+
+ "use strict";
+
+ function storedColorPixelDataToCanvasImageData(image, lut, canvasImageDataData)
+ {
+ var minPixelValue = image.minPixelValue;
+ var canvasImageDataIndex = 0;
+ var storedPixelDataIndex = 0;
+ var numPixels = image.width * image.height * 4;
+ var storedPixelData = image.getPixelData();
+ var localLut = lut;
+ var localCanvasImageDataData = canvasImageDataData;
+ // NOTE: As of Nov 2014, most javascript engines have lower performance when indexing negative indexes.
+ // We have a special code path for this case that improves performance. Thanks to @jpambrun for this enhancement
+ if(minPixelValue < 0){
+ while(storedPixelDataIndex < numPixels) {
+ localCanvasImageDataData[canvasImageDataIndex++] = localLut[storedPixelData[storedPixelDataIndex++] + (-minPixelValue)]; // red
+ localCanvasImageDataData[canvasImageDataIndex++] = localLut[storedPixelData[storedPixelDataIndex++] + (-minPixelValue)]; // green
+ localCanvasImageDataData[canvasImageDataIndex] = localLut[storedPixelData[storedPixelDataIndex] + (-minPixelValue)]; // blue
+ storedPixelDataIndex+=2;
+ canvasImageDataIndex+=2;
+ }
+ }else{
+ while(storedPixelDataIndex < numPixels) {
+ localCanvasImageDataData[canvasImageDataIndex++] = localLut[storedPixelData[storedPixelDataIndex++]]; // red
+ localCanvasImageDataData[canvasImageDataIndex++] = localLut[storedPixelData[storedPixelDataIndex++]]; // green
+ localCanvasImageDataData[canvasImageDataIndex] = localLut[storedPixelData[storedPixelDataIndex]]; // blue
+ storedPixelDataIndex+=2;
+ canvasImageDataIndex+=2;
+ }
+ }
+ }
+
+ // Module exports
+ cornerstone.internal.storedColorPixelDataToCanvasImageData = storedColorPixelDataToCanvasImageData;
+ cornerstone.storedColorPixelDataToCanvasImageData = storedColorPixelDataToCanvasImageData;
+
+}(cornerstone));
+
+/**
+ * This module contains a function to convert stored pixel values to display pixel values using a LUT
+ */
+(function (cornerstone) {
+
+ "use strict";
+
+ /**
+ * This function transforms stored pixel values into a canvas image data buffer
+ * by using a LUT. This is the most performance sensitive code in cornerstone and
+ * we use a special trick to make this go as fast as possible. Specifically we
+ * use the alpha channel only to control the luminance rather than the red, green and
+ * blue channels which makes it over 3x faster. The canvasImageDataData buffer needs
+ * to be previously filled with white pixels.
+ *
+ * NOTE: Attribution would be appreciated if you use this technique!
+ *
+ * @param pixelData the pixel data
+ * @param lut the lut
+ * @param canvasImageDataData a canvasImgageData.data buffer filled with white pixels
+ */
+ function storedPixelDataToCanvasImageData(image, lut, canvasImageDataData)
+ {
+ var pixelData = image.getPixelData();
+ var minPixelValue = image.minPixelValue;
+ var canvasImageDataIndex = 3;
+ var storedPixelDataIndex = 0;
+ var localNumPixels = pixelData.length;
+ var localPixelData = pixelData;
+ var localLut = lut;
+ var localCanvasImageDataData = canvasImageDataData;
+ // NOTE: As of Nov 2014, most javascript engines have lower performance when indexing negative indexes.
+ // We have a special code path for this case that improves performance. Thanks to @jpambrun for this enhancement
+ if(minPixelValue < 0){
+ while(storedPixelDataIndex < localNumPixels) {
+ localCanvasImageDataData[canvasImageDataIndex] = localLut[localPixelData[storedPixelDataIndex++] + (-minPixelValue)]; // alpha
+ canvasImageDataIndex += 4;
+ }
+ }else{
+ while(storedPixelDataIndex < localNumPixels) {
+ localCanvasImageDataData[canvasImageDataIndex] = localLut[localPixelData[storedPixelDataIndex++]]; // alpha
+ canvasImageDataIndex += 4;
+ }
+ }
+ }
+
+ // Module exports
+ cornerstone.internal.storedPixelDataToCanvasImageData = storedPixelDataToCanvasImageData;
+ cornerstone.storedPixelDataToCanvasImageData = storedPixelDataToCanvasImageData;
+
+}(cornerstone));
+
+// Last updated November 2011
+// By Simon Sarris
+// www.simonsarris.com
+// sarris@acm.org
+//
+// Free to use and distribute at will
+// So long as you are nice to people, etc
+
+// Simple class for keeping track of the current transformation matrix
+
+// For instance:
+// var t = new Transform();
+// t.rotate(5);
+// var m = t.m;
+// ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
+
+// Is equivalent to:
+// ctx.rotate(5);
+
+// But now you can retrieve it :)
+
+(function (cornerstone) {
+
+ "use strict";
+
+
+ // Remember that this does not account for any CSS transforms applied to the canvas
+ function Transform() {
+ this.reset();
+ }
+
+ Transform.prototype.reset = function() {
+ this.m = [1,0,0,1,0,0];
+ };
+
+ Transform.prototype.clone = function() {
+ var transform = new Transform();
+ transform.m[0] = this.m[0];
+ transform.m[1] = this.m[1];
+ transform.m[2] = this.m[2];
+ transform.m[3] = this.m[3];
+ transform.m[4] = this.m[4];
+ transform.m[5] = this.m[5];
+ return transform;
+ };
+
+
+ Transform.prototype.multiply = function(matrix) {
+ var m11 = this.m[0] * matrix.m[0] + this.m[2] * matrix.m[1];
+ var m12 = this.m[1] * matrix.m[0] + this.m[3] * matrix.m[1];
+
+ var m21 = this.m[0] * matrix.m[2] + this.m[2] * matrix.m[3];
+ var m22 = this.m[1] * matrix.m[2] + this.m[3] * matrix.m[3];
+
+ var dx = this.m[0] * matrix.m[4] + this.m[2] * matrix.m[5] + this.m[4];
+ var dy = this.m[1] * matrix.m[4] + this.m[3] * matrix.m[5] + this.m[5];
+
+ this.m[0] = m11;
+ this.m[1] = m12;
+ this.m[2] = m21;
+ this.m[3] = m22;
+ this.m[4] = dx;
+ this.m[5] = dy;
+ };
+
+ Transform.prototype.invert = function() {
+ var d = 1 / (this.m[0] * this.m[3] - this.m[1] * this.m[2]);
+ var m0 = this.m[3] * d;
+ var m1 = -this.m[1] * d;
+ var m2 = -this.m[2] * d;
+ var m3 = this.m[0] * d;
+ var m4 = d * (this.m[2] * this.m[5] - this.m[3] * this.m[4]);
+ var m5 = d * (this.m[1] * this.m[4] - this.m[0] * this.m[5]);
+ this.m[0] = m0;
+ this.m[1] = m1;
+ this.m[2] = m2;
+ this.m[3] = m3;
+ this.m[4] = m4;
+ this.m[5] = m5;
+ };
+
+ Transform.prototype.rotate = function(rad) {
+ var c = Math.cos(rad);
+ var s = Math.sin(rad);
+ var m11 = this.m[0] * c + this.m[2] * s;
+ var m12 = this.m[1] * c + this.m[3] * s;
+ var m21 = this.m[0] * -s + this.m[2] * c;
+ var m22 = this.m[1] * -s + this.m[3] * c;
+ this.m[0] = m11;
+ this.m[1] = m12;
+ this.m[2] = m21;
+ this.m[3] = m22;
+ };
+
+ Transform.prototype.translate = function(x, y) {
+ this.m[4] += this.m[0] * x + this.m[2] * y;
+ this.m[5] += this.m[1] * x + this.m[3] * y;
+ };
+
+ Transform.prototype.scale = function(sx, sy) {
+ this.m[0] *= sx;
+ this.m[1] *= sx;
+ this.m[2] *= sy;
+ this.m[3] *= sy;
+ };
+
+ Transform.prototype.transformPoint = function(px, py) {
+ var x = px;
+ var y = py;
+ px = x * this.m[0] + y * this.m[2] + this.m[4];
+ py = x * this.m[1] + y * this.m[3] + this.m[5];
+ return {x: px, y: py};
+ };
+
+ cornerstone.internal.Transform = Transform;
+}(cornerstone));
+/**
+ * This module generates a VOI LUT
+ */
+
+(function (cornerstone) {
+
+ "use strict";
+
+ function generateLinearVOILUT(windowWidth, windowCenter) {
+ return function(modalityLutValue) {
+ return (((modalityLutValue - (windowCenter)) / (windowWidth) + 0.5) * 255.0);
+ }
+ }
+
+ function generateNonLinearVOILUT(voiLUT) {
+ var shift = voiLUT.numBitsPerEntry - 8;
+ var minValue = voiLUT.lut[0] >> shift;
+ var maxValue = voiLUT.lut[voiLUT.lut.length -1] >> shift;
+ var maxValueMapped = voiLUT.firstValueMapped + voiLUT.lut.length - 1;
+ return function(modalityLutValue) {
+ if(modalityLutValue < voiLUT.firstValueMapped) {
+ return minValue;
+ }
+ else if(modalityLutValue >= maxValueMapped)
+ {
+ return maxValue;
+ }
+ else
+ {
+ return voiLUT.lut[modalityLutValue - voiLUT.firstValueMapped] >> shift;
+ }
+ }
+ }
+
+ function getVOILUT(windowWidth, windowCenter, voiLUT) {
+ if(voiLUT) {
+ return generateNonLinearVOILUT(voiLUT);
+ } else {
+ return generateLinearVOILUT(windowWidth, windowCenter);
+ }
+ }
+
+ // Module exports
+ cornerstone.internal.getVOILUT = getVOILUT;
+}(cornerstone));
+
+/**
+ * This module contains a function to make an image is invalid
+ */
+(function (cornerstone) {
+
+ "use strict";
+
+ /**
+ * Sets the invalid flag on the enabled element and fire an event
+ * @param element
+ */
+ function invalidate(element) {
+ var enabledElement = cornerstone.getEnabledElement(element);
+ enabledElement.invalid = true;
+ var eventData = {
+ element: element
+ };
+ $(enabledElement.element).trigger("CornerstoneInvalidated", eventData);
+ }
+
+ // module exports
+ cornerstone.invalidate = invalidate;
+}(cornerstone));
+/**
+ * This module contains a function to immediately invalidate an image
+ */
+(function (cornerstone) {
+
+ "use strict";
+
+ /**
+ * Forces the image to be updated/redrawn for the specified enabled element
+ * @param element
+ */
+ function invalidateImageId(imageId) {
+
+ var enabledElements = cornerstone.getEnabledElementsByImageId(imageId);
+ enabledElements.forEach(function(enabledElement) {
+ cornerstone.drawImage(enabledElement, true);
+ });
+ }
+
+ // module exports
+ cornerstone.invalidateImageId = invalidateImageId;
+}(cornerstone));
+/**
+ * This module contains a helper function to covert page coordinates to pixel coordinates
+ */
+(function (cornerstone) {
+
+ "use strict";
+
+ /**
+ * Converts a point in the page coordinate system to the pixel coordinate
+ * system
+ * @param element
+ * @param pageX
+ * @param pageY
+ * @returns {{x: number, y: number}}
+ */
+ function pageToPixel(element, pageX, pageY) {
+ var enabledElement = cornerstone.getEnabledElement(element);
+
+ if(enabledElement.image === undefined) {
+ throw "image has not been loaded yet";
+ }
+
+ var image = enabledElement.image;
+
+ // convert the pageX and pageY to the canvas client coordinates
+ var rect = element.getBoundingClientRect();
+ var clientX = pageX - rect.left - window.pageXOffset;
+ var clientY = pageY - rect.top - window.pageYOffset;
+
+ var pt = {x: clientX, y: clientY};
+ var transform = cornerstone.internal.getTransform(enabledElement);
+ transform.invert();
+ return transform.transformPoint(pt.x, pt.y);
+ }
+
+ // module/private exports
+ cornerstone.pageToPixel = pageToPixel;
+
+}(cornerstone));
+
+(function (cornerstone) {
+
+ "use strict";
+
+ /**
+ * Converts a point in the pixel coordinate system to the canvas coordinate system
+ * system. This can be used to render using canvas context without having the weird
+ * side effects that come from scaling and non square pixels
+ * @param element
+ * @param pt
+ * @returns {x: number, y: number}
+ */
+ function pixelToCanvas(element, pt) {
+ var enabledElement = cornerstone.getEnabledElement(element);
+ var transform = cornerstone.internal.getTransform(enabledElement);
+ return transform.transformPoint(pt.x, pt.y);
+ }
+
+ // module/private exports
+ cornerstone.pixelToCanvas = pixelToCanvas;
+
+}(cornerstone));
+
+/**
+ * This module is responsible for drawing an image to an enabled elements canvas element
+ */
+
+(function (cornerstone) {
+
+ "use strict";
+
+ var colorRenderCanvas = document.createElement('canvas');
+ var colorRenderCanvasContext;
+ var colorRenderCanvasData;
+
+ var lastRenderedImageId;
+ var lastRenderedViewport = {};
+
+ function initializeColorRenderCanvas(image)
+ {
+ // Resize the canvas
+ colorRenderCanvas.width = image.width;
+ colorRenderCanvas.height = image.height;
+
+ // get the canvas data so we can write to it directly
+ colorRenderCanvasContext = colorRenderCanvas.getContext('2d');
+ colorRenderCanvasContext.fillStyle = 'white';
+ colorRenderCanvasContext.fillRect(0,0, colorRenderCanvas.width, colorRenderCanvas.height);
+ colorRenderCanvasData = colorRenderCanvasContext.getImageData(0,0,image.width, image.height);
+ }
+
+
+ function getLut(image, viewport)
+ {
+ // if we have a cached lut and it has the right values, return it immediately
+ if(image.lut !== undefined &&
+ image.lut.windowCenter === viewport.voi.windowCenter &&
+ image.lut.windowWidth === viewport.voi.windowWidth &&
+ image.lut.invert === viewport.invert) {
+ return image.lut;
+ }
+
+ // lut is invalid or not present, regenerate it and cache it
+ cornerstone.generateLut(image, viewport.voi.windowWidth, viewport.voi.windowCenter, viewport.invert);
+ image.lut.windowWidth = viewport.voi.windowWidth;
+ image.lut.windowCenter = viewport.voi.windowCenter;
+ image.lut.invert = viewport.invert;
+ return image.lut;
+ }
+
+ function doesImageNeedToBeRendered(enabledElement, image)
+ {
+ if(image.imageId !== lastRenderedImageId ||
+ lastRenderedViewport.windowCenter !== enabledElement.viewport.voi.windowCenter ||
+ lastRenderedViewport.windowWidth !== enabledElement.viewport.voi.windowWidth ||
+ lastRenderedViewport.invert !== enabledElement.viewport.invert ||
+ lastRenderedViewport.rotation !== enabledElement.viewport.rotation ||
+ lastRenderedViewport.hflip !== enabledElement.viewport.hflip ||
+ lastRenderedViewport.vflip !== enabledElement.viewport.vflip
+ )
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ function getRenderCanvas(enabledElement, image, invalidated)
+ {
+ // apply the lut to the stored pixel data onto the render canvas
+
+ if(enabledElement.viewport.voi.windowWidth === enabledElement.image.windowWidth &&
+ enabledElement.viewport.voi.windowCenter === enabledElement.image.windowCenter &&
+ enabledElement.viewport.invert === false)
+ {
+ // the color image voi/invert has not been modified, request the canvas that contains
+ // it so we can draw it directly to the display canvas
+ return image.getCanvas();
+ }
+ else
+ {
+ if(doesImageNeedToBeRendered(enabledElement, image) === false && invalidated !== true) {
+ return colorRenderCanvas;
+ }
+
+ // If our render canvas does not match the size of this image reset it
+ // NOTE: This might be inefficient if we are updating multiple images of different
+ // sizes frequently.
+ if(colorRenderCanvas.width !== image.width || colorRenderCanvas.height != image.height) {
+ initializeColorRenderCanvas(image);
+ }
+
+ // get the lut to use
+ var colorLut = getLut(image, enabledElement.viewport);
+
+ // the color image voi/invert has been modified - apply the lut to the underlying
+ // pixel data and put it into the renderCanvas
+ cornerstone.storedColorPixelDataToCanvasImageData(image, colorLut, colorRenderCanvasData.data);
+ colorRenderCanvasContext.putImageData(colorRenderCanvasData, 0, 0);
+ return colorRenderCanvas;
+ }
+ }
+
+ /**
+ * API function to render a color image to an enabled element
+ * @param enabledElement
+ * @param invalidated - true if pixel data has been invaldiated and cached rendering should not be used
+ */
+ function renderColorImage(enabledElement, invalidated) {
+
+ if(enabledElement === undefined) {
+ throw "drawImage: enabledElement parameter must not be undefined";
+ }
+ var image = enabledElement.image;
+ if(image === undefined) {
+ throw "drawImage: image must be loaded before it can be drawn";
+ }
+
+ // get the canvas context and reset the transform
+ var context = enabledElement.canvas.getContext('2d');
+ context.setTransform(1, 0, 0, 1, 0, 0);
+
+ // clear the canvas
+ context.fillStyle = 'black';
+ context.fillRect(0,0, enabledElement.canvas.width, enabledElement.canvas.height);
+
+ // turn off image smooth/interpolation if pixelReplication is set in the viewport
+ if(enabledElement.viewport.pixelReplication === true) {
+ context.imageSmoothingEnabled = false;
+ context.mozImageSmoothingEnabled = false; // firefox doesn't support imageSmoothingEnabled yet
+ }
+ else {
+ context.imageSmoothingEnabled = true;
+ context.mozImageSmoothingEnabled = true;
+ }
+
+ // save the canvas context state and apply the viewport properties
+ context.save();
+ cornerstone.setToPixelCoordinateSystem(enabledElement, context);
+
+ var renderCanvas = getRenderCanvas(enabledElement, image, invalidated);
+
+ context.drawImage(renderCanvas, 0,0, image.width, image.height, 0, 0, image.width, image.height);
+
+ context.restore();
+
+ lastRenderedImageId = image.imageId;
+ lastRenderedViewport.windowCenter = enabledElement.viewport.voi.windowCenter;
+ lastRenderedViewport.windowWidth = enabledElement.viewport.voi.windowWidth;
+ lastRenderedViewport.invert = enabledElement.viewport.invert;
+ lastRenderedViewport.rotation = enabledElement.viewport.rotation;
+ lastRenderedViewport.hflip = enabledElement.viewport.hflip;
+ lastRenderedViewport.vflip = enabledElement.viewport.vflip;
+ }
+
+ // Module exports
+ cornerstone.rendering.colorImage = renderColorImage;
+ cornerstone.renderColorImage = renderColorImage;
+}(cornerstone));
+
+/**
+ * This module is responsible for drawing a grayscale image
+ */
+
+(function (cornerstone) {
+
+ "use strict";
+
+ var grayscaleRenderCanvas = document.createElement('canvas');
+ var grayscaleRenderCanvasContext;
+ var grayscaleRenderCanvasData;
+
+ var lastRenderedImageId;
+ var lastRenderedViewport = {};
+
+ function initializeGrayscaleRenderCanvas(image)
+ {
+ // Resize the canvas
+ grayscaleRenderCanvas.width = image.width;
+ grayscaleRenderCanvas.height = image.height;
+
+ // NOTE - we need to fill the render canvas with white pixels since we control the luminance
+ // using the alpha channel to improve rendering performance.
+ grayscaleRenderCanvasContext = grayscaleRenderCanvas.getContext('2d');
+ grayscaleRenderCanvasContext.fillStyle = 'white';
+ grayscaleRenderCanvasContext.fillRect(0,0, grayscaleRenderCanvas.width, grayscaleRenderCanvas.height);
+ grayscaleRenderCanvasData = grayscaleRenderCanvasContext.getImageData(0,0,image.width, image.height);
+ }
+
+ function lutMatches(a, b) {
+ // if undefined, they are equal
+ if(!a && !b) {
+ return true;
+ }
+ // if one is undefined, not equal
+ if(!a || !b) {
+ return false;
+ }
+ // check the unique ids
+ return (a.id !== b.id)
+ }
+
+ function getLut(image, viewport, invalidated)
+ {
+ // if we have a cached lut and it has the right values, return it immediately
+ if(image.lut !== undefined &&
+ image.lut.windowCenter === viewport.voi.windowCenter &&
+ image.lut.windowWidth === viewport.voi.windowWidth &&
+ lutMatches(image.lut.modalityLUT, viewport.modalityLUT) &&
+ lutMatches(image.lut.voiLUT, viewport.voiLUT) &&
+ image.lut.invert === viewport.invert &&
+ invalidated !== true) {
+ return image.lut;
+ }
+
+ // lut is invalid or not present, regenerate it and cache it
+ cornerstone.generateLut(image, viewport.voi.windowWidth, viewport.voi.windowCenter, viewport.invert, viewport.modalityLUT, viewport.voiLUT);
+ image.lut.windowWidth = viewport.voi.windowWidth;
+ image.lut.windowCenter = viewport.voi.windowCenter;
+ image.lut.invert = viewport.invert;
+ image.lut.voiLUT = viewport.voiLUT;
+ image.lut.modalityLUT = viewport.modalityLUT;
+ return image.lut;
+ }
+
+ function doesImageNeedToBeRendered(enabledElement, image)
+ {
+ if(image.imageId !== lastRenderedImageId ||
+ lastRenderedViewport.windowCenter !== enabledElement.viewport.voi.windowCenter ||
+ lastRenderedViewport.windowWidth !== enabledElement.viewport.voi.windowWidth ||
+ lastRenderedViewport.invert !== enabledElement.viewport.invert ||
+ lastRenderedViewport.rotation !== enabledElement.viewport.rotation ||
+ lastRenderedViewport.hflip !== enabledElement.viewport.hflip ||
+ lastRenderedViewport.vflip !== enabledElement.viewport.vflip ||
+ lastRenderedViewport.modalityLUT !== enabledElement.viewport.modalityLUT ||
+ lastRenderedViewport.voiLUT !== enabledElement.viewport.voiLUT
+ )
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ function getRenderCanvas(enabledElement, image, invalidated)
+ {
+ // apply the lut to the stored pixel data onto the render canvas
+
+ if(doesImageNeedToBeRendered(enabledElement, image) === false && invalidated !== true) {
+ return grayscaleRenderCanvas;
+ }
+
+ // If our render canvas does not match the size of this image reset it
+ // NOTE: This might be inefficient if we are updating multiple images of different
+ // sizes frequently.
+ if(grayscaleRenderCanvas.width !== image.width || grayscaleRenderCanvas.height != image.height) {
+ initializeGrayscaleRenderCanvas(image);
+ }
+
+ // get the lut to use
+ var lut = getLut(image, enabledElement.viewport, invalidated);
+ // gray scale image - apply the lut and put the resulting image onto the render canvas
+ cornerstone.storedPixelDataToCanvasImageData(image, lut, grayscaleRenderCanvasData.data);
+ grayscaleRenderCanvasContext.putImageData(grayscaleRenderCanvasData, 0, 0);
+ return grayscaleRenderCanvas;
+ }
+
+ /**
+ * API function to draw a grayscale image to a given enabledElement
+ * @param enabledElement
+ * @param invalidated - true if pixel data has been invaldiated and cached rendering should not be used
+ */
+ function renderGrayscaleImage(enabledElement, invalidated) {
+
+ if(enabledElement === undefined) {
+ throw "drawImage: enabledElement parameter must not be undefined";
+ }
+ var image = enabledElement.image;
+ if(image === undefined) {
+ throw "drawImage: image must be loaded before it can be drawn";
+ }
+
+ // get the canvas context and reset the transform
+ var context = enabledElement.canvas.getContext('2d');
+ context.setTransform(1, 0, 0, 1, 0, 0);
+
+ // clear the canvas
+ context.fillStyle = 'black';
+ context.fillRect(0,0, enabledElement.canvas.width, enabledElement.canvas.height);
+
+ // turn off image smooth/interpolation if pixelReplication is set in the viewport
+ if(enabledElement.viewport.pixelReplication === true) {
+ context.imageSmoothingEnabled = false;
+ context.mozImageSmoothingEnabled = false; // firefox doesn't support imageSmoothingEnabled yet
+ }
+ else {
+ context.imageSmoothingEnabled = true;
+ context.mozImageSmoothingEnabled = true;
+ }
+
+ // save the canvas context state and apply the viewport properties
+ cornerstone.setToPixelCoordinateSystem(enabledElement, context);
+
+ var renderCanvas = getRenderCanvas(enabledElement, image, invalidated);
+
+ // Draw the render canvas half the image size (because we set origin to the middle of the canvas above)
+ context.drawImage(renderCanvas, 0,0, image.width, image.height, 0, 0, image.width, image.height);
+
+ lastRenderedImageId = image.imageId;
+ lastRenderedViewport.windowCenter = enabledElement.viewport.voi.windowCenter;
+ lastRenderedViewport.windowWidth = enabledElement.viewport.voi.windowWidth;
+ lastRenderedViewport.invert = enabledElement.viewport.invert;
+ lastRenderedViewport.rotation = enabledElement.viewport.rotation;
+ lastRenderedViewport.hflip = enabledElement.viewport.hflip;
+ lastRenderedViewport.vflip = enabledElement.viewport.vflip;
+ lastRenderedViewport.modalityLUT = enabledElement.viewport.modalityLUT;
+ lastRenderedViewport.voiLUT = enabledElement.viewport.voiLUT;
+ }
+
+ // Module exports
+ cornerstone.rendering.grayscaleImage = renderGrayscaleImage;
+ cornerstone.renderGrayscaleImage = renderGrayscaleImage;
+
+}(cornerstone));
+
+/**
+ * This module is responsible for drawing an image to an enabled elements canvas element
+ */
+
+(function (cornerstone) {
+
+ "use strict";
+
+ /**
+ * API function to draw a standard web image (PNG, JPG) to an enabledImage
+ *
+ * @param enabledElement
+ * @param invalidated - true if pixel data has been invaldiated and cached rendering should not be used
+ */
+ function renderWebImage(enabledElement, invalidated) {
+
+ if(enabledElement === undefined) {
+ throw "drawImage: enabledElement parameter must not be undefined";
+ }
+ var image = enabledElement.image;
+ if(image === undefined) {
+ throw "drawImage: image must be loaded before it can be drawn";
+ }
+
+ // get the canvas context and reset the transform
+ var context = enabledElement.canvas.getContext('2d');
+ context.setTransform(1, 0, 0, 1, 0, 0);
+
+ // clear the canvas
+ context.fillStyle = 'black';
+ context.fillRect(0,0, enabledElement.canvas.width, enabledElement.canvas.height);
+
+ // turn off image smooth/interpolation if pixelReplication is set in the viewport
+ if(enabledElement.viewport.pixelReplication === true) {
+ context.imageSmoothingEnabled = false;
+ context.mozImageSmoothingEnabled = false; // firefox doesn't support imageSmoothingEnabled yet
+ }
+ else {
+ context.imageSmoothingEnabled = true;
+ context.mozImageSmoothingEnabled = true;
+ }
+
+ // save the canvas context state and apply the viewport properties
+ cornerstone.setToPixelCoordinateSystem(enabledElement, context);
+
+ // if the viewport ww/wc and invert all match the initial state of the image, we can draw the image
+ // directly. If any of those are changed, we call renderColorImage() to apply the lut
+ if(enabledElement.viewport.voi.windowWidth === enabledElement.image.windowWidth &&
+ enabledElement.viewport.voi.windowCenter === enabledElement.image.windowCenter &&
+ enabledElement.viewport.invert === false)
+ {
+ context.drawImage(image.getImage(), 0, 0, image.width, image.height, 0, 0, image.width, image.height);
+ } else {
+ cornerstone.renderColorImage(enabledElement, invalidated);
+ }
+
+ }
+
+ // Module exports
+ cornerstone.rendering.webImage = renderWebImage;
+ cornerstone.renderWebImage = renderWebImage;
+
+}(cornerstone));
+/**
+ */
+(function (cornerstone) {
+
+ "use strict";
+
+ /**
+ * Resets the viewport to the default settings
+ *
+ * @param element
+ */
+ function reset(element)
+ {
+ var enabledElement = cornerstone.getEnabledElement(element);
+ var defaultViewport = cornerstone.internal.getDefaultViewport(enabledElement.canvas, enabledElement.image);
+ enabledElement.viewport = defaultViewport;
+ cornerstone.updateImage(element);
+ }
+
+ cornerstone.reset = reset;
+}(cornerstone));
+
+/**
+ * This module is responsible for enabling an element to display images with cornerstone
+ */
+(function (cornerstone) {
+
+ "use strict";
+
+ function setCanvasSize(element, canvas)
+ {
+ // the device pixel ratio is 1.0 for normal displays and > 1.0
+ // for high DPI displays like Retina
+ /*
+
+ This functionality is disabled due to buggy behavior on systems with mixed DPI's. If the canvas
+ is created on a display with high DPI (e.g. 2.0) and then the browser window is dragged to
+ a different display with a different DPI (e.g. 1.0), the canvas is not recreated so the pageToPixel
+ produces incorrect results. I couldn't find any way to determine when the DPI changed other than
+ by polling which is not very clean. If anyone has any ideas here, please let me know, but for now
+ we will disable this functionality. We may want
+ to add a mechanism to optionally enable this functionality if we can determine it is safe to do
+ so (e.g. iPad or iPhone or perhaps enumerate the displays on the system. I am choosing
+ to be cautious here since I would rather not have bug reports or safety issues related to this
+ scenario.
+
+ var devicePixelRatio = window.devicePixelRatio;
+ if(devicePixelRatio === undefined) {
+ devicePixelRatio = 1.0;
+ }
+ */
+
+ canvas.width = element.clientWidth;
+ canvas.height = element.clientHeight;
+ canvas.style.width = element.clientWidth + "px";
+ canvas.style.height = element.clientHeight + "px";
+ }
+
+ /**
+ * resizes an enabled element and optionally fits the image to window
+ * @param element
+ * @param fitToWindow true to refit, false to leave viewport parameters as they are
+ */
+ function resize(element, fitToWindow) {
+
+ var enabledElement = cornerstone.getEnabledElement(element);
+
+ setCanvasSize(element, enabledElement.canvas);
+
+ if(enabledElement.image === undefined ) {
+ return;
+ }
+
+ if(fitToWindow === true) {
+ cornerstone.fitToWindow(element);
+ }
+ else {
+ cornerstone.updateImage(element);
+ }
+ }
+
+ // module/private exports
+ cornerstone.resize = resize;
+
+}(cornerstone));
+/**
+ * This module contains a function that will set the canvas context to the pixel coordinates system
+ * making it easy to draw geometry on the image
+ */
+
+(function (cornerstone) {
+
+ "use strict";
+
+ /**
+ * Sets the canvas context transformation matrix to the pixel coordinate system. This allows
+ * geometry to be driven using the canvas context using coordinates in the pixel coordinate system
+ * @param ee
+ * @param context
+ * @param scale optional scaler to apply
+ */
+ function setToPixelCoordinateSystem(enabledElement, context, scale)
+ {
+ if(enabledElement === undefined) {
+ throw "setToPixelCoordinateSystem: parameter enabledElement must not be undefined";
+ }
+ if(context === undefined) {
+ throw "setToPixelCoordinateSystem: parameter context must not be undefined";
+ }
+
+ var transform = cornerstone.internal.calculateTransform(enabledElement, scale);
+ context.setTransform(transform.m[0],transform.m[1],transform.m[2],transform.m[3],transform.m[4],transform.m[5],transform.m[6]);
+ }
+
+ // Module exports
+ cornerstone.setToPixelCoordinateSystem = setToPixelCoordinateSystem;
+}(cornerstone));
+/**
+ * This module contains functions to deal with getting and setting the viewport for an enabled element
+ */
+(function (cornerstone) {
+
+ "use strict";
+
+ /**
+ * Sets the viewport for an element and corrects invalid values
+ *
+ * @param element - DOM element of the enabled element
+ * @param viewport - Object containing the viewport properties
+ * @returns {*}
+ */
+ function setViewport(element, viewport) {
+
+ var enabledElement = cornerstone.getEnabledElement(element);
+
+ enabledElement.viewport.scale = viewport.scale;
+ enabledElement.viewport.translation.x = viewport.translation.x;
+ enabledElement.viewport.translation.y = viewport.translation.y;
+ enabledElement.viewport.voi.windowWidth = viewport.voi.windowWidth;
+ enabledElement.viewport.voi.windowCenter = viewport.voi.windowCenter;
+ enabledElement.viewport.invert = viewport.invert;
+ enabledElement.viewport.pixelReplication = viewport.pixelReplication;
+ enabledElement.viewport.rotation = viewport.rotation;
+ enabledElement.viewport.hflip = viewport.hflip;
+ enabledElement.viewport.vflip = viewport.vflip;
+ enabledElement.viewport.modalityLUT = viewport.modalityLUT;
+ enabledElement.viewport.voiLUT = viewport.voiLUT;
+
+ // prevent window width from being too small (note that values close to zero are valid and can occur with
+ // PET images in particular)
+ if(enabledElement.viewport.voi.windowWidth < 0.000001) {
+ enabledElement.viewport.voi.windowWidth = 0.000001;
+ }
+ // prevent scale from getting too small
+ if(enabledElement.viewport.scale < 0.0001) {
+ enabledElement.viewport.scale = 0.25;
+ }
+
+ if(enabledElement.viewport.rotation===360 || enabledElement.viewport.rotation===-360) {
+ enabledElement.viewport.rotation = 0;
+ }
+
+ // Force the image to be updated since the viewport has been modified
+ cornerstone.updateImage(element);
+ }
+
+
+ // module/private exports
+ cornerstone.setViewport = setViewport;
+
+}(cornerstone));
+
+/**
+ * This module contains a function to immediately redraw an image
+ */
+(function (cornerstone) {
+
+ "use strict";
+
+ /**
+ * Forces the image to be updated/redrawn for the specified enabled element
+ * @param element
+ */
+ function updateImage(element, invalidated) {
+ var enabledElement = cornerstone.getEnabledElement(element);
+
+ if(enabledElement.image === undefined) {
+ throw "updateImage: image has not been loaded yet";
+ }
+
+ cornerstone.drawImage(enabledElement, invalidated);
+ }
+
+ // module exports
+ cornerstone.updateImage = updateImage;
+
+}(cornerstone));
+(function (cornerstone) {
+
+ "use strict";
+
+ if (!cornerstone.webGL) {
+ cornerstone.webGL = {};
+ }
+
+ /**
+ * Creates and compiles a shader.
+ *
+ * @param {!WebGLRenderingContext} gl The WebGL Context.
+ * @param {string} shaderSource The GLSL source code for the shader.
+ * @param {number} shaderType The type of shader, VERTEX_SHADER or FRAGMENT_SHADER.
+ *
+ * @return {!WebGLShader} The shader.
+ */
+ function compileShader(gl, shaderSource, shaderType) {
+
+ // Create the shader object
+ var shader = gl.createShader(shaderType);
+
+ // Set the shader source code.
+ gl.shaderSource(shader, shaderSource);
+
+ // Compile the shader
+ gl.compileShader(shader);
+
+ // Check if it compiled
+ var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
+ if (!success && !gl.isContextLost()) {
+ // Something went wrong during compilation; get the error
+ var infoLog = gl.getShaderInfoLog(shader);
+ console.error("Could not compile shader:\n" + infoLog);
+ }
+
+ return shader;
+ }
+
+ /**
+ * Creates a program from 2 shaders.
+ *
+ * @param {!WebGLRenderingContext) gl The WebGL context.
+ * @param {!WebGLShader} vertexShader A vertex shader.
+ * @param {!WebGLShader} fragmentShader A fragment shader.
+ * @return {!WebGLProgram} A program.
+ */
+ function createProgram(gl, vertexShader, fragmentShader) {
+
+ // create a program.
+ var program = gl.createProgram();
+
+ // attach the shaders.
+ gl.attachShader(program, vertexShader);
+ gl.attachShader(program, fragmentShader);
+
+ // link the program.
+ gl.linkProgram(program);
+
+ // Check if it linked.
+ var success = gl.getProgramParameter(program, gl.LINK_STATUS);
+ if (!success && !gl.isContextLost()) {
+ // something went wrong with the link
+ var infoLog = gl.getProgramInfoLog(program);
+ console.error("WebGL program filed to link:\n" + infoLog);
+ }
+
+ return program;
+ }
+
+ /**
+ * Creates a program from 2 shaders source (Strings)
+ * @param {!WebGLRenderingContext} gl The WebGL context.
+ * @param {!WebGLShader} vertexShaderSrc Vertex shader string
+ * @param {!WebGLShader} fragShaderSrc Fragment shader string
+ * @return {!WebGLProgram} A program
+ */
+ function createProgramFromString(gl, vertexShaderSrc, fragShaderSrc) {
+ var vertexShader = compileShader(gl, vertexShaderSrc, gl.VERTEX_SHADER);
+ var fragShader = compileShader(gl, fragShaderSrc, gl.FRAGMENT_SHADER);
+ return createProgram(gl, vertexShader, fragShader);
+ }
+
+ cornerstone.webGL.createProgramFromString = createProgramFromString;
+
+}(cornerstone));
+
+(function (cornerstone) {
+
+ "use strict";
+
+ if (!cornerstone.webGL) {
+ cornerstone.webGL = {};
+ }
+
+ var renderCanvas = document.createElement('canvas');
+ var renderCanvasContext;
+ var renderCanvasData;
+ var gl;
+ var programs;
+ var shader;
+ var texCoordBuffer, positionBuffer;
+ cornerstone.webGL.isWebGLInitialized = false;
+
+ function getRenderCanvas() {
+ return renderCanvas;
+ }
+
+ function initShaders() {
+ for (var id in cornerstone.webGL.shaders) {
+ console.log("WEBGL: Loading shader", id);
+ var shader = cornerstone.webGL.shaders[ id ];
+ shader.attributes = {};
+ shader.uniforms = {};
+ shader.vert = cornerstone.webGL.vertexShader;
+
+ shader.program = cornerstone.webGL.createProgramFromString(gl, shader.vert, shader.frag);
+
+ shader.attributes.texCoordLocation = gl.getAttribLocation(shader.program, "a_texCoord");
+ gl.enableVertexAttribArray(shader.attributes.texCoordLocation);
+
+ shader.attributes.positionLocation = gl.getAttribLocation(shader.program, "a_position");
+ gl.enableVertexAttribArray(shader.attributes.positionLocation);
+
+ shader.uniforms.resolutionLocation = gl.getUniformLocation(shader.program, "u_resolution");
+ }
+ }
+
+ function initRenderer() {
+ if (cornerstone.webGL.isWebGLInitialized === true) {
+ console.log("WEBGL Renderer already initialized");
+ return;
+ }
+ if ( initWebGL( renderCanvas ) ) {
+ initBuffers();
+ initShaders();
+ console.log("WEBGL Renderer initialized!");
+ cornerstone.webGL.isWebGLInitialized = true;
+ }
+ }
+
+ function updateRectangle(gl, width, height) {
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
+ width, height,
+ 0, height,
+ width, 0,
+ 0, 0]), gl.STATIC_DRAW);
+ }
+
+ function handleLostContext(event) {
+ event.preventDefault();
+ console.warn('WebGL Context Lost!');
+ }
+
+ function handleRestoredContext(event) {
+ event.preventDefault();
+ cornerstone.webGL.isWebGLInitialized = false;
+ cornerstone.webGL.textureCache.purgeCache();
+ initRenderer();
+ console.log('WebGL Context Restored.');
+ }
+
+ function initWebGL(canvas) {
+
+ gl = null;
+ try {
+ // Try to grab the standard context. If it fails, fallback to experimental.
+ var options = {
+ preserveDrawingBuffer: true, // preserve buffer so we can copy to display canvas element
+ };
+
+ // ---------------- Testing purposes -------------
+ if (cornerstone.webGL.debug === true && WebGLDebugUtils) {
+ renderCanvas = WebGLDebugUtils.makeLostContextSimulatingCanvas(renderCanvas);
+ }
+ // ---------------- Testing purposes -------------
+
+ gl = canvas.getContext("webgl", options) || canvas.getContext("experimental-webgl", options);
+
+ // Set up event listeners for context lost / context restored
+ canvas.removeEventListener("webglcontextlost", handleLostContext, false);
+ canvas.addEventListener("webglcontextlost", handleLostContext, false);
+
+ canvas.removeEventListener("webglcontextrestored", handleRestoredContext, false);
+ canvas.addEventListener("webglcontextrestored", handleRestoredContext, false);
+
+ } catch(error) {
+ throw "Error creating WebGL context";
+ }
+
+ // If we don't have a GL context, give up now
+ if (!gl) {
+ console.error("Unable to initialize WebGL. Your browser may not support it.");
+ gl = null;
+ }
+ return gl;
+ }
+
+ function getImageDataType(image) {
+ if (image.color) {
+ return 'rgb';
+ }
+
+ var datatype = 'int';
+ if (image.minPixelValue >= 0) {
+ datatype = 'u' + datatype;
+ }
+
+ if (image.maxPixelValue > 255) {
+ datatype += '16';
+ } else {
+ datatype += '8';
+ }
+ return datatype;
+ }
+
+ function getShaderProgram(image) {
+
+ var datatype = getImageDataType(image);
+ // We need a mechanism for
+ // choosing the shader based on the image datatype
+ // console.log("Datatype: " + datatype);
+ if (cornerstone.webGL.shaders.hasOwnProperty(datatype)) {
+ return cornerstone.webGL.shaders[ datatype ];
+ }
+
+ var shader = cornerstone.webGL.shaders.rgb;
+ return shader;
+ }
+
+ function getImageTexture( image ) {
+ var imageTexture = cornerstone.webGL.textureCache.getImageTexture(image.imageId);
+ if (!imageTexture) {
+ //console.log("Generating texture for imageid: ", image.imageId);
+ imageTexture = generateTexture(image);
+ cornerstone.webGL.textureCache.putImageTexture(image, imageTexture);
+ }
+ return imageTexture.texture;
+
+ }
+
+ function generateTexture( image ) {
+ var TEXTURE_FORMAT = {
+ uint8: gl.LUMINANCE,
+ int8: gl.LUMINANCE_ALPHA,
+ uint16: gl.LUMINANCE_ALPHA,
+ int16: gl.RGB,
+ rgb: gl.RGB
+ };
+
+ var TEXTURE_BYTES = {
+ int8: 1, // Luminance
+ uint16: 2, // Luminance + Alpha
+ int16: 3, // RGB
+ rgb: 3 // RGB
+ };
+
+ var imageDataType = getImageDataType(image);
+ var format = TEXTURE_FORMAT[imageDataType];
+
+ // GL texture configuration
+ var texture = gl.createTexture();
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
+
+ var imageData = cornerstone.webGL.dataUtilities[imageDataType].storedPixelDataToImageData(image, image.width, image.height);
+
+ gl.texImage2D(gl.TEXTURE_2D, 0, format, image.width, image.height, 0, format, gl.UNSIGNED_BYTE, imageData);
+
+ // Calculate the size in bytes of this image in memory
+ var sizeInBytes = image.width * image.height * TEXTURE_BYTES[imageDataType];
+ var imageTexture = {
+ texture: texture,
+ sizeInBytes: sizeInBytes
+ };
+ return imageTexture;
+
+ }
+
+ function initBuffers() {
+
+ positionBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
+ 1, 1,
+ 0, 1,
+ 1, 0,
+ 0, 0
+ ]), gl.STATIC_DRAW);
+
+
+ texCoordBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
+ 1.0, 1.0,
+ 0.0, 1.0,
+ 1.0, 0.0,
+ 0.0, 0.0,
+ ]), gl.STATIC_DRAW);
+ }
+
+ function renderQuad(shader, parameters, texture, width, height )
+ {
+ gl.clearColor(1.0,0.0,0.0,1.0);
+ gl.viewport( 0, 0, width, height );
+
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+ gl.useProgram(shader.program);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
+ gl.vertexAttribPointer(shader.attributes.texCoordLocation, 2, gl.FLOAT, false, 0, 0);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
+ gl.vertexAttribPointer(shader.attributes.positionLocation, 2, gl.FLOAT, false, 0, 0);
+
+ for (var key in parameters) {
+ var uniformLocation = gl.getUniformLocation(shader.program, key);
+ if ( !uniformLocation ) {
+ throw "Could not access location for uniform: " + key;
+ }
+
+ var uniform = parameters[key];
+
+ var type = uniform.type;
+ var value = uniform.value;
+
+ if( type == "i" ) {
+ gl.uniform1i( uniformLocation, value );
+ } else if( type == "f" ) {
+ gl.uniform1f( uniformLocation, value );
+ } else if( type == "2f" ) {
+ gl.uniform2f( uniformLocation, value[0], value[1] );
+ }
+ }
+
+ updateRectangle(gl, width, height);
+
+ gl.activeTexture(gl.TEXTURE0);
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+
+ }
+
+ function render(enabledElement) {
+
+ if (!enabledElement) {
+ throw "drawImage: enabledElement parameter must not be undefined";
+ }
+
+ var image = enabledElement.image;
+ if (!image) {
+ throw "drawImage: image must be loaded before it can be drawn";
+ }
+
+ // Resize the canvas
+ renderCanvas.width = image.width;
+ renderCanvas.height = image.height;
+
+ // Get the canvas context and reset the transform
+ var context = enabledElement.canvas.getContext('2d');
+ context.setTransform(1, 0, 0, 1, 0, 0);
+
+ // Clear the canvas
+ context.fillStyle = 'black';
+ context.fillRect(0,0, enabledElement.canvas.width, enabledElement.canvas.height);
+
+ // Turn off image smooth/interpolation if pixelReplication is set in the viewport
+ if (enabledElement.viewport.pixelReplication === true) {
+ context.imageSmoothingEnabled = false;
+ context.mozImageSmoothingEnabled = false; // firefox doesn't support imageSmoothingEnabled yet
+ } else {
+ context.imageSmoothingEnabled = true;
+ context.mozImageSmoothingEnabled = true;
+ }
+
+ var viewport = enabledElement.viewport;
+
+ // Render the current image
+ var shader = getShaderProgram(image);
+ var texture = getImageTexture(image);
+ var parameters = {
+ "u_resolution": { type: "2f", value: [image.width, image.height] },
+ "wc": { type: "f", value: enabledElement.viewport.voi.windowCenter },
+ "ww": { type: "f", value: enabledElement.viewport.voi.windowWidth },
+ "slope": { type: "f", value: image.slope },
+ "intercept": { type: "f", value: image.intercept },
+ //"minPixelValue": { type: "f", value: image.minPixelValue },
+ "invert": { type: "i", value: enabledElement.viewport.invert ? 1 : 0 },
+ };
+ renderQuad(shader, parameters, texture, image.width, image.height );
+
+ // Save the canvas context state and apply the viewport properties
+ cornerstone.setToPixelCoordinateSystem(enabledElement, context);
+
+ // Copy pixels from the offscreen canvas to the onscreen canvas
+ context.drawImage(renderCanvas, 0,0, image.width, image.height, 0, 0, image.width, image.height);
+
+ }
+
+ function isWebGLAvailable() {
+ // Adapted from
+ // http://stackoverflow.com/questions/9899807/three-js-detect-webgl-support-and-fallback-to-regular-canvas
+
+ var options = {
+ failIfMajorPerformanceCaveat: true
+ };
+
+ try {
+ var canvas = document.createElement("canvas");
+ return !!
+ window.WebGLRenderingContext &&
+ (canvas.getContext("webgl", options) || canvas.getContext("experimental-webgl", options));
+ } catch(e) {
+ return false;
+ }
+ }
+
+ cornerstone.webGL.renderer = {
+ render: render,
+ initRenderer: initRenderer,
+ getRenderCanvas: getRenderCanvas,
+ isWebGLAvailable: isWebGLAvailable
+ };
+
+}(cornerstone));
+
+
+(function (cornerstone) {
+
+ "use strict";
+
+ if (!cornerstone.webGL) {
+ cornerstone.webGL = {};
+ }
+
+ if (!cornerstone.webGL.shaders) {
+ cornerstone.webGL.shaders = {};
+ }
+
+ if (!cornerstone.webGL.dataUtilities) {
+ cornerstone.webGL.dataUtilities = {};
+ }
+
+ // Pack int16 into three uint8 channels (r, g, b)
+ var shader = {};
+
+ function storedPixelDataToImageData(image) {
+
+ // Transfer image data to alpha and luminance channels of WebGL texture
+ // Credit to @jpambrun and @fernandojsg
+
+ // Pack int16 into three uint8 channels (r, g, b)
+ var pixelData = image.getPixelData();
+ var numberOfChannels = 3;
+ var data = new Uint8Array(image.width * image.height * numberOfChannels);
+ var offset = 0;
+
+ for (var i = 0; i < pixelData.length; i++) {
+ var val = Math.abs(pixelData[i]);
+ data[offset++] = parseInt(val & 0xFF, 10);
+ data[offset++] = parseInt(val >> 8, 10);
+ data[offset++] = pixelData[i] < 0 ? 0: 1; // 0 For negative, 1 for positive
+ }
+ return data;
+ }
+
+ cornerstone.webGL.dataUtilities.int16 = {
+ storedPixelDataToImageData: storedPixelDataToImageData
+ };
+
+ shader.frag = 'precision mediump float;' +
+ 'uniform sampler2D u_image;' +
+ 'uniform float ww;' +
+ 'uniform float wc;' +
+ 'uniform float slope;' +
+ 'uniform float intercept;' +
+ 'uniform int invert;' +
+ 'varying vec2 v_texCoord;' +
+
+ 'void main() {' +
+ // Get texture
+ 'vec4 color = texture2D(u_image, v_texCoord);' +
+
+ // Calculate luminance from packed texture
+ 'float intensity = color.r*256.0 + color.g*65536.0;'+
+
+ 'if (color.b == 0.0)' +
+ 'intensity = -intensity;' +
+
+ // Rescale based on slope and window settings
+ 'intensity = intensity * slope + intercept;'+
+ 'float center0 = wc - 0.5;'+
+ 'float width0 = max(ww, 1.0);' +
+ 'intensity = (intensity - center0) / width0 + 0.5;'+
+
+ // Clamp intensity
+ 'intensity = clamp(intensity, 0.0, 1.0);' +
+
+ // RGBA output
+ 'gl_FragColor = vec4(intensity, intensity, intensity, 1.0);' +
+
+ // Apply any inversion necessary
+ 'if (invert == 1)' +
+ 'gl_FragColor.rgb = 1.0 - gl_FragColor.rgb;' +
+ '}';
+
+ cornerstone.webGL.shaders.int16 = shader;
+
+}(cornerstone));
+(function (cornerstone) {
+
+ "use strict";
+
+ if (!cornerstone.webGL) {
+ cornerstone.webGL = {};
+ }
+
+ if (!cornerstone.webGL.shaders) {
+ cornerstone.webGL.shaders = {};
+ }
+
+ if (!cornerstone.webGL.dataUtilities) {
+ cornerstone.webGL.dataUtilities = {};
+ }
+
+ var shader = {};
+
+ function storedPixelDataToImageData(image) {
+ // Transfer image data to alpha channel of WebGL texture
+ // Store data in Uint8Array
+ var pixelData = image.getPixelData();
+ var numberOfChannels = 2;
+ var data = new Uint8Array(image.width * image.height * numberOfChannels);
+ var offset = 0;
+
+ for (var i = 0; i < pixelData.length; i++) {
+ data[offset++] = parseInt(pixelData[i], 10);
+ data[offset++] = pixelData[i] < 0 ? 0: 1; // 0 For negative, 1 for positive
+ }
+ return data;
+ }
+
+ cornerstone.webGL.dataUtilities.int8 = {
+ storedPixelDataToImageData: storedPixelDataToImageData
+ };
+
+ shader.frag = 'precision mediump float;' +
+ 'uniform sampler2D u_image;' +
+ 'uniform float ww;' +
+ 'uniform float wc;' +
+ 'uniform float slope;' +
+ 'uniform float intercept;' +
+ 'uniform float minPixelValue;' +
+ 'uniform int invert;' +
+ 'varying vec2 v_texCoord;' +
+
+ 'void main() {' +
+ // Get texture
+ 'vec4 color = texture2D(u_image, v_texCoord);' +
+
+ // Calculate luminance from packed texture
+ 'float intensity = color.r*256.;'+
+
+ 'if (color.a == 0.0)' +
+ 'intensity = -intensity;' +
+
+ // Rescale based on slope and window settings
+ 'intensity = intensity * slope + intercept;'+
+ 'float center0 = wc - 0.5;'+
+ 'float width0 = max(ww, 1.0);' +
+ 'intensity = (intensity - center0) / width0 + 0.5;'+
+
+ // Clamp intensity
+ 'intensity = clamp(intensity, 0.0, 1.0);' +
+
+ // RGBA output
+ 'gl_FragColor = vec4(intensity, intensity, intensity, 1.0);' +
+
+ // Apply any inversion necessary
+ 'if (invert == 1)' +
+ 'gl_FragColor.rgb = 1.0 - gl_FragColor.rgb;' +
+ '}';
+
+ cornerstone.webGL.shaders.int8 = shader;
+
+
+}(cornerstone));
+(function (cornerstone) {
+
+ "use strict";
+
+ if (!cornerstone.webGL) {
+ cornerstone.webGL = {};
+ }
+
+ if (!cornerstone.webGL.shaders) {
+ cornerstone.webGL.shaders = {};
+ }
+
+ if (!cornerstone.webGL.dataUtilities) {
+ cornerstone.webGL.dataUtilities = {};
+ }
+
+ // Pack RGB images into a 3-channel RGB texture
+ var shader = {};
+
+ function storedPixelDataToImageData(image) {
+ var minPixelValue = image.minPixelValue;
+ var canvasImageDataIndex = 0;
+ var storedPixelDataIndex = 0;
+ // Only 3 channels, since we use WebGL's RGB texture format
+ var numStoredPixels = image.width * image.height * 4;
+ var numOutputPixels = image.width * image.height * 3;
+ var storedPixelData = image.getPixelData();
+ var data = new Uint8Array(numOutputPixels);
+
+ // NOTE: As of Nov 2014, most javascript engines have lower performance when indexing negative indexes.
+ // We have a special code path for this case that improves performance. Thanks to @jpambrun for this enhancement
+ if (minPixelValue < 0){
+ while (storedPixelDataIndex < numStoredPixels) {
+ data[canvasImageDataIndex++] = storedPixelData[storedPixelDataIndex++] + (-minPixelValue); // red
+ data[canvasImageDataIndex++] = storedPixelData[storedPixelDataIndex++] + (-minPixelValue); // green
+ data[canvasImageDataIndex++] = storedPixelData[storedPixelDataIndex++] + (-minPixelValue); // blue
+ storedPixelDataIndex += 1; // The stored pixel data has 4 channels
+ }
+ } else {
+ while (storedPixelDataIndex < numStoredPixels) {
+ data[canvasImageDataIndex++] = storedPixelData[storedPixelDataIndex++]; // red
+ data[canvasImageDataIndex++] = storedPixelData[storedPixelDataIndex++]; // green
+ data[canvasImageDataIndex++] = storedPixelData[storedPixelDataIndex++]; // blue
+ storedPixelDataIndex += 1; // The stored pixel data has 4 channels
+ }
+ }
+ return data;
+ }
+
+ cornerstone.webGL.dataUtilities.rgb = {
+ storedPixelDataToImageData: storedPixelDataToImageData
+ };
+
+ shader.frag = 'precision mediump float;' +
+ 'uniform sampler2D u_image;' +
+ 'uniform float ww;' +
+ 'uniform float wc;' +
+ 'uniform float slope;' +
+ 'uniform float intercept;' +
+ 'uniform float minPixelValue;' +
+ 'uniform int invert;' +
+ 'varying vec2 v_texCoord;' +
+
+ 'void main() {' +
+
+ // Get texture
+ 'vec3 color = texture2D(u_image, v_texCoord).xyz;' +
+
+ // Rescale based on slope and intercept
+ 'color = color * 256.0 * slope + intercept;' +
+
+ // Apply window settings
+ 'float center0 = wc - 0.5 - minPixelValue;'+
+ 'float width0 = ww - 1.0;'+
+ 'color = (color - center0) / width0 + 0.5;'+
+
+ // RGBA output
+ 'gl_FragColor = vec4(color, 1);' +
+
+ // Apply any inversion necessary
+ 'if (invert == 1)' +
+ 'gl_FragColor.rgb = 1. - gl_FragColor.rgb;' +
+ '}';
+
+ cornerstone.webGL.shaders.rgb = shader;
+
+}(cornerstone));
+(function (cornerstone) {
+
+ "use strict";
+
+ if (!cornerstone.webGL) {
+ cornerstone.webGL = {};
+ }
+
+ if (!cornerstone.webGL.shaders) {
+ cornerstone.webGL.shaders = {};
+ }
+
+ if (!cornerstone.webGL.dataUtilities) {
+ cornerstone.webGL.dataUtilities = {};
+ }
+
+ // For uint16 pack uint16 into two uint8 channels (r and a)
+ var shader = {};
+
+ function storedPixelDataToImageData(image) {
+
+ // Transfer image data to alpha and luminance channels of WebGL texture
+ // Credit to @jpambrun and @fernandojsg
+
+ // Pack uint16 into two uint8 channels (r and a)
+ var pixelData = image.getPixelData();
+ var numberOfChannels = 2;
+ var data = new Uint8Array(image.width * image.height * numberOfChannels);
+ var offset = 0;
+
+ for (var i = 0; i < pixelData.length; i++) {
+ var val = pixelData[i];
+ data[offset++] = parseInt(val & 0xFF, 10);
+ data[offset++] = parseInt(val >> 8, 10);
+ }
+ return data;
+ }
+
+ cornerstone.webGL.dataUtilities.uint16 = {
+ storedPixelDataToImageData: storedPixelDataToImageData
+ };
+
+ shader.frag = 'precision mediump float;' +
+ 'uniform sampler2D u_image;' +
+ 'uniform float ww;' +
+ 'uniform float wc;' +
+ 'uniform float slope;' +
+ 'uniform float intercept;' +
+ 'uniform int invert;' +
+ 'varying vec2 v_texCoord;' +
+
+ 'void main() {' +
+ // Get texture
+ 'vec4 color = texture2D(u_image, v_texCoord);' +
+
+ // Calculate luminance from packed texture
+ 'float intensity = color.r*256.0 + color.a*65536.0;'+
+
+ // Rescale based on slope and window settings
+ 'intensity = intensity * slope + intercept;'+
+ 'float center0 = wc - 0.5;'+
+ 'float width0 = ww - 1.0;'+
+ 'intensity = (intensity - center0) / width0 + 0.5;'+
+
+ // Clamp intensity
+ 'intensity = clamp(intensity, 0.0, 1.0);' +
+
+ // RGBA output
+ 'gl_FragColor = vec4(intensity, intensity, intensity, 1.0);' +
+
+ // Apply any inversion necessary
+ 'if (invert == 1)' +
+ 'gl_FragColor.rgb = 1.0 - gl_FragColor.rgb;' +
+ '}';
+
+ cornerstone.webGL.shaders.uint16 = shader;
+
+}(cornerstone));
+(function (cornerstone) {
+
+ "use strict";
+
+ if (!cornerstone.webGL) {
+ cornerstone.webGL = {};
+ }
+
+ if (!cornerstone.webGL.shaders) {
+ cornerstone.webGL.shaders = {};
+ }
+
+ if (!cornerstone.webGL.dataUtilities) {
+ cornerstone.webGL.dataUtilities = {};
+ }
+
+ var shader = {};
+
+ function storedPixelDataToImageData(image) {
+ // Transfer image data to alpha channel of WebGL texture
+ // Store data in Uuint8Array
+ var pixelData = image.getPixelData();
+ var data = new Uint8Array(pixelData.length);
+ for (var i = 0; i < pixelData.length; i++) {
+ data[i] = parseInt(pixelData[i], 10);
+ }
+ return data;
+ }
+
+ cornerstone.webGL.dataUtilities.uint8 = {
+ storedPixelDataToImageData: storedPixelDataToImageData
+ };
+
+ shader.frag = 'precision mediump float;' +
+ 'uniform sampler2D u_image;' +
+ 'uniform float ww;' +
+ 'uniform float wc;' +
+ 'uniform float slope;' +
+ 'uniform float intercept;' +
+ //'uniform float minPixelValue;' +
+ 'uniform int invert;' +
+ 'varying vec2 v_texCoord;' +
+
+ 'void main() {' +
+ // Get texture
+ 'vec4 color = texture2D(u_image, v_texCoord);' +
+
+ // Calculate luminance from packed texture
+ 'float intensity = color.r*256.0;'+
+
+ // Rescale based on slope and window settings
+ 'intensity = intensity * slope + intercept;'+
+ 'float center0 = wc - 0.5;'+
+ 'float width0 = max(ww, 1.0);' +
+ 'intensity = (intensity - center0) / width0 + 0.5;'+
+
+ // Clamp intensity
+ 'intensity = clamp(intensity, 0.0, 1.0);' +
+
+ // RGBA output
+ 'gl_FragColor = vec4(intensity, intensity, intensity, 1.0);' +
+
+ // Apply any inversion necessary
+ 'if (invert == 1)' +
+ 'gl_FragColor.rgb = 1.0 - gl_FragColor.rgb;' +
+ '}';
+
+ cornerstone.webGL.shaders.uint8 = shader;
+
+
+}(cornerstone));
+/**
+ * This module deals with caching image textures in VRAM for WebGL
+ */
+
+(function (cornerstone) {
+
+ "use strict";
+
+ var imageCache = {};
+
+ var cachedImages = [];
+
+ var maximumSizeInBytes = 1024 * 1024 * 256; // 256 MB
+ var cacheSizeInBytes = 0;
+
+ function setMaximumSizeBytes(numBytes) {
+ if (numBytes === undefined) {
+ throw "setMaximumSizeBytes: parameter numBytes must not be undefined";
+ }
+ if (numBytes.toFixed === undefined) {
+ throw "setMaximumSizeBytes: parameter numBytes must be a number";
+ }
+
+ maximumSizeInBytes = numBytes;
+ purgeCacheIfNecessary();
+ }
+
+ function purgeCacheIfNecessary() {
+ // if max cache size has not been exceeded, do nothing
+ if (cacheSizeInBytes <= maximumSizeInBytes) {
+ return;
+ }
+
+ // cache size has been exceeded, create list of images sorted by timeStamp
+ // so we can purge the least recently used image
+ function compare(a,b) {
+ if (a.timeStamp > b.timeStamp) {
+ return -1;
+ }
+ if (a.timeStamp < b.timeStamp) {
+ return 1;
+ }
+ return 0;
+ }
+ cachedImages.sort(compare);
+
+ // remove images as necessary
+ while(cacheSizeInBytes > maximumSizeInBytes) {
+ var lastCachedImage = cachedImages[cachedImages.length - 1];
+ cacheSizeInBytes -= lastCachedImage.sizeInBytes;
+ delete imageCache[lastCachedImage.imageId];
+ cachedImages.pop();
+ $(cornerstone).trigger('CornerstoneWebGLTextureRemoved', {imageId: lastCachedImage.imageId});
+ }
+
+ var cacheInfo = cornerstone.imageCache.getCacheInfo();
+ console.log('CornerstoneWebGLTextureCacheFull');
+ $(cornerstone).trigger('CornerstoneWebGLTextureCacheFull', cacheInfo);
+ }
+
+ function putImageTexture(image, imageTexture) {
+ var imageId = image.imageId;
+ if (image === undefined) {
+ throw "putImageTexture: image must not be undefined";
+ }
+
+ if (imageId === undefined) {
+ throw "putImageTexture: imageId must not be undefined";
+ }
+
+ if (imageTexture === undefined) {
+ throw "putImageTexture: imageTexture must not be undefined";
+ }
+
+ if (imageCache.hasOwnProperty(imageId) === true) {
+ throw "putImageTexture: imageId already in cache";
+ }
+
+ var cachedImage = {
+ imageId : imageId,
+ imageTexture : imageTexture,
+ timeStamp : new Date(),
+ sizeInBytes: imageTexture.sizeInBytes
+ };
+
+ imageCache[imageId] = cachedImage;
+ cachedImages.push(cachedImage);
+
+ if (imageTexture.sizeInBytes === undefined) {
+ throw "putImageTexture: imageTexture does not have sizeInBytes property or";
+ }
+ if (imageTexture.sizeInBytes.toFixed === undefined) {
+ throw "putImageTexture: imageTexture.sizeInBytes is not a number";
+ }
+ cacheSizeInBytes += cachedImage.sizeInBytes;
+ purgeCacheIfNecessary();
+ }
+
+ function getImageTexture(imageId) {
+ if (imageId === undefined) {
+ throw "getImageTexture: imageId must not be undefined";
+ }
+ var cachedImage = imageCache[imageId];
+ if (cachedImage === undefined) {
+ return undefined;
+ }
+
+ // bump time stamp for cached image
+ cachedImage.timeStamp = new Date();
+ return cachedImage.imageTexture;
+ }
+
+ function removeImageTexture(imageId) {
+ if (imageId === undefined) {
+ throw "removeImageTexture: imageId must not be undefined";
+ }
+ var cachedImage = imageCache[imageId];
+ if (cachedImage === undefined) {
+ throw "removeImageTexture: imageId must not be undefined";
+ }
+ cachedImages.splice( cachedImages.indexOf(cachedImage), 1);
+ cacheSizeInBytes -= cachedImage.sizeInBytes;
+ delete imageCache[imageId];
+
+ return cachedImage.imageTexture;
+ }
+
+ function getCacheInfo() {
+ return {
+ maximumSizeInBytes : maximumSizeInBytes,
+ cacheSizeInBytes : cacheSizeInBytes,
+ numberOfImagesCached: cachedImages.length
+ };
+ }
+
+ function purgeCache() {
+ while (cachedImages.length > 0) {
+ var removedCachedImage = cachedImages.pop();
+ delete imageCache[removedCachedImage.imageId];
+ }
+ cacheSizeInBytes = 0;
+ }
+
+ // module exports
+ cornerstone.webGL.textureCache = {
+ putImageTexture : putImageTexture,
+ getImageTexture: getImageTexture,
+ removeImageTexture: removeImageTexture,
+ setMaximumSizeBytes: setMaximumSizeBytes,
+ getCacheInfo : getCacheInfo,
+ purgeCache: purgeCache,
+ cachedImages: cachedImages
+ };
+
+}(cornerstone));
+
+(function (cornerstone) {
+
+ "use strict";
+
+ if (!cornerstone.webGL) {
+ cornerstone.webGL = {};
+ }
+
+ cornerstone.webGL.vertexShader = 'attribute vec2 a_position;' +
+ 'attribute vec2 a_texCoord;' +
+ 'uniform vec2 u_resolution;' +
+ 'varying vec2 v_texCoord;' +
+ 'void main() {' +
+ 'vec2 zeroToOne = a_position / u_resolution;' +
+ 'vec2 zeroToTwo = zeroToOne * 2.0;' +
+ 'vec2 clipSpace = zeroToTwo - 1.0;' +
+ 'gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);' +
+ 'v_texCoord = a_texCoord;' +
+ '}';
+
+}(cornerstone));
\ No newline at end of file
diff --git a/Packages/cornerstone/client/cornerstoneMath.js b/Packages/cornerstone/client/cornerstoneMath.js
new file mode 100644
index 000000000..ff087e519
--- /dev/null
+++ b/Packages/cornerstone/client/cornerstoneMath.js
@@ -0,0 +1,1779 @@
+/*! cornerstoneMath - v0.1.2 - 2015-08-31 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneMath */
+// Begin Source: src/vector3.js
+// Based on THREE.JS
+
+var cornerstoneMath = (function (cornerstoneMath) {
+
+ "use strict";
+
+ if(cornerstoneMath === undefined) {
+ cornerstoneMath = {};
+ }
+
+ cornerstoneMath.Vector3 = function ( x, y, z ) {
+
+ this.x = x || 0;
+ this.y = y || 0;
+ this.z = z || 0;
+
+ };
+
+ cornerstoneMath.Vector3.prototype = {
+
+ constructor: cornerstoneMath.Vector3,
+
+ set: function ( x, y, z ) {
+
+ this.x = x;
+ this.y = y;
+ this.z = z;
+
+ return this;
+
+ },
+
+ setX: function ( x ) {
+
+ this.x = x;
+
+ return this;
+
+ },
+
+ setY: function ( y ) {
+
+ this.y = y;
+
+ return this;
+
+ },
+
+ setZ: function ( z ) {
+
+ this.z = z;
+
+ return this;
+
+ },
+
+ setComponent: function ( index, value ) {
+
+ switch ( index ) {
+
+ case 0: this.x = value; break;
+ case 1: this.y = value; break;
+ case 2: this.z = value; break;
+ default: throw new Error( "index is out of range: " + index );
+
+ }
+
+ },
+
+ getComponent: function ( index ) {
+
+ switch ( index ) {
+
+ case 0: return this.x;
+ case 1: return this.y;
+ case 2: return this.z;
+ default: throw new Error( "index is out of range: " + index );
+
+ }
+
+ },
+
+ copy: function ( v ) {
+
+ this.x = v.x;
+ this.y = v.y;
+ this.z = v.z;
+
+ return this;
+
+ },
+
+ add: function ( v, w ) {
+
+ if ( w !== undefined ) {
+
+ console.warn( 'DEPRECATED: Vector3\'s .add() now only accepts one argument. Use .addVectors( a, b ) instead.' );
+ return this.addVectors( v, w );
+
+ }
+
+ this.x += v.x;
+ this.y += v.y;
+ this.z += v.z;
+
+ return this;
+
+ },
+
+ addScalar: function ( s ) {
+
+ this.x += s;
+ this.y += s;
+ this.z += s;
+
+ return this;
+
+ },
+
+ addVectors: function ( a, b ) {
+
+ this.x = a.x + b.x;
+ this.y = a.y + b.y;
+ this.z = a.z + b.z;
+
+ return this;
+
+ },
+
+ sub: function ( v, w ) {
+
+ if ( w !== undefined ) {
+
+ console.warn( 'DEPRECATED: Vector3\'s .sub() now only accepts one argument. Use .subVectors( a, b ) instead.' );
+ return this.subVectors( v, w );
+
+ }
+
+ this.x -= v.x;
+ this.y -= v.y;
+ this.z -= v.z;
+
+ return this;
+
+ },
+
+ subVectors: function ( a, b ) {
+
+ this.x = a.x - b.x;
+ this.y = a.y - b.y;
+ this.z = a.z - b.z;
+
+ return this;
+
+ },
+
+ multiply: function ( v, w ) {
+
+ if ( w !== undefined ) {
+
+ console.warn( 'DEPRECATED: Vector3\'s .multiply() now only accepts one argument. Use .multiplyVectors( a, b ) instead.' );
+ return this.multiplyVectors( v, w );
+
+ }
+
+ this.x *= v.x;
+ this.y *= v.y;
+ this.z *= v.z;
+
+ return this;
+
+ },
+
+ multiplyScalar: function ( scalar ) {
+
+ this.x *= scalar;
+ this.y *= scalar;
+ this.z *= scalar;
+
+ return this;
+
+ },
+
+ multiplyVectors: function ( a, b ) {
+
+ this.x = a.x * b.x;
+ this.y = a.y * b.y;
+ this.z = a.z * b.z;
+
+ return this;
+
+ },
+
+
+ applyAxisAngle: function () {
+
+ var quaternion;
+
+ return function ( axis, angle ) {
+
+ if ( quaternion === undefined ) quaternion = new cornerstoneMath.Quaternion();
+
+ this.applyQuaternion( quaternion.setFromAxisAngle( axis, angle ) );
+
+ return this;
+
+ };
+
+ }(),
+
+ applyMatrix3: function ( m ) {
+
+ var x = this.x;
+ var y = this.y;
+ var z = this.z;
+
+ var e = m.elements;
+
+ this.x = e[0] * x + e[3] * y + e[6] * z;
+ this.y = e[1] * x + e[4] * y + e[7] * z;
+ this.z = e[2] * x + e[5] * y + e[8] * z;
+
+ return this;
+
+ },
+
+ applyMatrix4: function ( m ) {
+
+ // input: THREE.Matrix4 affine matrix
+
+ var x = this.x, y = this.y, z = this.z;
+
+ var e = m.elements;
+
+ this.x = e[0] * x + e[4] * y + e[8] * z + e[12];
+ this.y = e[1] * x + e[5] * y + e[9] * z + e[13];
+ this.z = e[2] * x + e[6] * y + e[10] * z + e[14];
+
+ return this;
+
+ },
+
+ applyProjection: function ( m ) {
+
+ // input: THREE.Matrix4 projection matrix
+
+ var x = this.x, y = this.y, z = this.z;
+
+ var e = m.elements;
+ var d = 1 / ( e[3] * x + e[7] * y + e[11] * z + e[15] ); // perspective divide
+
+ this.x = ( e[0] * x + e[4] * y + e[8] * z + e[12] ) * d;
+ this.y = ( e[1] * x + e[5] * y + e[9] * z + e[13] ) * d;
+ this.z = ( e[2] * x + e[6] * y + e[10] * z + e[14] ) * d;
+
+ return this;
+
+ },
+
+ applyQuaternion: function ( q ) {
+
+ var x = this.x;
+ var y = this.y;
+ var z = this.z;
+
+ var qx = q.x;
+ var qy = q.y;
+ var qz = q.z;
+ var qw = q.w;
+
+ // calculate quat * vector
+
+ var ix = qw * x + qy * z - qz * y;
+ var iy = qw * y + qz * x - qx * z;
+ var iz = qw * z + qx * y - qy * x;
+ var iw = -qx * x - qy * y - qz * z;
+
+ // calculate result * inverse quat
+
+ this.x = ix * qw + iw * -qx + iy * -qz - iz * -qy;
+ this.y = iy * qw + iw * -qy + iz * -qx - ix * -qz;
+ this.z = iz * qw + iw * -qz + ix * -qy - iy * -qx;
+
+ return this;
+
+ },
+
+ transformDirection: function ( m ) {
+
+ // input: THREE.Matrix4 affine matrix
+ // vector interpreted as a direction
+
+ var x = this.x, y = this.y, z = this.z;
+
+ var e = m.elements;
+
+ this.x = e[0] * x + e[4] * y + e[8] * z;
+ this.y = e[1] * x + e[5] * y + e[9] * z;
+ this.z = e[2] * x + e[6] * y + e[10] * z;
+
+ this.normalize();
+
+ return this;
+
+ },
+
+ divide: function ( v ) {
+
+ this.x /= v.x;
+ this.y /= v.y;
+ this.z /= v.z;
+
+ return this;
+
+ },
+
+ divideScalar: function ( scalar ) {
+
+ if ( scalar !== 0 ) {
+
+ var invScalar = 1 / scalar;
+
+ this.x *= invScalar;
+ this.y *= invScalar;
+ this.z *= invScalar;
+
+ } else {
+
+ this.x = 0;
+ this.y = 0;
+ this.z = 0;
+
+ }
+
+ return this;
+
+ },
+
+ min: function ( v ) {
+
+ if ( this.x > v.x ) {
+
+ this.x = v.x;
+
+ }
+
+ if ( this.y > v.y ) {
+
+ this.y = v.y;
+
+ }
+
+ if ( this.z > v.z ) {
+
+ this.z = v.z;
+
+ }
+
+ return this;
+
+ },
+
+ max: function ( v ) {
+
+ if ( this.x < v.x ) {
+
+ this.x = v.x;
+
+ }
+
+ if ( this.y < v.y ) {
+
+ this.y = v.y;
+
+ }
+
+ if ( this.z < v.z ) {
+
+ this.z = v.z;
+
+ }
+
+ return this;
+
+ },
+
+ clamp: function ( min, max ) {
+
+ // This function assumes min < max, if this assumption isn't true it will not operate correctly
+
+ if ( this.x < min.x ) {
+
+ this.x = min.x;
+
+ } else if ( this.x > max.x ) {
+
+ this.x = max.x;
+
+ }
+
+ if ( this.y < min.y ) {
+
+ this.y = min.y;
+
+ } else if ( this.y > max.y ) {
+
+ this.y = max.y;
+
+ }
+
+ if ( this.z < min.z ) {
+
+ this.z = min.z;
+
+ } else if ( this.z > max.z ) {
+
+ this.z = max.z;
+
+ }
+
+ return this;
+
+ },
+
+ clampScalar: ( function () {
+
+ var min, max;
+
+ return function ( minVal, maxVal ) {
+
+ if ( min === undefined ) {
+
+ min = new cornerstoneMath.Vector3();
+ max = new cornerstoneMath.Vector3();
+
+ }
+
+ min.set( minVal, minVal, minVal );
+ max.set( maxVal, maxVal, maxVal );
+
+ return this.clamp( min, max );
+
+ };
+
+ } )(),
+
+ floor: function () {
+
+ this.x = Math.floor( this.x );
+ this.y = Math.floor( this.y );
+ this.z = Math.floor( this.z );
+
+ return this;
+
+ },
+
+ ceil: function () {
+
+ this.x = Math.ceil( this.x );
+ this.y = Math.ceil( this.y );
+ this.z = Math.ceil( this.z );
+
+ return this;
+
+ },
+
+ round: function () {
+
+ this.x = Math.round( this.x );
+ this.y = Math.round( this.y );
+ this.z = Math.round( this.z );
+
+ return this;
+
+ },
+
+ roundToZero: function () {
+
+ this.x = ( this.x < 0 ) ? Math.ceil( this.x ) : Math.floor( this.x );
+ this.y = ( this.y < 0 ) ? Math.ceil( this.y ) : Math.floor( this.y );
+ this.z = ( this.z < 0 ) ? Math.ceil( this.z ) : Math.floor( this.z );
+
+ return this;
+
+ },
+
+ negate: function () {
+
+ return this.multiplyScalar( - 1 );
+
+ },
+
+ dot: function ( v ) {
+
+ return this.x * v.x + this.y * v.y + this.z * v.z;
+
+ },
+
+ lengthSq: function () {
+
+ return this.x * this.x + this.y * this.y + this.z * this.z;
+
+ },
+
+ length: function () {
+
+ return Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z );
+
+ },
+
+ lengthManhattan: function () {
+
+ return Math.abs( this.x ) + Math.abs( this.y ) + Math.abs( this.z );
+
+ },
+
+ normalize: function () {
+
+ return this.divideScalar( this.length() );
+
+ },
+
+ setLength: function ( l ) {
+
+ var oldLength = this.length();
+
+ if ( oldLength !== 0 && l !== oldLength ) {
+
+ this.multiplyScalar( l / oldLength );
+ }
+
+ return this;
+
+ },
+
+ lerp: function ( v, alpha ) {
+
+ this.x += ( v.x - this.x ) * alpha;
+ this.y += ( v.y - this.y ) * alpha;
+ this.z += ( v.z - this.z ) * alpha;
+
+ return this;
+
+ },
+
+ cross: function ( v, w ) {
+
+ if ( w !== undefined ) {
+
+ console.warn( 'DEPRECATED: Vector3\'s .cross() now only accepts one argument. Use .crossVectors( a, b ) instead.' );
+ return this.crossVectors( v, w );
+
+ }
+
+ var x = this.x, y = this.y, z = this.z;
+
+ this.x = y * v.z - z * v.y;
+ this.y = z * v.x - x * v.z;
+ this.z = x * v.y - y * v.x;
+
+ return this;
+
+ },
+
+ crossVectors: function ( a, b ) {
+
+ var ax = a.x, ay = a.y, az = a.z;
+ var bx = b.x, by = b.y, bz = b.z;
+
+ this.x = ay * bz - az * by;
+ this.y = az * bx - ax * bz;
+ this.z = ax * by - ay * bx;
+
+ return this;
+
+ },
+
+ projectOnVector: function () {
+
+ var v1, dot;
+
+ return function ( vector ) {
+
+ if ( v1 === undefined ) v1 = new cornerstoneMath.Vector3();
+
+ v1.copy( vector ).normalize();
+
+ dot = this.dot( v1 );
+
+ return this.copy( v1 ).multiplyScalar( dot );
+
+ };
+
+ }(),
+
+ projectOnPlane: function () {
+
+ var v1;
+
+ return function ( planeNormal ) {
+
+ if ( v1 === undefined ) v1 = new cornerstoneMath.Vector3();
+
+ v1.copy( this ).projectOnVector( planeNormal );
+
+ return this.sub( v1 );
+
+ };
+
+ }(),
+
+ reflect: function () {
+
+ // reflect incident vector off plane orthogonal to normal
+ // normal is assumed to have unit length
+
+ var v1;
+
+ return function ( normal ) {
+
+ if ( v1 === undefined ) v1 = new cornerstoneMath.Vector3();
+
+ return this.sub( v1.copy( normal ).multiplyScalar( 2 * this.dot( normal ) ) );
+
+ };
+
+ }(),
+
+ angleTo: function ( v ) {
+
+ var theta = this.dot( v ) / ( this.length() * v.length() );
+
+ // clamp, to handle numerical problems
+
+ return Math.acos( cornerstoneMath.clamp( theta, -1, 1 ) );
+
+ },
+
+ distanceTo: function ( v ) {
+
+ return Math.sqrt( this.distanceToSquared( v ) );
+
+ },
+
+ distanceToSquared: function ( v ) {
+
+ var dx = this.x - v.x;
+ var dy = this.y - v.y;
+ var dz = this.z - v.z;
+
+ return dx * dx + dy * dy + dz * dz;
+
+ },
+
+ setEulerFromRotationMatrix: function ( m, order ) {
+
+ console.error( "REMOVED: Vector3\'s setEulerFromRotationMatrix has been removed in favor of Euler.setFromRotationMatrix(), please update your code.");
+
+ },
+
+ setEulerFromQuaternion: function ( q, order ) {
+
+ console.error( "REMOVED: Vector3\'s setEulerFromQuaternion: has been removed in favor of Euler.setFromQuaternion(), please update your code.");
+
+ },
+
+ getPositionFromMatrix: function ( m ) {
+
+ console.warn( "DEPRECATED: Vector3\'s .getPositionFromMatrix() has been renamed to .setFromMatrixPosition(). Please update your code." );
+
+ return this.setFromMatrixPosition( m );
+
+ },
+
+ getScaleFromMatrix: function ( m ) {
+
+ console.warn( "DEPRECATED: Vector3\'s .getScaleFromMatrix() has been renamed to .setFromMatrixScale(). Please update your code." );
+
+ return this.setFromMatrixScale( m );
+ },
+
+ getColumnFromMatrix: function ( index, matrix ) {
+
+ console.warn( "DEPRECATED: Vector3\'s .getColumnFromMatrix() has been renamed to .setFromMatrixColumn(). Please update your code." );
+
+ return this.setFromMatrixColumn( index, matrix );
+
+ },
+
+ setFromMatrixPosition: function ( m ) {
+
+ this.x = m.elements[ 12 ];
+ this.y = m.elements[ 13 ];
+ this.z = m.elements[ 14 ];
+
+ return this;
+
+ },
+
+ setFromMatrixScale: function ( m ) {
+
+ var sx = this.set( m.elements[ 0 ], m.elements[ 1 ], m.elements[ 2 ] ).length();
+ var sy = this.set( m.elements[ 4 ], m.elements[ 5 ], m.elements[ 6 ] ).length();
+ var sz = this.set( m.elements[ 8 ], m.elements[ 9 ], m.elements[ 10 ] ).length();
+
+ this.x = sx;
+ this.y = sy;
+ this.z = sz;
+
+ return this;
+ },
+
+ setFromMatrixColumn: function ( index, matrix ) {
+
+ var offset = index * 4;
+
+ var me = matrix.elements;
+
+ this.x = me[ offset ];
+ this.y = me[ offset + 1 ];
+ this.z = me[ offset + 2 ];
+
+ return this;
+
+ },
+
+ equals: function ( v ) {
+
+ return ( ( v.x === this.x ) && ( v.y === this.y ) && ( v.z === this.z ) );
+
+ },
+
+ fromArray: function ( array ) {
+
+ this.x = array[ 0 ];
+ this.y = array[ 1 ];
+ this.z = array[ 2 ];
+
+ return this;
+
+ },
+
+ toArray: function () {
+
+ return [ this.x, this.y, this.z ];
+
+ },
+
+ clone: function () {
+
+ return new cornerstoneMath.Vector3( this.x, this.y, this.z );
+
+ }
+
+ };
+
+ return cornerstoneMath;
+}(cornerstoneMath));
+// End Source; src/vector3.js
+
+// Begin Source: src/Line3.js
+// Copied from THREE.JS
+/**
+ * @author bhouston / http://exocortex.com
+ */
+
+
+var cornerstoneMath = (function (cornerstoneMath) {
+
+ "use strict";
+
+ if (cornerstoneMath === undefined) {
+ cornerstoneMath = {};
+ }
+
+ cornerstoneMath.Line3 = function ( start, end ) {
+
+ this.start = ( start !== undefined ) ? start : new cornerstoneMath.Vector3();
+ this.end = ( end !== undefined ) ? end : new cornerstoneMath.Vector3();
+
+ };
+
+ cornerstoneMath.Line3.prototype = {
+
+ constructor: cornerstoneMath.Line3,
+
+ set: function ( start, end ) {
+
+ this.start.copy( start );
+ this.end.copy( end );
+
+ return this;
+
+ },
+
+ copy: function ( line ) {
+
+ this.start.copy( line.start );
+ this.end.copy( line.end );
+
+ return this;
+
+ },
+
+ center: function ( optionalTarget ) {
+
+ var result = optionalTarget || new cornerstoneMath.Vector3();
+ return result.addVectors( this.start, this.end ).multiplyScalar( 0.5 );
+
+ },
+
+ delta: function ( optionalTarget ) {
+
+ var result = optionalTarget || new cornerstoneMath.Vector3();
+ return result.subVectors( this.end, this.start );
+
+ },
+
+ distanceSq: function () {
+
+ return this.start.distanceToSquared( this.end );
+
+ },
+
+ distance: function () {
+
+ return this.start.distanceTo( this.end );
+
+ },
+
+ at: function ( t, optionalTarget ) {
+
+ var result = optionalTarget || new cornerstoneMath.Vector3();
+
+ return this.delta( result ).multiplyScalar( t ).add( this.start );
+
+ },
+
+ closestPointToPointParameter: function () {
+
+ var startP = new cornerstoneMath.Vector3();
+ var startEnd = new cornerstoneMath.Vector3();
+
+ return function ( point, clampToLine ) {
+
+ startP.subVectors( point, this.start );
+ startEnd.subVectors( this.end, this.start );
+
+ var startEnd2 = startEnd.dot( startEnd );
+ var startEnd_startP = startEnd.dot( startP );
+
+ var t = startEnd_startP / startEnd2;
+
+ if ( clampToLine ) {
+
+ t = cornerstoneMath.Math.clamp( t, 0, 1 );
+
+ }
+
+ return t;
+
+ };
+
+ }(),
+
+ closestPointToPoint: function ( point, clampToLine, optionalTarget ) {
+
+ var t = this.closestPointToPointParameter( point, clampToLine );
+
+ var result = optionalTarget || new cornerstoneMath.Vector3();
+
+ return this.delta( result ).multiplyScalar( t ).add( this.start );
+
+ },
+
+ applyMatrix4: function ( matrix ) {
+
+ this.start.applyMatrix4( matrix );
+ this.end.applyMatrix4( matrix );
+
+ return this;
+
+ },
+
+ equals: function ( line ) {
+
+ return line.start.equals( this.start ) && line.end.equals( this.end );
+
+ },
+
+ clone: function () {
+
+ return new cornerstoneMath.Line3().copy( this );
+
+ },
+
+ intersectLine: function ( line ) {
+ // http://stackoverflow.com/questions/2316490/the-algorithm-to-find-the-point-of-intersection-of-two-3d-line-segment/10288710#10288710
+ var da = this.end.clone().sub(this.start);
+ var db = line.end.clone().sub(line.start);
+ var dc = line.start.clone().sub(this.start);
+
+ var daCrossDb = da.clone().cross(db);
+ var dcCrossDb = dc.clone().cross(db);
+
+ if (dc.dot(da) === 0){
+ // Lines are not coplanar, stop here
+ return;
+ }
+
+ var s = dcCrossDb.dot(daCrossDb) / daCrossDb.lengthSq();
+
+ // Make sure we have an intersection
+ if (s > 1.0 || isNaN(s)) {
+ return;
+ }
+
+ var intersection = this.start.clone().add(da.clone().multiplyScalar(s));
+ var distanceTest = intersection.clone().sub(line.start).lengthSq() + intersection.clone().sub(line.end).lengthSq();
+ if (distanceTest <= line.distanceSq()) {
+ return intersection;
+ }
+ return;
+ }
+ };
+
+ return cornerstoneMath;
+}(cornerstoneMath));
+// End Source; src/Line3.js
+
+// Begin Source: src/lineSegment.js
+var cornerstoneMath = (function (cornerstoneMath) {
+
+ "use strict";
+
+ if(cornerstoneMath === undefined) {
+ cornerstoneMath = {};
+ }
+
+ // based on http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
+ function sqr(x)
+ {
+ return x * x;
+ }
+
+ function dist2(v, w) {
+ return sqr(v.x - w.x) + sqr(v.y - w.y);
+ }
+
+ function distanceToPointSquared(lineSegment, point)
+ {
+ var l2 = dist2(lineSegment.start, lineSegment.end);
+ if(l2 === 0) {
+ return dist2(point, lineSegment.start);
+ }
+ var t = ((point.x - lineSegment.start.x) * (lineSegment.end.x - lineSegment.start.x) +
+ (point.y - lineSegment.start.y) * (lineSegment.end.y - lineSegment.start.y)) / l2;
+ if(t < 0) {
+ return dist2(point, lineSegment.start);
+ }
+ if(t > 1) {
+ return dist2(point, lineSegment.end);
+ }
+
+ var pt = {
+ x : lineSegment.start.x + t * (lineSegment.end.x - lineSegment.start.x),
+ y : lineSegment.start.y + t * (lineSegment.end.y - lineSegment.start.y)
+ };
+ return dist2(point, pt);
+ }
+
+ function distanceToPoint(lineSegment, point)
+ {
+ return Math.sqrt(distanceToPointSquared(lineSegment, point));
+ }
+
+ // module exports
+ cornerstoneMath.lineSegment =
+ {
+ distanceToPoint : distanceToPoint
+ };
+
+
+ return cornerstoneMath;
+}(cornerstoneMath));
+// End Source; src/lineSegment.js
+
+// Begin Source: src/math.js
+// Based on THREE.JS
+
+var cornerstoneMath = (function (cornerstoneMath) {
+
+ "use strict";
+
+ if (cornerstoneMath === undefined) {
+ cornerstoneMath = {};
+ }
+
+ function clamp(x,a,b) {
+ return ( x < a ) ? a : ( ( x > b ) ? b : x );
+ }
+
+ function degToRad(degrees) {
+ var degreeToRadiansFactor = Math.PI / 180;
+ return degrees * degreeToRadiansFactor;
+ }
+
+ function radToDeg(radians) {
+ var radianToDegreesFactor = 180 / Math.PI;
+ return radians * radianToDegreesFactor;
+ }
+
+ cornerstoneMath.clamp = clamp;
+ cornerstoneMath.degToRad = degToRad;
+ cornerstoneMath.radToDeg = radToDeg;
+
+ return cornerstoneMath;
+}(cornerstoneMath));
+// End Source; src/math.js
+
+// Begin Source: src/matrix4.js
+// Based on THREE.JS
+
+var cornerstoneMath = (function (cornerstoneMath) {
+
+ "use strict";
+
+ if(cornerstoneMath === undefined) {
+ cornerstoneMath = {};
+ }
+
+ cornerstoneMath.Matrix4 = function Matrix4(n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44 )
+ {
+ this.elements = new Float32Array( 16 );
+
+ // TODO: if n11 is undefined, then just set to identity, otherwise copy all other values into matrix
+ // we should not support semi specification of Matrix4, it is just weird.
+
+ var te = this.elements;
+
+ te[0] = ( n11 !== undefined ) ? n11 : 1; te[4] = n12 || 0; te[8] = n13 || 0; te[12] = n14 || 0;
+ te[1] = n21 || 0; te[5] = ( n22 !== undefined ) ? n22 : 1; te[9] = n23 || 0; te[13] = n24 || 0;
+ te[2] = n31 || 0; te[6] = n32 || 0; te[10] = ( n33 !== undefined ) ? n33 : 1; te[14] = n34 || 0;
+ te[3] = n41 || 0; te[7] = n42 || 0; te[11] = n43 || 0; te[15] = ( n44 !== undefined ) ? n44 : 1;
+ };
+
+ cornerstoneMath.Matrix4.prototype.makeRotationFromQuaternion = function(q) {
+ var te = this.elements;
+
+ var x = q.x, y = q.y, z = q.z, w = q.w;
+ var x2 = x + x, y2 = y + y, z2 = z + z;
+ var xx = x * x2, xy = x * y2, xz = x * z2;
+ var yy = y * y2, yz = y * z2, zz = z * z2;
+ var wx = w * x2, wy = w * y2, wz = w * z2;
+
+ te[0] = 1 - ( yy + zz );
+ te[4] = xy - wz;
+ te[8] = xz + wy;
+
+ te[1] = xy + wz;
+ te[5] = 1 - ( xx + zz );
+ te[9] = yz - wx;
+
+ te[2] = xz - wy;
+ te[6] = yz + wx;
+ te[10] = 1 - ( xx + yy );
+
+ // last column
+ te[3] = 0;
+ te[7] = 0;
+ te[11] = 0;
+
+ // bottom row
+ te[12] = 0;
+ te[13] = 0;
+ te[14] = 0;
+ te[15] = 1;
+
+ return this;
+ };
+
+ cornerstoneMath.Matrix4.prototype.multiplyMatrices = function(a, b)
+ {
+ var ae = a.elements;
+ var be = b.elements;
+ var te = this.elements;
+
+ var a11 = ae[0], a12 = ae[4], a13 = ae[8], a14 = ae[12];
+ var a21 = ae[1], a22 = ae[5], a23 = ae[9], a24 = ae[13];
+ var a31 = ae[2], a32 = ae[6], a33 = ae[10], a34 = ae[14];
+ var a41 = ae[3], a42 = ae[7], a43 = ae[11], a44 = ae[15];
+
+ var b11 = be[0], b12 = be[4], b13 = be[8], b14 = be[12];
+ var b21 = be[1], b22 = be[5], b23 = be[9], b24 = be[13];
+ var b31 = be[2], b32 = be[6], b33 = be[10], b34 = be[14];
+ var b41 = be[3], b42 = be[7], b43 = be[11], b44 = be[15];
+
+ te[0] = a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41;
+ te[4] = a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42;
+ te[8] = a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43;
+ te[12] = a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44;
+
+ te[1] = a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41;
+ te[5] = a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42;
+ te[9] = a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43;
+ te[13] = a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44;
+
+ te[2] = a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41;
+ te[6] = a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42;
+ te[10] = a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43;
+ te[14] = a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44;
+
+ te[3] = a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41;
+ te[7] = a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42;
+ te[11] = a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43;
+ te[15] = a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44;
+
+ return this;
+ };
+
+ cornerstoneMath.Matrix4.prototype.multiply = function(m, n )
+ {
+
+ if (n !== undefined) {
+
+ console.warn('DEPRECATED: Matrix4\'s .multiply() now only accepts one argument. Use .multiplyMatrices( a, b ) instead.');
+ return this.multiplyMatrices(m, n);
+ }
+
+ return this.multiplyMatrices(this, m);
+ };
+
+ cornerstoneMath.Matrix4.prototype.getInverse = function ( m, throwOnInvertible ) {
+
+ // based on http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.htm
+ var te = this.elements;
+ var me = m.elements;
+
+ var n11 = me[0], n12 = me[4], n13 = me[8], n14 = me[12];
+ var n21 = me[1], n22 = me[5], n23 = me[9], n24 = me[13];
+ var n31 = me[2], n32 = me[6], n33 = me[10], n34 = me[14];
+ var n41 = me[3], n42 = me[7], n43 = me[11], n44 = me[15];
+
+ te[0] = n23*n34*n42 - n24*n33*n42 + n24*n32*n43 - n22*n34*n43 - n23*n32*n44 + n22*n33*n44;
+ te[4] = n14*n33*n42 - n13*n34*n42 - n14*n32*n43 + n12*n34*n43 + n13*n32*n44 - n12*n33*n44;
+ te[8] = n13*n24*n42 - n14*n23*n42 + n14*n22*n43 - n12*n24*n43 - n13*n22*n44 + n12*n23*n44;
+ te[12] = n14*n23*n32 - n13*n24*n32 - n14*n22*n33 + n12*n24*n33 + n13*n22*n34 - n12*n23*n34;
+ te[1] = n24*n33*n41 - n23*n34*n41 - n24*n31*n43 + n21*n34*n43 + n23*n31*n44 - n21*n33*n44;
+ te[5] = n13*n34*n41 - n14*n33*n41 + n14*n31*n43 - n11*n34*n43 - n13*n31*n44 + n11*n33*n44;
+ te[9] = n14*n23*n41 - n13*n24*n41 - n14*n21*n43 + n11*n24*n43 + n13*n21*n44 - n11*n23*n44;
+ te[13] = n13*n24*n31 - n14*n23*n31 + n14*n21*n33 - n11*n24*n33 - n13*n21*n34 + n11*n23*n34;
+ te[2] = n22*n34*n41 - n24*n32*n41 + n24*n31*n42 - n21*n34*n42 - n22*n31*n44 + n21*n32*n44;
+ te[6] = n14*n32*n41 - n12*n34*n41 - n14*n31*n42 + n11*n34*n42 + n12*n31*n44 - n11*n32*n44;
+ te[10] = n12*n24*n41 - n14*n22*n41 + n14*n21*n42 - n11*n24*n42 - n12*n21*n44 + n11*n22*n44;
+ te[14] = n14*n22*n31 - n12*n24*n31 - n14*n21*n32 + n11*n24*n32 + n12*n21*n34 - n11*n22*n34;
+ te[3] = n23*n32*n41 - n22*n33*n41 - n23*n31*n42 + n21*n33*n42 + n22*n31*n43 - n21*n32*n43;
+ te[7] = n12*n33*n41 - n13*n32*n41 + n13*n31*n42 - n11*n33*n42 - n12*n31*n43 + n11*n32*n43;
+ te[11] = n13*n22*n41 - n12*n23*n41 - n13*n21*n42 + n11*n23*n42 + n12*n21*n43 - n11*n22*n43;
+ te[15] = n12*n23*n31 - n13*n22*n31 + n13*n21*n32 - n11*n23*n32 - n12*n21*n33 + n11*n22*n33;
+
+ var det = n11 * te[ 0 ] + n21 * te[ 4 ] + n31 * te[ 8 ] + n41 * te[ 12 ];
+
+ if ( det === 0 ) {
+
+ var msg = "Matrix4.getInverse(): can't invert matrix, determinant is 0";
+
+ if ( throwOnInvertible || false ) {
+
+ throw new Error( msg );
+
+ } else {
+
+ console.warn( msg );
+
+ }
+
+ this.identity();
+
+ return this;
+ }
+
+ this.multiplyScalar( 1 / det );
+
+ return this;
+
+ };
+
+ cornerstoneMath.Matrix4.prototype.applyToVector3Array = function() {
+
+ var v1 = new cornerstoneMath.Vector3();
+
+ return function ( array, offset, length ) {
+
+ if ( offset === undefined ) offset = 0;
+ if ( length === undefined ) length = array.length;
+
+ for ( var i = 0, j = offset, il; i < length; i += 3, j += 3 ) {
+
+ v1.x = array[ j ];
+ v1.y = array[ j + 1 ];
+ v1.z = array[ j + 2 ];
+
+ v1.applyMatrix4( this );
+
+ array[ j ] = v1.x;
+ array[ j + 1 ] = v1.y;
+ array[ j + 2 ] = v1.z;
+
+ }
+
+ return array;
+
+ };
+
+ };
+
+ cornerstoneMath.Matrix4.prototype.makeTranslation = function ( x, y, z ) {
+
+ this.set(
+
+ 1, 0, 0, x,
+ 0, 1, 0, y,
+ 0, 0, 1, z,
+ 0, 0, 0, 1
+
+ );
+
+ return this;
+
+ };
+ cornerstoneMath.Matrix4.prototype.multiplyScalar = function ( s ) {
+
+ var te = this.elements;
+
+ te[0] *= s; te[4] *= s; te[8] *= s; te[12] *= s;
+ te[1] *= s; te[5] *= s; te[9] *= s; te[13] *= s;
+ te[2] *= s; te[6] *= s; te[10] *= s; te[14] *= s;
+ te[3] *= s; te[7] *= s; te[11] *= s; te[15] *= s;
+
+ return this;
+
+ };
+ cornerstoneMath.Matrix4.prototype.set = function ( n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44 ) {
+
+ var te = this.elements;
+
+ te[0] = n11; te[4] = n12; te[8] = n13; te[12] = n14;
+ te[1] = n21; te[5] = n22; te[9] = n23; te[13] = n24;
+ te[2] = n31; te[6] = n32; te[10] = n33; te[14] = n34;
+ te[3] = n41; te[7] = n42; te[11] = n43; te[15] = n44;
+
+ return this;
+
+ };
+
+ cornerstoneMath.Matrix4.prototype.makeScale = function ( x, y, z ) {
+
+ this.set(
+
+ x, 0, 0, 0,
+ 0, y, 0, 0,
+ 0, 0, z, 0,
+ 0, 0, 0, 1
+
+ );
+
+ return this;
+
+ };
+
+
+ return cornerstoneMath;
+}(cornerstoneMath));
+// End Source; src/matrix4.js
+
+// Begin Source: src/plane.js
+// Copied from Three.JS
+/**
+ * @author bhouston / http://exocortex.com
+ */
+
+var cornerstoneMath = (function (cornerstoneMath) {
+
+ "use strict";
+
+ if (cornerstoneMath === undefined) {
+ cornerstoneMath = {};
+ }
+
+ cornerstoneMath.Plane = function ( normal, constant ) {
+
+ this.normal = ( normal !== undefined ) ? normal : new cornerstoneMath.Vector3( 1, 0, 0 );
+ this.constant = ( constant !== undefined ) ? constant : 0;
+
+ };
+
+ cornerstoneMath.Plane.prototype = {
+
+ constructor: cornerstoneMath.Plane,
+
+ set: function ( normal, constant ) {
+
+ this.normal.copy( normal );
+ this.constant = constant;
+
+ return this;
+
+ },
+
+ setComponents: function ( x, y, z, w ) {
+
+ this.normal.set( x, y, z );
+ this.constant = w;
+
+ return this;
+
+ },
+
+ setFromNormalAndCoplanarPoint: function ( normal, point ) {
+
+ this.normal.copy( normal );
+ this.constant = - point.dot( this.normal ); // must be this.normal, not normal, as this.normal is normalized
+
+ return this;
+
+ },
+
+ setFromCoplanarPoints: function () {
+
+ var v1 = new cornerstoneMath.Vector3();
+ var v2 = new cornerstoneMath.Vector3();
+
+ return function ( a, b, c ) {
+
+ var normal = v1.subVectors( c, b ).cross( v2.subVectors( a, b ) ).normalize();
+
+ // Q: should an error be thrown if normal is zero (e.g. degenerate plane)?
+
+ this.setFromNormalAndCoplanarPoint( normal, a );
+
+ return this;
+
+ };
+
+ }(),
+
+
+ copy: function ( plane ) {
+
+ this.normal.copy( plane.normal );
+ this.constant = plane.constant;
+
+ return this;
+
+ },
+
+ normalize: function () {
+
+ // Note: will lead to a divide by zero if the plane is invalid.
+
+ var inverseNormalLength = 1.0 / this.normal.length();
+ this.normal.multiplyScalar( inverseNormalLength );
+ this.constant *= inverseNormalLength;
+
+ return this;
+
+ },
+
+ negate: function () {
+
+ this.constant *= - 1;
+ this.normal.negate();
+
+ return this;
+
+ },
+
+ distanceToPoint: function ( point ) {
+
+ return this.normal.dot( point ) + this.constant;
+
+ },
+
+ distanceToSphere: function ( sphere ) {
+
+ return this.distanceToPoint( sphere.center ) - sphere.radius;
+
+ },
+
+ projectPoint: function ( point, optionalTarget ) {
+
+ return this.orthoPoint( point, optionalTarget ).sub( point ).negate();
+
+ },
+
+ orthoPoint: function ( point, optionalTarget ) {
+
+ var perpendicularMagnitude = this.distanceToPoint( point );
+
+ var result = optionalTarget || new cornerstoneMath.Vector3();
+ return result.copy( this.normal ).multiplyScalar( perpendicularMagnitude );
+
+ },
+
+ isIntersectionLine: function ( line ) {
+
+ // Note: this tests if a line intersects the plane, not whether it (or its end-points) are coplanar with it.
+
+ var startSign = this.distanceToPoint( line.start );
+ var endSign = this.distanceToPoint( line.end );
+
+ return ( startSign < 0 && endSign > 0 ) || ( endSign < 0 && startSign > 0 );
+
+ },
+
+ intersectLine: function () {
+
+ var v1 = new cornerstoneMath.Vector3();
+
+ return function ( line, optionalTarget ) {
+
+ var result = optionalTarget || new cornerstoneMath.Vector3();
+
+ var direction = line.delta( v1 );
+
+ var denominator = this.normal.dot( direction );
+
+ if ( denominator === 0 ) {
+
+ // line is coplanar, return origin
+ if ( this.distanceToPoint( line.start ) === 0 ) {
+
+ return result.copy( line.start );
+
+ }
+
+ // Unsure if this is the correct method to handle this case.
+ return undefined;
+
+ }
+
+ var t = - ( line.start.dot( this.normal ) + this.constant ) / denominator;
+
+ if ( t < 0 || t > 1 ) {
+
+ return undefined;
+
+ }
+
+ return result.copy( direction ).multiplyScalar( t ).add( line.start );
+
+ };
+
+ }(),
+
+ intersectPlane: function (targetPlane) {
+ // Returns the intersection line between two planes
+ var direction = this.normal.clone().cross(targetPlane.normal);
+ var origin = new cornerstoneMath.Vector3();
+ var intersectionData = {
+ origin: origin,
+ direction: direction
+ };
+
+ // If the planes are parallel, return an empty vector for the
+ // intersection line
+ if (this.normal.clone().cross(targetPlane.normal).length < 1e-10) {
+ intersectionData.direction = new cornerstoneMath.Vector3();
+ return intersectionData;
+ }
+
+ var h1 = this.constant;
+ var h2 = targetPlane.constant;
+ var n1dotn2 = this.normal.clone().dot(targetPlane.normal);
+
+ var c1 = -(h1 - h2 * n1dotn2) / (1 - n1dotn2 * n1dotn2);
+ var c2 = -(h2 - h1 * n1dotn2) / (1 - n1dotn2 * n1dotn2);
+ intersectionData.origin = this.normal.clone().multiplyScalar(c1).add(targetPlane.normal.clone().multiplyScalar(c2));
+ return intersectionData;
+ },
+
+ coplanarPoint: function ( optionalTarget ) {
+
+ var result = optionalTarget || new cornerstoneMath.Vector3();
+ return result.copy( this.normal ).multiplyScalar( - this.constant );
+
+ },
+
+ translate: function ( offset ) {
+
+ this.constant = this.constant - offset.dot( this.normal );
+
+ return this;
+
+ },
+
+ equals: function ( plane ) {
+
+ return plane.normal.equals( this.normal ) && ( plane.constant == this.constant );
+
+ },
+
+ clone: function () {
+
+ return new cornerstoneMath.Plane().copy( this );
+
+ }
+ };
+
+ return cornerstoneMath;
+}(cornerstoneMath));
+// End Source; src/plane.js
+
+// Begin Source: src/point.js
+var cornerstoneMath = (function (cornerstoneMath) {
+
+ "use strict";
+
+ if(cornerstoneMath === undefined) {
+ cornerstoneMath = {};
+ }
+
+ function pageToPoint(e)
+ {
+ return {
+ x : e.pageX,
+ y : e.pageY
+ };
+ }
+
+ function subtract(lhs, rhs)
+ {
+ return {
+ x : lhs.x - rhs.x,
+ y : lhs.y - rhs.y
+ };
+ }
+
+ function copy(point)
+ {
+ return {
+ x : point.x,
+ y : point.y
+ };
+ }
+
+ function distance(from, to)
+ {
+ return Math.sqrt(distanceSquared(from, to));
+ }
+
+ function distanceSquared(from, to)
+ {
+ var delta = subtract(from, to);
+ return delta.x * delta.x + delta.y * delta.y;
+ }
+
+ function insideRect(point, rect)
+ {
+ if( point.x < rect.left ||
+ point.x > rect.left + rect.width ||
+ point.y < rect.top ||
+ point.y > rect.top + rect.height)
+ {
+ return false;
+ }
+ return true;
+ }
+
+
+ // module exports
+ cornerstoneMath.point =
+ {
+ subtract : subtract,
+ copy: copy,
+ pageToPoint: pageToPoint,
+ distance: distance,
+ distanceSquared: distanceSquared,
+ insideRect: insideRect
+ };
+
+
+ return cornerstoneMath;
+}(cornerstoneMath));
+// End Source; src/point.js
+
+// Begin Source: src/quaternion.js
+// Based on THREE.JS
+
+var cornerstoneMath = (function (cornerstoneMath) {
+
+ "use strict";
+
+ if(cornerstoneMath === undefined) {
+ cornerstoneMath = {};
+ }
+
+ cornerstoneMath.Quaternion = function Quaternion(x, y, z, w)
+ {
+ this.x = x || 0;
+ this.y = y || 0;
+ this.z = z || 0;
+ this.w = ( w !== undefined ) ? w : 1;
+ };
+
+ cornerstoneMath.Quaternion.prototype.setFromAxisAngle = function(axis, angle)
+ {
+ var halfAngle = angle / 2, s = Math.sin( halfAngle );
+
+ this.x = axis.x * s;
+ this.y = axis.y * s;
+ this.z = axis.z * s;
+ this.w = Math.cos( halfAngle );
+
+ return this;
+ };
+
+ cornerstoneMath.Quaternion.prototype.multiplyQuaternions = function( a, b)
+ {
+ var qax = a.x, qay = a.y, qaz = a.z, qaw = a.w;
+ var qbx = b.x, qby = b.y, qbz = b.z, qbw = b.w;
+
+ this.x = qax * qbw + qaw * qbx + qay * qbz - qaz * qby;
+ this.y = qay * qbw + qaw * qby + qaz * qbx - qax * qbz;
+ this.z = qaz * qbw + qaw * qbz + qax * qby - qay * qbx;
+ this.w = qaw * qbw - qax * qbx - qay * qby - qaz * qbz;
+
+ return this;
+ };
+
+ cornerstoneMath.Quaternion.prototype.setFromRotationMatrix = function(m)
+ {
+ var te = m.elements,
+
+ m11 = te[0], m12 = te[4], m13 = te[8],
+ m21 = te[1], m22 = te[5], m23 = te[9],
+ m31 = te[2], m32 = te[6], m33 = te[10],
+
+ trace = m11 + m22 + m33,
+ s;
+
+ if ( trace > 0 ) {
+
+ s = 0.5 / Math.sqrt( trace + 1.0 );
+
+ this.w = 0.25 / s;
+ this.x = ( m32 - m23 ) * s;
+ this.y = ( m13 - m31 ) * s;
+ this.z = ( m21 - m12 ) * s;
+
+ } else if ( m11 > m22 && m11 > m33 ) {
+
+ s = 2.0 * Math.sqrt( 1.0 + m11 - m22 - m33 );
+
+ this.w = (m32 - m23 ) / s;
+ this.x = 0.25 * s;
+ this.y = (m12 + m21 ) / s;
+ this.z = (m13 + m31 ) / s;
+
+ } else if ( m22 > m33 ) {
+
+ s = 2.0 * Math.sqrt( 1.0 + m22 - m11 - m33 );
+
+ this.w = (m13 - m31 ) / s;
+ this.x = (m12 + m21 ) / s;
+ this.y = 0.25 * s;
+ this.z = (m23 + m32 ) / s;
+
+ } else {
+
+ s = 2.0 * Math.sqrt( 1.0 + m33 - m11 - m22 );
+
+ this.w = ( m21 - m12 ) / s;
+ this.x = ( m13 + m31 ) / s;
+ this.y = ( m23 + m32 ) / s;
+ this.z = 0.25 * s;
+
+ }
+
+ return this;
+ };
+
+ return cornerstoneMath;
+}(cornerstoneMath));
+// End Source; src/quaternion.js
+
+// Begin Source: src/rect.js
+var cornerstoneMath = (function (cornerstoneMath) {
+
+ "use strict";
+
+ if(cornerstoneMath === undefined) {
+ cornerstoneMath = {};
+ }
+
+ function rectToLineSegments(rect)
+ {
+ var top = {
+ start : {
+ x :rect.left,
+ y :rect.top
+ },
+ end : {
+ x :rect.left + rect.width,
+ y :rect.top
+
+ }
+ };
+ var right = {
+ start : {
+ x :rect.left + rect.width,
+ y :rect.top
+ },
+ end : {
+ x :rect.left + rect.width,
+ y :rect.top + rect.height
+
+ }
+ };
+ var bottom = {
+ start : {
+ x :rect.left + rect.width,
+ y :rect.top + rect.height
+ },
+ end : {
+ x :rect.left,
+ y :rect.top + rect.height
+
+ }
+ };
+ var left = {
+ start : {
+ x :rect.left,
+ y :rect.top + rect.height
+ },
+ end : {
+ x :rect.left,
+ y :rect.top
+
+ }
+ };
+ var lineSegments = [top, right, bottom, left];
+ return lineSegments;
+ }
+
+ function pointNearLineSegment(point, lineSegment, maxDistance)
+ {
+ if(maxDistance === undefined) {
+ maxDistance = 5;
+ }
+ var distance = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, point);
+
+ return (distance < maxDistance);
+ }
+ function distanceToPoint(rect, point)
+ {
+ var minDistance = 655535;
+ var lineSegments = rectToLineSegments(rect);
+ lineSegments.forEach(function(lineSegment) {
+ var distance = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, point);
+ if(distance < minDistance) {
+ minDistance = distance;
+ }
+ });
+ return minDistance;
+ }
+
+ // module exports
+ cornerstoneMath.rect =
+ {
+ distanceToPoint : distanceToPoint
+ };
+
+
+ return cornerstoneMath;
+}(cornerstoneMath));
+// End Source; src/rect.js
\ No newline at end of file
diff --git a/Packages/cornerstone/client/cornerstoneTools.js b/Packages/cornerstone/client/cornerstoneTools.js
new file mode 100644
index 000000000..4a5b3e686
--- /dev/null
+++ b/Packages/cornerstone/client/cornerstoneTools.js
@@ -0,0 +1,9913 @@
+/*! cornerstoneTools - v0.7.6 - 2015-10-08 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneTools */
+// Begin Source: src/header.js
+if (typeof cornerstone === 'undefined') {
+ cornerstone = {};
+}
+
+if (typeof cornerstoneTools === 'undefined') {
+ cornerstoneTools = {
+ referenceLines: {},
+ orientation: {}
+ };
+}
+
+// End Source; src/header.js
+
+// Begin Source: src/inputSources/mouseWheelInput.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function mouseWheel(e) {
+ // !!!HACK/NOTE/WARNING!!!
+ // for some reason I am getting mousewheel and DOMMouseScroll events on my
+ // mac os x mavericks system when middle mouse button dragging.
+ // I couldn't find any info about this so this might break other systems
+ // webkit hack
+ if (e.originalEvent.type === 'mousewheel' && e.originalEvent.wheelDeltaY === 0) {
+ return;
+ }
+ // firefox hack
+ if (e.originalEvent.type === 'DOMMouseScroll' && e.originalEvent.axis === 1) {
+ return;
+ }
+
+ var element = e.currentTarget;
+ var startingCoords = cornerstone.pageToPixel(element, e.pageX || e.originalEvent.pageX, e.pageY || e.originalEvent.pageY);
+
+ e = window.event || e; // old IE support
+ var wheelDelta = e.wheelDelta || -e.detail || -e.originalEvent.detail;
+ 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).trigger('CornerstoneToolsMouseWheel', mouseWheelData);
+ }
+
+ var mouseWheelEvents = 'mousewheel DOMMouseScroll';
+
+ function enable(element) {
+ $(element).on(mouseWheelEvents, mouseWheel);
+ }
+
+ function disable(element) {
+ $(element).unbind(mouseWheelEvents, mouseWheel);
+ }
+
+ // module exports
+ cornerstoneTools.mouseWheelInput = {
+ enable: enable, disable: disable
+ };
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/inputSources/mouseWheelInput.js
+
+// Begin Source: src/inputSources/mouseInput.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ var isClickEvent;
+ var preventClickTimeout;
+ var clickDelay = 200;
+
+ function preventClickHandler() {
+ isClickEvent = false;
+ }
+
+ function activateMouseDown(mouseEventDetail) {
+ $(mouseEventDetail.element).trigger('CornerstoneToolsMouseDownActivate', mouseEventDetail);
+ }
+
+ function mouseDoubleClick(e) {
+ var element = e.currentTarget;
+ 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
+ }
+ };
+ startPoints.canvas = cornerstone.pixelToCanvas(element, startPoints.image);
+
+ var lastPoints = cornerstoneTools.copyPoints(startPoints);
+ var eventData = {
+ event: e,
+ which: e.which,
+ viewport: cornerstone.getViewport(element),
+ image: cornerstone.getEnabledElement(element).image,
+ element: element,
+ startPoints: startPoints,
+ lastPoints: lastPoints,
+ currentPoints: startPoints,
+ deltaPoints: {
+ x: 0, y: 0
+ },
+ type: eventType
+ };
+
+ var event = $.Event(eventType, eventData);
+ $(eventData.element).trigger(event, eventData);
+ }
+
+ function mouseDown(e) {
+ preventClickTimeout = setTimeout(preventClickHandler, clickDelay);
+
+ var element = e.currentTarget;
+ var eventType = 'CornerstoneToolsMouseDown';
+
+ // Prevent CornerstoneToolsMouseMove while mouse is down
+ $(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
+ }
+ };
+ startPoints.canvas = cornerstone.pixelToCanvas(element, startPoints.image);
+
+ var lastPoints = cornerstoneTools.copyPoints(startPoints);
+ var eventData = {
+ event: e,
+ which: e.which,
+ viewport: cornerstone.getViewport(element),
+ image: cornerstone.getEnabledElement(element).image,
+ element: element,
+ startPoints: startPoints,
+ lastPoints: lastPoints,
+ currentPoints: startPoints,
+ deltaPoints: {
+ x: 0, y: 0
+ },
+ type: eventType
+ };
+
+ var event = $.Event(eventType, eventData);
+ $(eventData.element).trigger(event, eventData);
+
+ if (event.isImmediatePropagationStopped() === false) {
+ // no tools responded to this event, give the active tool a chance
+ if (activateMouseDown(eventData) === true) {
+ return cornerstoneTools.pauseEvent(e);
+ }
+ }
+
+ var whichMouseButton = e.which;
+
+ function onMouseMove(e) {
+ // 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
+ }
+ };
+ 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)
+ };
+
+ var eventData = {
+ which: whichMouseButton,
+ viewport: cornerstone.getViewport(element),
+ image: cornerstone.getEnabledElement(element).image,
+ element: element,
+ startPoints: startPoints,
+ lastPoints: lastPoints,
+ currentPoints: currentPoints,
+ deltaPoints: deltaPoints,
+ type: eventType
+ };
+
+ $(eventData.element).trigger(eventType, eventData);
+
+ // update the last points
+ lastPoints = cornerstoneTools.copyPoints(currentPoints);
+
+ // prevent left click selection of DOM elements
+ return cornerstoneTools.pauseEvent(e);
+ }
+
+ // hook mouseup so we can unbind our event listeners
+ // when they stop dragging
+ function onMouseUp(e) {
+ // Cancel the timeout preventing the click event from triggering
+ clearTimeout(preventClickTimeout);
+
+ var eventType = 'CornerstoneToolsMouseUp';
+ if (isClickEvent) {
+ eventType = 'CornerstoneToolsMouseClick';
+ }
+
+ // 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
+ }
+ };
+ 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)
+ };
+
+ var eventData = {
+ event: e,
+ which: whichMouseButton,
+ viewport: cornerstone.getViewport(element),
+ image: cornerstone.getEnabledElement(element).image,
+ element: element, startPoints: startPoints,
+ lastPoints: lastPoints,
+ currentPoints: currentPoints,
+ deltaPoints: deltaPoints,
+ type: eventType
+ };
+
+ var event = $.Event(eventType, eventData);
+ $(eventData.element).trigger(event, eventData);
+
+ $(document).off('mousemove', onMouseMove);
+ $(document).off('mouseup', onMouseUp);
+
+ $(eventData.element).on('mousemove', mouseMove);
+
+ isClickEvent = true;
+ }
+
+ $(document).on('mousemove', onMouseMove);
+ $(document).on('mouseup', onMouseUp);
+
+ return cornerstoneTools.pauseEvent(e);
+ }
+
+ function mouseMove(e) {
+ var element = e.currentTarget;
+ 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
+ }
+ };
+ startPoints.canvas = cornerstone.pixelToCanvas(element, startPoints.image);
+
+ var lastPoints = cornerstoneTools.copyPoints(startPoints);
+
+ var whichMouseButton = e.which;
+
+ // 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
+ }
+ };
+ 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)
+ };
+
+ var eventData = {
+ which: whichMouseButton,
+ viewport: cornerstone.getViewport(element),
+ image: cornerstone.getEnabledElement(element).image,
+ element: element,
+ startPoints: startPoints,
+ lastPoints: lastPoints,
+ currentPoints: currentPoints,
+ deltaPoints: deltaPoints,
+ type: eventType
+ };
+ $(element).trigger(eventType, eventData);
+
+ // update the last points
+ lastPoints = cornerstoneTools.copyPoints(currentPoints);
+ }
+
+ function disable(element) {
+ $(element).off('mousedown', mouseDown);
+ $(element).off('mousemove', mouseMove);
+ $(element).off('dblclick', mouseDoubleClick);
+ }
+
+ function enable(element) {
+ // Prevent handlers from being attached multiple times
+ disable(element);
+
+ $(element).on('mousedown', mouseDown);
+ $(element).on('mousemove', mouseMove);
+ $(element).on('dblclick', mouseDoubleClick);
+ }
+
+ // module exports
+ cornerstoneTools.mouseInput = {
+ enable: enable,
+ disable: disable
+ };
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/inputSources/mouseInput.js
+
+// Begin Source: src/inputSources/touchInput.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ /*jshint newcap: false */
+
+ var initScale = 1.0,
+ lastRotation = 0.0,
+ startPoints,
+ currentPoints,
+ lastPoints,
+ deltaPoints,
+ eventData;
+
+ function onTouch(e) {
+ console.log(e.type);
+ var element = e.target.parentNode,
+ event,
+ eventType;
+
+ // Prevent mouse events from occurring alongside touch events
+ e.preventDefault();
+
+ switch (e.type) {
+ case 'tap':
+ // 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 = 'CornerstoneToolsTap';
+ 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 'doubletap':
+ // 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 = 'CornerstoneToolsDoubleTap';
+ 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 '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;
+ break;
+
+ case 'pinchmove':
+ var scaleChange = initScale * e.scale;
+
+ startPoints = {
+ page: e.center,
+ image: cornerstone.pageToPixel(element, e.center.x, e.center.y),
+ };
+ startPoints.canvas = cornerstone.pixelToCanvas(element, startPoints.image);
+
+ eventType = 'CornerstoneToolsTouchPinch';
+ eventData = {
+ event: e,
+ startPoints: startPoints,
+ viewport: cornerstone.getViewport(element),
+ image: cornerstone.getEnabledElement(element).image,
+ element: element,
+ direction: e.scale < 1 ? 1 : -1,
+ scaleChange: scaleChange,
+ type: eventType,
+ isTouchEvent: true
+ };
+
+ event = $.Event(eventType, eventData);
+ $(element).trigger(event, eventData);
+ 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
+ }
+ };
+ startPoints.canvas = cornerstone.pixelToCanvas(element, startPoints.image);
+
+ eventType = 'CornerstoneToolsTouchStart';
+
+ 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);
+ $(eventData.element).trigger(event, eventData);
+
+ 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);
+ break;
+
+ case 'touchend':
+ startPoints = {
+ page: cornerstoneMath.point.pageToPoint(e.originalEvent.changedTouches[0]),
+ image: cornerstone.pageToPixel(element, e.originalEvent.changedTouches[0].pageX, e.originalEvent.changedTouches[0].pageY),
+ client: {
+ x: e.originalEvent.changedTouches[0].clientX,
+ y: e.originalEvent.changedTouches[0].clientY
+ }
+ };
+ startPoints.canvas = cornerstone.pixelToCanvas(element, startPoints.image);
+
+ eventType = 'CornerstoneToolsTouchEnd';
+
+ 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);
+ $(eventData.element).trigger(event, eventData);
+ break;
+
+ case 'panmove':
+ // 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);
+
+ // Calculate delta values in page and image coordinates
+ 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)
+ };
+
+ eventType = 'CornerstoneToolsTouchDrag';
+ if (e.pointers.length > 1) {
+ eventType = 'CornerstoneToolsMultiTouchDrag';
+ }
+
+ eventData = {
+ viewport: cornerstone.getViewport(element),
+ image: cornerstone.getEnabledElement(element).image,
+ element: element,
+ startPoints: startPoints,
+ lastPoints: lastPoints,
+ currentPoints: currentPoints,
+ deltaPoints: deltaPoints,
+ numPointers: e.pointers.length,
+ type: eventType,
+ isTouchEvent: true
+ };
+
+ event = $.Event(eventType, eventData);
+ $(element).trigger(event, eventData);
+
+ lastPoints = cornerstoneTools.copyPoints(currentPoints);
+ break;
+
+ case 'panend':
+ // If lastPoints is not yet set, it means panend fired without panstart or pan,
+ // so we can ignore this event
+ if (!lastPoints) {
+ return false;
+ }
+
+ 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);
+
+ // Calculate delta values in page and image coordinates
+ 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)
+ };
+
+ eventType = 'CornerstoneToolsDragEnd';
+
+ eventData = {
+ event: e.srcEvent,
+ viewport: cornerstone.getViewport(element),
+ image: cornerstone.getEnabledElement(element).image,
+ element: element,
+ startPoints: startPoints,
+ lastPoints: lastPoints,
+ currentPoints: currentPoints,
+ deltaPoints: deltaPoints,
+ type: eventType,
+ isTouchEvent: true
+ };
+
+ event = $.Event(eventType, eventData);
+ $(element).trigger(event, eventData);
+ return cornerstoneTools.pauseEvent(e);
+
+ case 'rotatemove':
+ var rotation = e.rotation - lastRotation;
+ lastRotation = e.rotation;
+
+ eventType = 'CornerstoneToolsTouchRotate';
+ eventData = {
+ event: e.srcEvent,
+ viewport: cornerstone.getViewport(element),
+ image: cornerstone.getEnabledElement(element).image,
+ element: element,
+ rotation: rotation,
+ type: eventType
+ };
+ event = $.Event(eventType, eventData);
+ $(element).trigger(event, eventData);
+ break;
+ }
+ return false;
+ }
+
+ function enable(element) {
+ disable(element);
+ var hammerOptions = {
+ inputClass: Hammer.SUPPORT_POINTER_EVENTS ? Hammer.PointerEventInput : Hammer.TouchInput
+ };
+
+ var mc = new Hammer.Manager(element, hammerOptions);
+
+ var panOptions = {
+ pointers: 0,
+ direction: Hammer.DIRECTION_ALL,
+ threshold: 0
+ };
+
+ var pan = new Hammer.Pan(panOptions);
+ var pinch = new Hammer.Pinch({
+ threshold: 0
+ });
+ 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);
+
+ $(element).data('hammer', mc);
+ $(element).on('touchstart touchend', onTouch);
+ cornerstoneTools.preventGhostClick(element);
+ }
+
+ function 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);
+ }
+ }
+
+ // module exports
+ cornerstoneTools.touchInput = {
+ enable: enable,
+ disable: disable
+ };
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/inputSources/touchInput.js
+
+// Begin Source: src/imageTools/simpleMouseButtonTool.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function simpleMouseButtonTool(mouseDownCallback) {
+ var configuration = {};
+
+ var toolInterface = {
+ activate: function(element, mouseButtonMask, options) {
+ $(element).off('CornerstoneToolsMouseDownActivate', mouseDownCallback);
+ var eventData = {
+ mouseButtonMask: mouseButtonMask, options: options
+ };
+ $(element).on('CornerstoneToolsMouseDownActivate', eventData, mouseDownCallback);
+ },
+ disable: function(element) {$(element).off('CornerstoneToolsMouseDownActivate', mouseDownCallback);},
+ enable: function(element) {$(element).off('CornerstoneToolsMouseDownActivate', mouseDownCallback);},
+ deactivate: function(element) {$(element).off('CornerstoneToolsMouseDownActivate', mouseDownCallback);},
+ getConfiguration: function() { return configuration;},
+ setConfiguration: function(config) {configuration = config;}
+ };
+ return toolInterface;
+ }
+
+ // module exports
+ cornerstoneTools.simpleMouseButtonTool = simpleMouseButtonTool;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/simpleMouseButtonTool.js
+
+// Begin Source: src/imageTools/mouseButtonTool.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ function mouseButtonTool(mouseToolInterface) {
+ var configuration = {};
+
+ ///////// BEGIN ACTIVE TOOL ///////
+ function addNewMeasurement(mouseEventData) {
+ var element = mouseEventData.element;
+
+ var measurementData = mouseToolInterface.createNewMeasurement(mouseEventData);
+ if (!measurementData) {
+ return;
+ }
+
+ var eventData = {
+ mouseButtonMask: mouseEventData.which,
+ };
+
+ // associate this data with this imageId so we can render it and manipulate it
+ cornerstoneTools.addToolState(mouseEventData.element, mouseToolInterface.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', mouseToolInterface.mouseMoveCallback || mouseMoveCallback);
+ $(element).off('CornerstoneToolsMouseDown', mouseToolInterface.mouseDownCallback || mouseDownCallback);
+ $(element).off('CornerstoneToolsMouseDownActivate', mouseToolInterface.mouseDownActivateCallback || mouseDownActivateCallback);
+
+ if (mouseToolInterface.mouseDoubleClickCallback) {
+ $(element).off('CornerstoneToolsMouseDoubleClick', mouseToolInterface.mouseDoubleClickCallback);
+ }
+
+ cornerstone.updateImage(element);
+
+ var handleMover;
+ if (Object.keys(measurementData.handles).length === 1) {
+ handleMover = cornerstoneTools.moveHandle;
+ } else {
+ handleMover = cornerstoneTools.moveNewHandle;
+ }
+
+ handleMover(mouseEventData, measurementData.handles.end, function() {
+ measurementData.active = false;
+ measurementData.invalidated = true;
+ if (cornerstoneTools.anyHandlesOutsideImage(mouseEventData, measurementData.handles)) {
+ // delete the measurement
+ cornerstoneTools.removeToolState(element, mouseToolInterface.toolType, measurementData);
+ }
+
+ $(element).on('CornerstoneToolsMouseMove', eventData, mouseToolInterface.mouseMoveCallback || mouseMoveCallback);
+ $(element).on('CornerstoneToolsMouseDown', eventData, mouseToolInterface.mouseDownCallback || mouseDownCallback);
+ $(element).on('CornerstoneToolsMouseDownActivate', eventData, mouseToolInterface.mouseDownActivateCallback || mouseDownActivateCallback);
+
+ if (mouseToolInterface.mouseDoubleClickCallback) {
+ $(element).on('CornerstoneToolsMouseDoubleClick', eventData, mouseToolInterface.mouseDoubleClickCallback);
+ }
+
+ cornerstone.updateImage(element);
+ });
+ }
+
+ function mouseDownActivateCallback(e, eventData) {
+ if (cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
+ if (mouseToolInterface.addNewMeasurement) {
+ mouseToolInterface.addNewMeasurement(eventData);
+ } else {
+ addNewMeasurement(eventData);
+ }
+
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+ }
+
+ ///////// END ACTIVE TOOL ///////
+
+ ///////// BEGIN DEACTIVE TOOL ///////
+
+ function mouseMoveCallback(e, eventData) {
+ cornerstoneTools.toolCoordinates.setCoords(eventData);
+ // if a mouse button is down, do nothing
+ if (eventData.which !== 0) {
+ return;
+ }
+
+ // if we have no tool data for this element, do nothing
+ var toolData = cornerstoneTools.getToolState(eventData.element, mouseToolInterface.toolType);
+ if (toolData === undefined) {
+ return;
+ }
+
+ // We have tool data, search through all data
+ // and see if we can activate a handle
+ var imageNeedsUpdate = false;
+ for (var i = 0; i < toolData.data.length; i++) {
+ // get the cursor position in canvas coordinates
+ var coords = eventData.currentPoints.canvas;
+
+ var data = toolData.data[i];
+ if (cornerstoneTools.handleActivator(eventData.element, data.handles, coords) === true) {
+ imageNeedsUpdate = true;
+ }
+
+ if ((mouseToolInterface.pointNearTool(eventData.element, data, coords) && !data.active) || (!mouseToolInterface.pointNearTool(eventData.element, data, coords) && data.active)) {
+ data.active = !data.active;
+ imageNeedsUpdate = true;
+ }
+ }
+
+ // Handle activation status changed, redraw the image
+ if (imageNeedsUpdate === true) {
+ cornerstone.updateImage(eventData.element);
+ }
+ }
+
+ function mouseDownCallback(e, eventData) {
+ var data;
+ var element = eventData.element;
+
+ function handleDoneMove() {
+ data.active = false;
+ data.invalidated = true;
+ if (cornerstoneTools.anyHandlesOutsideImage(eventData, data.handles)) {
+ // delete the measurement
+ cornerstoneTools.removeToolState(element, mouseToolInterface.toolType, data);
+ }
+
+ cornerstone.updateImage(element);
+ $(element).on('CornerstoneToolsMouseMove', eventData, mouseToolInterface.mouseMoveCallback || mouseMoveCallback);
+ }
+
+ if (cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
+ var coords = eventData.startPoints.canvas;
+ var toolData = cornerstoneTools.getToolState(e.currentTarget, mouseToolInterface.toolType);
+
+ var i;
+
+ // now check to see if there is a handle we can move
+ if (toolData) {
+ for (i = 0; i < toolData.data.length; i++) {
+ data = toolData.data[i];
+ var distanceSq = 25;
+ var handle = cornerstoneTools.getHandleNearImagePoint(element, data, coords, distanceSq);
+ if (handle) {
+ $(element).off('CornerstoneToolsMouseMove', mouseToolInterface.mouseMoveCallback || mouseMoveCallback);
+ data.active = true;
+ cornerstoneTools.moveHandle(eventData, handle, handleDoneMove);
+ e.stopImmediatePropagation();
+ return false;
+ }
+ }
+ }
+
+ // 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
+ if (toolData && mouseToolInterface.pointNearTool) {
+ var options = {
+ deleteIfHandleOutsideImage: true,
+ preventHandleOutsideImage: false
+ };
+
+ for (i = 0; i < toolData.data.length; i++) {
+ data = toolData.data[i];
+ if (mouseToolInterface.pointNearTool(element, data, coords)) {
+ $(element).off('CornerstoneToolsMouseMove', mouseToolInterface.mouseMoveCallback || mouseMoveCallback);
+ cornerstoneTools.moveAllHandles(e, data, toolData, options, handleDoneMove);
+ e.stopImmediatePropagation();
+ return false;
+ }
+ }
+ }
+ }
+ }
+ ///////// END DEACTIVE TOOL ///////
+
+ // not visible, not interactive
+ function disable(element) {
+ $(element).off('CornerstoneImageRendered', mouseToolInterface.onImageRendered);
+ $(element).off('CornerstoneToolsMouseMove', mouseToolInterface.mouseMoveCallback || mouseMoveCallback);
+ $(element).off('CornerstoneToolsMouseDown', mouseToolInterface.mouseDownCallback || mouseDownCallback);
+ $(element).off('CornerstoneToolsMouseDownActivate', mouseToolInterface.mouseDownActivateCallback || mouseDownActivateCallback);
+
+ if (mouseToolInterface.mouseDoubleClickCallback) {
+ $(element).off('CornerstoneToolsMouseDoubleClick', mouseToolInterface.mouseDoubleClickCallback);
+ }
+
+ cornerstone.updateImage(element);
+ }
+
+ // visible but not interactive
+ function enable(element) {
+ $(element).off('CornerstoneImageRendered', mouseToolInterface.onImageRendered);
+ $(element).off('CornerstoneToolsMouseMove', mouseToolInterface.mouseMoveCallback || mouseMoveCallback);
+ $(element).off('CornerstoneToolsMouseDown', mouseToolInterface.mouseDownCallback || mouseDownCallback);
+ $(element).off('CornerstoneToolsMouseDownActivate', mouseToolInterface.mouseDownActivateCallback || mouseDownActivateCallback);
+
+ if (mouseToolInterface.mouseDoubleClickCallback) {
+ $(element).off('CornerstoneToolsMouseDoubleClick', mouseToolInterface.mouseDoubleClickCallback);
+ }
+
+ $(element).on('CornerstoneImageRendered', mouseToolInterface.onImageRendered);
+
+ cornerstone.updateImage(element);
+ }
+
+ // visible, interactive and can create
+ function activate(element, mouseButtonMask) {
+ var eventData = {
+ mouseButtonMask: mouseButtonMask,
+ };
+
+ $(element).off('CornerstoneImageRendered', mouseToolInterface.onImageRendered);
+ $(element).off('CornerstoneToolsMouseMove', mouseToolInterface.mouseMoveCallback || mouseMoveCallback);
+ $(element).off('CornerstoneToolsMouseDown', mouseToolInterface.mouseDownCallback || mouseDownCallback);
+ $(element).off('CornerstoneToolsMouseDownActivate', mouseToolInterface.mouseDownActivateCallback || mouseDownActivateCallback);
+
+ $(element).on('CornerstoneImageRendered', mouseToolInterface.onImageRendered);
+ $(element).on('CornerstoneToolsMouseMove', eventData, mouseToolInterface.mouseMoveCallback || mouseMoveCallback);
+ $(element).on('CornerstoneToolsMouseDown', eventData, mouseToolInterface.mouseDownCallback || mouseDownCallback);
+ $(element).on('CornerstoneToolsMouseDownActivate', eventData, mouseToolInterface.mouseDownActivateCallback || mouseDownActivateCallback);
+
+ if (mouseToolInterface.mouseDoubleClickCallback) {
+ $(element).off('CornerstoneToolsMouseDoubleClick', mouseToolInterface.mouseDoubleClickCallback);
+ $(element).on('CornerstoneToolsMouseDoubleClick', eventData, mouseToolInterface.mouseDoubleClickCallback);
+ }
+
+ cornerstone.updateImage(element);
+ }
+
+ // visible, interactive
+ function deactivate(element, mouseButtonMask) {
+ var eventData = {
+ mouseButtonMask: mouseButtonMask,
+ };
+
+ $(element).off('CornerstoneImageRendered', mouseToolInterface.onImageRendered);
+ $(element).off('CornerstoneToolsMouseMove', mouseToolInterface.mouseMoveCallback || mouseMoveCallback);
+ $(element).off('CornerstoneToolsMouseDown', mouseToolInterface.mouseDownCallback || mouseDownCallback);
+ $(element).off('CornerstoneToolsMouseDownActivate', mouseToolInterface.mouseDownActivateCallback || mouseDownActivateCallback);
+
+ $(element).on('CornerstoneImageRendered', mouseToolInterface.onImageRendered);
+ $(element).on('CornerstoneToolsMouseMove', eventData, mouseToolInterface.mouseMoveCallback || mouseMoveCallback);
+ $(element).on('CornerstoneToolsMouseDown', eventData, mouseToolInterface.mouseDownCallback || mouseDownCallback);
+
+ if (mouseToolInterface.mouseDoubleClickCallback) {
+ $(element).off('CornerstoneToolsMouseDoubleClick', mouseToolInterface.mouseDoubleClickCallback);
+ $(element).on('CornerstoneToolsMouseDoubleClick', eventData, mouseToolInterface.mouseDoubleClickCallback);
+ }
+
+ cornerstone.updateImage(element);
+ }
+
+ function getConfiguration() {
+ return configuration;
+ }
+
+ function setConfiguration(config) {
+ configuration = config;
+ }
+
+ var toolInterface = {
+ enable: enable,
+ disable: disable,
+ activate: activate,
+ deactivate: deactivate,
+ getConfiguration: getConfiguration,
+ setConfiguration: setConfiguration,
+ mouseDownCallback: mouseDownCallback,
+ mouseMoveCallback: mouseMoveCallback,
+ mouseDownActivateCallback: mouseDownActivateCallback
+ };
+
+ // Expose pointNearTool if available
+ if (mouseToolInterface.pointNearTool) {
+ toolInterface.pointNearTool = mouseToolInterface.pointNearTool;
+ }
+
+ if (mouseToolInterface.mouseDoubleClickCallback) {
+ toolInterface.mouseDoubleClickCallback = mouseToolInterface.mouseDoubleClickCallback;
+ }
+
+ if (mouseToolInterface.addNewMeasurement) {
+ toolInterface.addNewMeasurement = mouseToolInterface.addNewMeasurement;
+ }
+
+ return toolInterface;
+ }
+
+ // module exports
+ cornerstoneTools.mouseButtonTool = mouseButtonTool;
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/imageTools/mouseButtonTool.js
+
+// Begin Source: src/imageTools/mouseButtonRectangleTool.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ function mouseButtonRectangleTool(mouseToolInterface, preventHandleOutsideImage) {
+ ///////// BEGIN ACTIVE TOOL ///////
+ function addNewMeasurement(mouseEventData) {
+ var measurementData = mouseToolInterface.createNewMeasurement(mouseEventData);
+
+ //prevent adding new measurement if tool returns nill
+ if (!measurementData) {
+ return;
+ }
+
+ // associate this data with this imageId so we can render it and manipulate it
+ cornerstoneTools.addToolState(mouseEventData.element, mouseToolInterface.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', mouseMoveCallback);
+ cornerstoneTools.moveHandle(mouseEventData, measurementData.handles.end, function() {
+ measurementData.active = false;
+ if (cornerstoneTools.anyHandlesOutsideImage(mouseEventData, measurementData.handles)) {
+ // delete the measurement
+ cornerstoneTools.removeToolState(mouseEventData.element, mouseToolInterface.toolType, measurementData);
+ }
+
+ $(mouseEventData.element).on('CornerstoneToolsMouseMove', mouseMoveCallback);
+ }, preventHandleOutsideImage);
+ }
+
+ function mouseDownActivateCallback(e, eventData) {
+ if (cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
+ addNewMeasurement(eventData);
+ return false; // false = cases jquery to preventDefault() and stopPropagation() this event
+ }
+ }
+ ///////// END ACTIVE TOOL ///////
+
+ ///////// BEGIN DEACTIVE TOOL ///////
+
+ function mouseMoveCallback(e, eventData) {
+ cornerstoneTools.toolCoordinates.setCoords(eventData);
+ // if a mouse button is down, do nothing
+ if (eventData.which !== 0) {
+ return;
+ }
+
+ // if we have no tool data for this element, do nothing
+ var toolData = cornerstoneTools.getToolState(eventData.element, mouseToolInterface.toolType);
+ if (toolData === undefined) {
+ return;
+ }
+
+ // We have tool data, search through all data
+ // and see if we can activate a handle
+ var imageNeedsUpdate = false;
+ var coords = eventData.currentPoints.canvas;
+
+ for (var i = 0; i < toolData.data.length; i++) {
+ // get the cursor position in image coordinates
+ var data = toolData.data[i];
+ if (cornerstoneTools.handleActivator(eventData.element, data.handles, coords) === true) {
+ imageNeedsUpdate = true;
+ }
+
+ if ((mouseToolInterface.pointInsideRect(eventData.element, data, coords) && !data.active) || (!mouseToolInterface.pointInsideRect(eventData.element, data, coords) && data.active)) {
+ data.active = !data.active;
+ imageNeedsUpdate = true;
+ }
+ }
+
+ // Handle activation status changed, redraw the image
+ if (imageNeedsUpdate === true) {
+ cornerstone.updateImage(eventData.element);
+ }
+ }
+
+ function mouseDownCallback(e, eventData) {
+ var data;
+
+ function handleDoneMove() {
+ data.active = false;
+ if (cornerstoneTools.anyHandlesOutsideImage(eventData, data.handles)) {
+ // delete the measurement
+ cornerstoneTools.removeToolState(eventData.element, mouseToolInterface.toolType, data);
+ }
+
+ cornerstone.updateImage(eventData.element);
+ $(eventData.element).on('CornerstoneToolsMouseMove', mouseMoveCallback);
+ }
+
+ if (cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
+ var coords = eventData.startPoints.canvas;
+ var toolData = cornerstoneTools.getToolState(e.currentTarget, mouseToolInterface.toolType);
+
+ var i;
+
+ // now check to see if there is a handle we can move
+ if (toolData !== undefined) {
+ for (i = 0; i < toolData.data.length; i++) {
+ data = toolData.data[i];
+ var handle = cornerstoneTools.getHandleNearImagePoint(eventData.element, data, coords);
+ if (handle !== undefined) {
+ $(eventData.element).off('CornerstoneToolsMouseMove', mouseMoveCallback);
+ data.active = true;
+ cornerstoneTools.moveHandle(eventData, handle, handleDoneMove, preventHandleOutsideImage);
+ e.stopImmediatePropagation();
+ return false;
+ }
+ }
+ }
+
+ // 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
+ 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);
+ $(eventData.element).on('CornerstoneToolsMouseMove', mouseMoveCallback);
+ e.stopImmediatePropagation();
+ return false;
+ }
+ }
+ }
+ }
+ }
+ ///////// END DEACTIVE TOOL ///////
+
+ // not visible, not interactive
+ function disable(element) {
+ $(element).off('CornerstoneImageRendered', mouseToolInterface.onImageRendered);
+ $(element).off('CornerstoneToolsMouseMove', mouseMoveCallback);
+ $(element).off('CornerstoneToolsMouseDown', mouseDownCallback);
+ $(element).off('CornerstoneToolsMouseDownActivate', mouseDownActivateCallback);
+
+ cornerstone.updateImage(element);
+ }
+
+ // visible but not interactive
+ function enable(element) {
+ $(element).off('CornerstoneImageRendered', mouseToolInterface.onImageRendered);
+ $(element).off('CornerstoneToolsMouseMove', mouseMoveCallback);
+ $(element).off('CornerstoneToolsMouseDown', mouseDownCallback);
+ $(element).off('CornerstoneToolsMouseDownActivate', mouseDownActivateCallback);
+
+ $(element).on('CornerstoneImageRendered', mouseToolInterface.onImageRendered);
+
+ cornerstone.updateImage(element);
+ }
+
+ // visible, interactive and can create
+ function activate(element, mouseButtonMask) {
+ var eventData = {
+ mouseButtonMask: mouseButtonMask,
+ };
+
+ $(element).off('CornerstoneImageRendered', mouseToolInterface.onImageRendered);
+ $(element).off('CornerstoneToolsMouseMove', mouseMoveCallback);
+ $(element).off('CornerstoneToolsMouseDown', mouseDownCallback);
+ $(element).off('CornerstoneToolsMouseDownActivate', mouseDownActivateCallback);
+
+ $(element).on('CornerstoneImageRendered', mouseToolInterface.onImageRendered);
+ $(element).on('CornerstoneToolsMouseMove', eventData, mouseMoveCallback);
+ $(element).on('CornerstoneToolsMouseDown', eventData, mouseDownCallback);
+ $(element).on('CornerstoneToolsMouseDownActivate', eventData, mouseDownActivateCallback);
+
+ cornerstone.updateImage(element);
+ }
+
+ // visible, interactive
+ function deactivate(element, mouseButtonMask) {
+ var eventData = {
+ mouseButtonMask: mouseButtonMask,
+ };
+
+ $(element).off('CornerstoneImageRendered', mouseToolInterface.onImageRendered);
+ $(element).off('CornerstoneToolsMouseMove', mouseMoveCallback);
+ $(element).off('CornerstoneToolsMouseDown', mouseDownCallback);
+ $(element).off('CornerstoneToolsMouseDownActivate', mouseDownActivateCallback);
+
+ $(element).on('CornerstoneImageRendered', mouseToolInterface.onImageRendered);
+ $(element).on('CornerstoneToolsMouseMove', eventData, mouseMoveCallback);
+ $(element).on('CornerstoneToolsMouseDown', eventData, mouseDownCallback);
+
+ cornerstone.updateImage(element);
+ }
+
+ var toolInterface = {
+ enable: enable,
+ disable: disable,
+ activate: activate,
+ deactivate: deactivate
+ };
+
+ return toolInterface;
+ }
+
+ // module exports
+ cornerstoneTools.mouseButtonRectangleTool = mouseButtonRectangleTool;
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/imageTools/mouseButtonRectangleTool.js
+
+// Begin Source: src/imageTools/mouseWheelTool.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function mouseWheelTool(mouseWheelCallback) {
+ var toolInterface = {
+ activate: function(element) {
+ $(element).off('CornerstoneToolsMouseWheel', mouseWheelCallback);
+ var eventData = {
+ };
+ $(element).on('CornerstoneToolsMouseWheel', eventData, mouseWheelCallback);
+ },
+ disable: function(element) {$(element).off('CornerstoneToolsMouseWheel', mouseWheelCallback);},
+ enable: function(element) {$(element).off('CornerstoneToolsMouseWheel', mouseWheelCallback);},
+ deactivate: function(element) {$(element).off('CornerstoneToolsMouseWheel', mouseWheelCallback);}
+ };
+ return toolInterface;
+ }
+
+ // module exports
+ cornerstoneTools.mouseWheelTool = mouseWheelTool;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/mouseWheelTool.js
+
+// Begin Source: src/imageTools/touchDragTool.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function touchDragTool(touchDragCallback, options) {
+ var events = 'CornerstoneToolsTouchDrag';
+ if (options && options.fireOnTouchStart === true) {
+ events += ' CornerstoneToolsTouchStart';
+ }
+
+ var toolInterface = {
+ activate: function(element) {
+ $(element).off(events, touchDragCallback);
+
+ if (options && options.eventData) {
+ $(element).on(events, options.eventData, touchDragCallback);
+ } else {
+ $(element).on(events, touchDragCallback);
+ }
+
+ if (options && options.activateCallback) {
+ options.activateCallback(element);
+ }
+ }, disable: function(element) {
+ $(element).off(events, touchDragCallback);
+ if (options && options.disableCallback) {
+ options.disableCallback(element);
+ }
+ }, enable: function(element) {
+ $(element).off(events, touchDragCallback);
+ if (options && options.enableCallback) {
+ options.enableCallback(element);
+ }
+ }, deactivate: function(element) {
+ $(element).off(events, touchDragCallback);
+ if (options && options.deactivateCallback) {
+ options.deactivateCallback(element);
+ }
+ }
+ };
+ return toolInterface;
+ }
+
+ // module exports
+ cornerstoneTools.touchDragTool = touchDragTool;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/touchDragTool.js
+
+// Begin Source: src/imageTools/touchPinchTool.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ /*jshint newcap: false */
+
+ function touchPinchTool(touchPinchCallback) {
+ var toolInterface = {
+ activate: function(element) {
+ $(element).off('CornerstoneToolsTouchPinch', touchPinchCallback);
+ var eventData = {
+ };
+ $(element).on('CornerstoneToolsTouchPinch', eventData, touchPinchCallback);
+ },
+ disable: function(element) {$(element).off('CornerstoneToolsTouchPinch', touchPinchCallback);},
+ enable: function(element) {$(element).off('CornerstoneToolsTouchPinch', touchPinchCallback);},
+ deactivate: function(element) {$(element).off('CornerstoneToolsTouchPinch', touchPinchCallback);}
+ };
+ return toolInterface;
+ }
+
+ // module exports
+ cornerstoneTools.touchPinchTool = touchPinchTool;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/touchPinchTool.js
+
+// Begin Source: src/imageTools/touchTool.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ function deactivateAllHandles(handles) {
+ Object.keys(handles).forEach(function(name) {
+ var handle = handles[name];
+ handle.active = false;
+ });
+ }
+
+ function deactivateAllToolInstances(toolData) {
+ if (!toolData) {
+ return;
+ }
+
+ for (var i = 0; i < toolData.data.length; i++) {
+ var data = toolData.data[i];
+ data.active = false;
+ if (!data.handles) {
+ continue;
+ }
+
+ deactivateAllHandles(data.handles);
+ }
+ }
+
+ function touchTool(touchToolInterface) {
+ ///////// BEGIN ACTIVE TOOL ///////
+
+ function addNewMeasurement(touchEventData) {
+ var element = touchEventData.element;
+
+ var measurementData = touchToolInterface.createNewMeasurement(touchEventData);
+ if (!measurementData) {
+ return;
+ }
+
+ cornerstoneTools.addToolState(element, touchToolInterface.toolType, measurementData);
+
+ if (Object.keys(measurementData.handles).length === 1 && touchEventData.type === 'CornerstoneToolsTap') {
+ measurementData.active = false;
+ measurementData.handles.end.active = false;
+ measurementData.handles.end.highlight = false;
+ measurementData.invalidated = true;
+ if (cornerstoneTools.anyHandlesOutsideImage(touchEventData, measurementData.handles)) {
+ // delete the measurement
+ cornerstoneTools.removeToolState(element, touchToolInterface.toolType, measurementData);
+ }
+
+ cornerstone.updateImage(element);
+ return;
+ }
+
+ $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback);
+ $(element).off('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback);
+ $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback);
+
+ cornerstone.updateImage(element);
+ cornerstoneTools.moveNewHandleTouch(touchEventData, measurementData.handles.end, function() {
+ measurementData.active = false;
+ measurementData.invalidated = true;
+ if (cornerstoneTools.anyHandlesOutsideImage(touchEventData, measurementData.handles)) {
+ // delete the measurement
+ cornerstoneTools.removeToolState(element, touchToolInterface.toolType, measurementData);
+ }
+
+ $(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) {
+ if (touchToolInterface.addNewMeasurement) {
+ touchToolInterface.addNewMeasurement(eventData);
+ } else {
+ addNewMeasurement(eventData);
+ }
+
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+ ///////// 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) {
+ var element = eventData.element;
+ var coords = eventData.currentPoints.canvas;
+ var toolData = cornerstoneTools.getToolState(e.currentTarget, touchToolInterface.toolType);
+ var data;
+ var i;
+
+ // Deactivate everything
+ deactivateAllToolInstances(toolData);
+
+ function doneMovingCallback() {
+ deactivateAllToolInstances(toolData);
+ if (cornerstoneTools.anyHandlesOutsideImage(eventData, data.handles)) {
+ // delete the measurement
+ cornerstoneTools.removeToolState(element, touchToolInterface.toolType, data);
+ }
+
+ cornerstone.updateImage(element);
+ $(element).on('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback);
+ $(element).on('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback);
+ $(element).on('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback);
+ }
+
+ // now check to see if there is a handle we can move
+ if (toolData) {
+ 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);
+ if (handle) {
+ $(element).off('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback);
+ $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback);
+ $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback);
+ data.active = true;
+ handle.active = true;
+ cornerstone.updateImage(element);
+ cornerstoneTools.touchMoveHandle(e, handle, doneMovingCallback);
+ e.stopImmediatePropagation();
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+ }
+ }
+
+ // Now check to see if we have a tool that we can move
+ if (toolData && touchToolInterface.pointNearTool) {
+ 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;
+ cornerstone.updateImage(element);
+ cornerstoneTools.touchMoveAllHandles(e, data, toolData, true, doneMovingCallback);
+ e.stopImmediatePropagation();
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+ }
+ }
+
+ // If there is nothing to move, add a new instance of the tool
+ // Need to check here to see if activation is allowed!
+ if (touchToolInterface.touchDownActivateCallback) {
+ touchToolInterface.touchDownActivateCallback(e, eventData);
+ } else {
+ touchDownActivateCallback(e, eventData);
+ }
+
+ return false;
+ }
+
+ function touchStartCallback(e, eventData) {
+ var element = eventData.element;
+ var coords = eventData.startPoints.canvas;
+ var data;
+ var toolData = cornerstoneTools.getToolState(e.currentTarget, touchToolInterface.toolType);
+ var i;
+
+ function doneMovingCallback() {
+ data.active = false;
+ data.invalidated = true;
+ if (cornerstoneTools.anyHandlesOutsideImage(eventData, data.handles)) {
+ // delete the measurement
+ cornerstoneTools.removeToolState(eventData.element, touchToolInterface.toolType, data);
+ }
+
+ cornerstone.updateImage(eventData.element);
+ $(element).on('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback);
+ $(element).on('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback);
+ $(element).on('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback);
+ }
+
+ // now check to see if there is a handle we can move
+ var distanceFromTouch = cornerstoneTools.touchSettings.getToolDistanceFromTouch();
+ var distanceSq = Math.pow(Math.max(Math.abs(distanceFromTouch.x), Math.abs(distanceFromTouch.y)), 2);
+ if (toolData) {
+ for (i = 0; i < toolData.data.length; i++) {
+ data = toolData.data[i];
+
+ var handle = cornerstoneTools.getHandleNearImagePoint(eventData.element, data, 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;
+ cornerstoneTools.touchMoveHandle(e, handle, doneMovingCallback);
+ e.stopImmediatePropagation();
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+ }
+ }
+
+ // Now check to see if we have a tool that we can move
+ if (toolData && touchToolInterface.pointNearTool) {
+ 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
+ }
+ }
+ }
+ }
+ ///////// END INACTIVE TOOL ///////
+
+ // 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);
+
+ if (touchToolInterface.doubleTapCallback) {
+ $(element).off('CornerstoneToolsDoubleTap', touchToolInterface.doubleTapCallback);
+ }
+
+ if (touchToolInterface.pressCallback) {
+ $(element).off('CornerstoneToolsTouchPress', touchToolInterface.pressCallback);
+ }
+
+ cornerstone.updateImage(element);
+ }
+
+ // 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);
+
+ $(element).on('CornerstoneImageRendered', touchToolInterface.onImageRendered);
+
+ if (touchToolInterface.doubleTapCallback) {
+ $(element).off('CornerstoneToolsDoubleTap', touchToolInterface.doubleTapCallback);
+ }
+
+ if (touchToolInterface.pressCallback) {
+ $(element).off('CornerstoneToolsTouchPress', touchToolInterface.pressCallback);
+ }
+
+ cornerstone.updateImage(element);
+ }
+
+ // visible, interactive and can create
+ function activate(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);
+
+ $(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);
+
+ if (touchToolInterface.doubleTapCallback) {
+ $(element).off('CornerstoneToolsDoubleTap', touchToolInterface.doubleTapCallback);
+ $(element).on('CornerstoneToolsDoubleTap', touchToolInterface.doubleTapCallback);
+ }
+
+ if (touchToolInterface.pressCallback) {
+ $(element).off('CornerstoneToolsTouchPress', touchToolInterface.pressCallback);
+ $(element).on('CornerstoneToolsTouchPress', touchToolInterface.pressCallback);
+ }
+
+ cornerstone.updateImage(element);
+ }
+
+ // visible, interactive
+ function deactivate(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);
+
+ $(element).on('CornerstoneImageRendered', touchToolInterface.onImageRendered);
+ $(element).on('CornerstoneToolsTouchDrag', touchToolInterface.touchMoveCallback || touchMoveCallback);
+ //$(element).on('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback);
+
+ if (touchToolInterface.doubleTapCallback) {
+ $(element).off('CornerstoneToolsDoubleTap', touchToolInterface.doubleTapCallback);
+ $(element).on('CornerstoneToolsDoubleTap', touchToolInterface.doubleTapCallback);
+ }
+
+ if (touchToolInterface.pressCallback) {
+ $(element).off('CornerstoneToolsTouchPress', touchToolInterface.pressCallback);
+ $(element).on('CornerstoneToolsTouchPress', touchToolInterface.pressCallback);
+ }
+
+ cornerstone.updateImage(element);
+ }
+
+ var toolInterface = {
+ enable: enable,
+ disable: disable,
+ activate: activate,
+ deactivate: deactivate,
+ touchStartCallback: touchToolInterface.touchStartCallback || touchStartCallback,
+ touchMoveCallback: touchToolInterface.touchMoveCallback || touchMoveCallback,
+ touchDownActivateCallback: touchToolInterface.touchDownActivateCallback || touchDownActivateCallback,
+ tapCallback: touchToolInterface.tapCallback || tapCallback
+ };
+
+ if (touchToolInterface.doubleTapCallback) {
+ toolInterface.doubleTapCallback = touchToolInterface.doubleTapCallback;
+ }
+
+ if (touchToolInterface.pressCallback) {
+ toolInterface.pressCallback = touchToolInterface.pressCallback;
+ }
+
+ if (touchToolInterface.addNewMeasurement) {
+ toolInterface.addNewMeasurement = touchToolInterface.addNewMeasurement;
+ }
+
+ return toolInterface;
+ }
+
+ // module exports
+ cornerstoneTools.touchTool = touchTool;
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/imageTools/touchTool.js
+
+// Begin Source: src/imageTools/AngleTool.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ var toolType = 'angle';
+
+ ///////// BEGIN ACTIVE TOOL ///////
+ function createNewMeasurement(mouseEventData) {
+ // create the measurement data for this tool with the end handle activated
+ var angleData = {
+ visible: true,
+ active: true,
+ handles: {
+ start: {
+ x: mouseEventData.currentPoints.image.x - 20,
+ y: mouseEventData.currentPoints.image.y + 10,
+ highlight: true,
+ active: false
+ },
+ end: {
+ x: mouseEventData.currentPoints.image.x,
+ y: mouseEventData.currentPoints.image.y,
+ highlight: true,
+ active: true
+ },
+ start2: {
+ x: mouseEventData.currentPoints.image.x - 20,
+ y: mouseEventData.currentPoints.image.y + 10,
+ highlight: true,
+ active: false
+ },
+ end2: {
+ x: mouseEventData.currentPoints.image.x,
+ y: mouseEventData.currentPoints.image.y + 20,
+ highlight: true,
+ active: false
+ }
+ }
+ };
+
+ return angleData;
+ }
+ ///////// 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 (distanceToPoint < 5) {
+ return true;
+ }
+
+ lineSegment.start = cornerstone.pixelToCanvas(element, data.handles.start2);
+ lineSegment.end = cornerstone.pixelToCanvas(element, data.handles.end2);
+
+ distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords);
+ return (distanceToPoint < 5);
+ }
+
+ ///////// BEGIN IMAGE RENDERING ///////
+ function onImageRendered(e, eventData) {
+
+ // 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;
+ }
+
+ // 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);
+
+ //activation color
+ var color;
+ var lineWidth = cornerstoneTools.toolStyle.getToolWidth();
+ var font = cornerstoneTools.textStyle.getFont();
+ var config = cornerstoneTools.angle.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];
+
+ //differentiate the color of activation tool
+ if (data.active) {
+ color = cornerstoneTools.toolColors.getActiveColor();
+ } else {
+ color = cornerstoneTools.toolColors.getToolColor();
+ }
+
+ // draw the line
+ context.beginPath();
+ context.strokeStyle = color;
+ context.lineWidth = lineWidth;
+
+ var handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.start);
+ var handleEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.end);
+
+ context.moveTo(handleStartCanvas.x, handleStartCanvas.y);
+ context.lineTo(handleEndCanvas.x, handleEndCanvas.y);
+
+ handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.start2);
+ handleEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.end2);
+
+ context.moveTo(handleStartCanvas.x, handleStartCanvas.y);
+ context.lineTo(handleEndCanvas.x, handleEndCanvas.y);
+ context.stroke();
+
+ // draw the handles
+ cornerstoneTools.drawHandles(context, eventData, data.handles);
+
+ // Draw the text
+ context.fillStyle = color;
+
+ // Need to work on correct angle to measure. This is a cobb angle and we need to determine
+ // where lines cross to measure angle. For now it will show smallest angle.
+ var dx1 = (Math.ceil(data.handles.start.x) - Math.ceil(data.handles.end.x)) * eventData.image.columnPixelSpacing;
+ var dy1 = (Math.ceil(data.handles.start.y) - Math.ceil(data.handles.end.y)) * eventData.image.rowPixelSpacing;
+ var dx2 = (Math.ceil(data.handles.start2.x) - Math.ceil(data.handles.end2.x)) * eventData.image.columnPixelSpacing;
+ var dy2 = (Math.ceil(data.handles.start2.y) - Math.ceil(data.handles.end2.y)) * eventData.image.rowPixelSpacing;
+
+ var angle = Math.acos(Math.abs(((dx1 * dx2) + (dy1 * dy2)) / (Math.sqrt((dx1 * dx1) + (dy1 * dy1)) * Math.sqrt((dx2 * dx2) + (dy2 * dy2)))));
+ angle = angle * (180 / Math.PI);
+
+ var rAngle = cornerstoneTools.roundToDecimal(angle, 2);
+ var str = '00B0'; // degrees symbol
+ var text = rAngle.toString() + String.fromCharCode(parseInt(str, 16));
+
+ var textX = (handleStartCanvas.x + handleEndCanvas.x) / 2;
+ var textY = (handleStartCanvas.y + handleEndCanvas.y) / 2;
+
+ context.font = font;
+ cornerstoneTools.drawTextBox(context, text, textX, textY, color);
+ context.restore();
+ }
+ }
+ ///////// END IMAGE RENDERING ///////
+
+ // module exports
+ cornerstoneTools.angle = cornerstoneTools.mouseButtonTool({
+ createNewMeasurement: createNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ toolType: toolType
+ });
+
+ cornerstoneTools.angleTouch = cornerstoneTools.touchTool({
+ createNewMeasurement: createNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ toolType: toolType
+ });
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/imageTools/AngleTool.js
+
+// Begin Source: src/imageTools/annotation.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ var toolType = 'arrowAnnotate';
+
+ // Define a callback to get your text annotation
+ // This could be used, e.g. to open a modal
+ function getTextCallback(doneChangingTextCallback) {
+ doneChangingTextCallback(prompt('Enter your annotation:'));
+ }
+
+ function changeTextCallback(data, doneChangingTextCallback) {
+ doneChangingTextCallback(prompt('Change your annotation:'));
+ }
+
+ var configuration = {
+ getTextCallback: getTextCallback,
+ changeTextCallback: changeTextCallback,
+ drawHandles: false,
+ drawHandlesOnHover: true,
+ arrowFirst: true
+ };
+
+ /// --- Mouse Tool --- ///
+
+ ///////// BEGIN ACTIVE TOOL ///////
+ function addNewMeasurement(mouseEventData) {
+
+ function doneChangingTextCallback(text) {
+ if (text !== null) {
+ measurementData.text = text;
+ } else {
+ cornerstoneTools.removeToolState(mouseEventData.element, toolType, measurementData);
+ }
+
+ measurementData.active = false;
+ cornerstone.updateImage(mouseEventData.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.arrowAnnotate.mouseMoveCallback);
+ $(mouseEventData.element).off('CornerstoneToolsMouseDown', cornerstoneTools.arrowAnnotate.mouseDownCallback);
+ $(mouseEventData.element).off('CornerstoneToolsMouseDownActivate', cornerstoneTools.arrowAnnotate.mouseDownActivateCallback);
+
+ cornerstone.updateImage(mouseEventData.element);
+ cornerstoneTools.moveNewHandle(mouseEventData, measurementData.handles.end, function() {
+ if (cornerstoneTools.anyHandlesOutsideImage(mouseEventData, measurementData.handles)) {
+ // delete the measurement
+ cornerstoneTools.removeToolState(mouseEventData.element, toolType, measurementData);
+ }
+
+ var config = cornerstoneTools.arrowAnnotate.getConfiguration();
+ if (measurementData.text === undefined) {
+ config.getTextCallback(doneChangingTextCallback);
+ }
+
+ $(mouseEventData.element).on('CornerstoneToolsMouseMove', eventData, cornerstoneTools.arrowAnnotate.mouseMoveCallback);
+ $(mouseEventData.element).on('CornerstoneToolsMouseDown', eventData, cornerstoneTools.arrowAnnotate.mouseDownCallback);
+ $(mouseEventData.element).on('CornerstoneToolsMouseDownActivate', eventData, cornerstoneTools.arrowAnnotate.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
+ }
+ }
+ };
+
+ 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);
+ return (distanceToPoint < 25);
+ }
+
+ 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();
+ }
+
+ ///////// BEGIN IMAGE RENDERING ///////
+ function onImageRendered(e, eventData) {
+ // 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;
+ }
+
+ // 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.arrowAnnotate.getConfiguration();
+
+ for (var i = 0; i < toolData.data.length; i++) {
+ context.save();
+
+ var data = toolData.data[i];
+
+ if (data.active) {
+ color = cornerstoneTools.toolColors.getActiveColor();
+ } else {
+ color = cornerstoneTools.toolColors.getToolColor();
+ }
+
+ // Draw the arrow
+ var handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.start);
+ var handleEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.end);
+
+ if (config.arrowFirst) {
+ drawArrow(context, handleEndCanvas, handleStartCanvas, color, lineWidth);
+ } else {
+ drawArrow(context, handleStartCanvas, handleEndCanvas, color, lineWidth);
+ }
+
+ if (config.drawHandles) {
+ cornerstoneTools.drawHandles(context, eventData, data.handles, color);
+ } else if (config.drawHandlesOnHover && data.handles.start.active) {
+ cornerstoneTools.drawHandles(context, eventData, [ data.handles.start ], color);
+ } else if (config.drawHandlesOnHover && data.handles.end.active) {
+ cornerstoneTools.drawHandles(context, eventData, [ data.handles.end ], color);
+ }
+
+ // Draw the text
+ if (data.text && data.text !== '') {
+ context.font = font;
+
+ var distance = 13;
+
+ // TODO: add 2 dimensional vector operations to cornerstoneMath
+ var vector;
+
+ var displacement = {
+ x: distance,
+ y: distance / 2
+ };
+
+ vector = {
+ x: handleEndCanvas.x - handleStartCanvas.x,
+ y: handleEndCanvas.y - handleStartCanvas.y
+ };
+
+ 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;
+ }
+
+ textCoords = {
+ x: vector.x + handleStartCanvas.x + displacement.x,
+ y: vector.y + handleStartCanvas.y + displacement.y
+ };
+ } else {
+ // Fix text placement if arrow faces right
+ if (vector.x > 0) {
+ displacement.x = -displacement.x - context.measureText(data.text).width;
+ }
+
+ textCoords = {
+ x: -vector.x + handleEndCanvas.x + displacement.x,
+ y: -vector.y + handleEndCanvas.y + displacement.y
+ };
+ }
+
+ cornerstoneTools.drawTextBox(context, data.text, textCoords.x, textCoords.y, color);
+ }
+
+ context.restore();
+ }
+ }
+ // ---- Touch tool ----
+
+ ///////// BEGIN ACTIVE TOOL ///////
+ function addNewMeasurementTouch(touchEventData) {
+ var element = touchEventData.element;
+
+ function doneChangingTextCallback(text) {
+ if (text !== null) {
+ measurementData.text = text;
+ } else {
+ cornerstoneTools.removeToolState(element, toolType, measurementData);
+ }
+
+ measurementData.active = false;
+ cornerstone.updateImage(element);
+ }
+
+ var measurementData = createNewMeasurement(touchEventData);
+ cornerstoneTools.addToolState(element, toolType, measurementData);
+ $(element).off('CornerstoneToolsTouchDrag', cornerstoneTools.arrowAnnotateTouch.touchMoveCallback);
+ $(element).off('CornerstoneToolsDragStartActive', cornerstoneTools.arrowAnnotateTouch.touchDownActivateCallback);
+ $(element).off('CornerstoneToolsTap', cornerstoneTools.arrowAnnotateTouch.tapCallback);
+ 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);
+ }
+
+ var config = cornerstoneTools.arrowAnnotate.getConfiguration();
+ if (measurementData.text === undefined) {
+ config.getTextCallback(doneChangingTextCallback);
+ }
+
+ $(element).on('CornerstoneToolsTouchDrag', cornerstoneTools.arrowAnnotateTouch.touchMoveCallback);
+ $(element).on('CornerstoneToolsDragStartActive', cornerstoneTools.arrowAnnotateTouch.touchDownActivateCallback);
+ $(element).on('CornerstoneToolsTap', cornerstoneTools.arrowAnnotateTouch.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.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
+ 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,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ toolType: toolType,
+ mouseDoubleClickCallback: doubleClickCallback
+ });
+
+ cornerstoneTools.arrowAnnotate.setConfiguration(configuration);
+
+ cornerstoneTools.arrowAnnotateTouch = cornerstoneTools.touchTool({
+ addNewMeasurement: addNewMeasurementTouch,
+ createNewMeasurement: createNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ toolType: toolType,
+ pressCallback: doubleClickCallback
+ });
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/imageTools/annotation.js
+
+// Begin Source: src/imageTools/crosshairs.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ var toolType = 'crosshairs';
+
+ function chooseLocation(e, eventData) {
+ // 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;
+ }
+
+ // Get current element target information
+ var sourceElement = e.currentTarget;
+ var sourceEnabledElement = cornerstone.getEnabledElement(sourceElement);
+ var sourceImageId = sourceEnabledElement.image.imageId;
+ var sourceImagePlane = cornerstoneTools.metaData.get('imagePlane', sourceImageId);
+
+ // Get currentPoints from mouse cursor on selected element
+ var sourceImagePoint = eventData.currentPoints.image;
+
+ // Transfer this to a patientPoint given imagePlane metadata
+ var patientPoint = cornerstoneTools.imagePointToPatientPoint(sourceImagePoint, sourceImagePlane);
+
+ // Get the enabled elements associated with this synchronization context
+ var syncContext = toolData.data[0].synchronizationContext;
+ var enabledElements = syncContext.getSourceElements();
+
+ // Iterate over each synchronized element
+ $.each(enabledElements, function(index, targetElement) {
+ // don't do anything if the target is the same as the source
+ if (targetElement === sourceElement) {
+ return; // Same as 'continue' in a normal for loop
+ }
+
+ var minDistance = Number.MAX_VALUE;
+ var newImageIdIndex = -1;
+
+ var stackToolDataSource = cornerstoneTools.getToolState(targetElement, 'stack');
+ if (stackToolDataSource === undefined) {
+ return; // Same as 'continue' in a normal for loop
+ }
+
+ var stackData = stackToolDataSource.data[0];
+
+ // Find within the element's stack the closest image plane to selected location
+ $.each(stackData.imageIds, function(index, imageId) {
+ var imagePlane = cornerstoneTools.metaData.get('imagePlane', imageId);
+ var imagePosition = imagePlane.imagePositionPatient;
+ var row = imagePlane.rowCosines.clone();
+ var column = imagePlane.columnCosines.clone();
+ var normal = column.clone().cross(row.clone());
+ var distance = Math.abs(normal.clone().dot(imagePosition) - normal.clone().dot(patientPoint));
+ //console.log(index + '=' + distance);
+ if (distance < minDistance) {
+ minDistance = distance;
+ newImageIdIndex = index;
+ }
+ });
+
+ if (newImageIdIndex === stackData.currentImageIdIndex) {
+ return;
+ }
+
+ // Switch the loaded image to the required image
+ if (newImageIdIndex !== -1 && stackData.imageIds[newImageIdIndex] !== undefined) {
+ var startLoadingHandler = cornerstoneTools.loadHandlerManager.getStartLoadHandler();
+ var endLoadingHandler = cornerstoneTools.loadHandlerManager.getEndLoadHandler();
+ var errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler();
+
+ if (startLoadingHandler) {
+ startLoadingHandler(targetElement);
+ }
+
+ cornerstone.loadAndCacheImage(stackData.imageIds[newImageIdIndex]).then(function(image) {
+ var viewport = cornerstone.getViewport(targetElement);
+ stackData.currentImageIdIndex = newImageIdIndex;
+ cornerstone.displayImage(targetElement, image, viewport);
+ if (endLoadingHandler) {
+ endLoadingHandler(targetElement);
+ }
+ }, function(error) {
+ var imageId = stackData.imageIds[newImageIdIndex];
+ if (errorLoadingHandler) {
+ errorLoadingHandler(targetElement, imageId, error);
+ }
+ });
+ }
+ });
+ }
+
+ function mouseUpCallback(e, eventData) {
+ $(eventData.element).off('CornerstoneToolsMouseDrag', mouseDragCallback);
+ $(eventData.element).off('CornerstoneToolsMouseUp', mouseUpCallback);
+ }
+
+ function mouseDownCallback(e, eventData) {
+ if (cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
+ $(eventData.element).on('CornerstoneToolsMouseDrag', mouseDragCallback);
+ $(eventData.element).on('CornerstoneToolsMouseUp', mouseUpCallback);
+ chooseLocation(e, eventData);
+ return false; // false = cases jquery to preventDefault() and stopPropagation() this event
+ }
+ }
+
+ function mouseDragCallback(e, eventData) {
+ chooseLocation(e, eventData);
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+
+ function enable(element, mouseButtonMask, synchronizationContext) {
+ var eventData = {
+ mouseButtonMask: mouseButtonMask,
+ };
+
+ // Clear any currently existing toolData
+ var toolData = cornerstoneTools.getToolState(element, toolType);
+ toolData = [];
+
+ cornerstoneTools.addToolState(element, toolType, {
+ synchronizationContext: synchronizationContext,
+ });
+
+ $(element).off('CornerstoneToolsMouseDown', mouseDownCallback);
+
+ $(element).on('CornerstoneToolsMouseDown', eventData, mouseDownCallback);
+ }
+
+ // disables the reference line tool for the given element
+ function disable(element) {
+ $(element).off('CornerstoneToolsMouseDown', mouseDownCallback);
+ }
+
+ // module/private exports
+ cornerstoneTools.crosshairs = {
+ activate: enable,
+ deactivate: disable,
+ enable: enable,
+ disable: disable
+ };
+
+ function dragEndCallback(e, eventData) {
+ $(eventData.element).off('CornerstoneToolsTouchDrag', dragCallback);
+ $(eventData.element).off('CornerstoneToolsDragEnd', dragEndCallback);
+ }
+
+ function dragStartCallback(e, eventData) {
+ $(eventData.element).on('CornerstoneToolsTouchDrag', dragCallback);
+ $(eventData.element).on('CornerstoneToolsDragEnd', dragEndCallback);
+ chooseLocation(e, eventData);
+ return false;
+ }
+
+ function dragCallback(e, eventData) {
+ chooseLocation(e, eventData);
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+
+ function enableTouch(element, synchronizationContext) {
+ // Clear any currently existing toolData
+ var toolData = cornerstoneTools.getToolState(element, toolType);
+ toolData = [];
+
+ cornerstoneTools.addToolState(element, toolType, {
+ synchronizationContext: synchronizationContext,
+ });
+
+ $(element).off('CornerstoneToolsTouchStart', dragStartCallback);
+
+ $(element).on('CornerstoneToolsTouchStart', dragStartCallback);
+ }
+
+ // disables the reference line tool for the given element
+ function disableTouch(element) {
+ $(element).off('CornerstoneToolsTouchStart', dragStartCallback);
+ }
+
+ cornerstoneTools.crosshairsTouch = {
+ activate: enableTouch,
+ deactivate: disableTouch,
+ enable: enableTouch,
+ disable: disableTouch
+ };
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/crosshairs.js
+
+// Begin Source: src/imageTools/displayTool.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function displayTool(onImageRendered) {
+ var configuration = {};
+
+ var toolInterface = {
+ disable: function(element) {$(element).off('CornerstoneImageRendered', onImageRendered);},
+ enable: function(element) {
+ $(element).off('CornerstoneImageRendered', onImageRendered);
+ $(element).on('CornerstoneImageRendered', onImageRendered);
+ cornerstone.updateImage(element);
+ },
+ getConfiguration: function() { return configuration; },
+ setConfiguration: function(config) {configuration = config;}
+ };
+
+ return toolInterface;
+ }
+
+ // module exports
+ cornerstoneTools.displayTool = displayTool;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/displayTool.js
+
+// Begin Source: src/imageTools/dragProbe.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function defaultStrategy(eventData) {
+ var enabledElement = cornerstone.getEnabledElement(eventData.element);
+
+ cornerstone.updateImage(eventData.element);
+
+ var context = enabledElement.canvas.getContext('2d');
+ context.setTransform(1, 0, 0, 1, 0, 0);
+
+ var color = cornerstoneTools.toolColors.getActiveColor();
+ var font = cornerstoneTools.textStyle.getFont();
+ var fontHeight = cornerstoneTools.textStyle.getFontSize();
+
+ context.save();
+
+ var x = Math.round(eventData.currentPoints.image.x);
+ var y = Math.round(eventData.currentPoints.image.y);
+
+ var storedPixels;
+ var text,
+ str;
+
+ if (x < 0 || y < 0 || x >= eventData.image.columns || y >= eventData.image.rows) {
+ return;
+ }
+
+ if (eventData.image.color) {
+ storedPixels = cornerstoneTools.getRGBPixels(eventData.element, x, y, 1, 1);
+ text = '' + x + ', ' + y;
+ str = 'R: ' + storedPixels[0] + ' G: ' + storedPixels[1] + ' B: ' + storedPixels[2] + ' A: ' + storedPixels[3];
+ } else {
+ storedPixels = cornerstone.getStoredPixels(eventData.element, x, y, 1, 1);
+ var sp = storedPixels[0];
+ var mo = sp * eventData.image.slope + eventData.image.intercept;
+ var suv = cornerstoneTools.calculateSUV(eventData.image, sp);
+
+ // Draw text
+ text = '' + x + ', ' + y;
+ str = 'SP: ' + sp + ' MO: ' + parseFloat(mo.toFixed(3));
+ if (suv) {
+ str += ' SUV: ' + parseFloat(suv.toFixed(3));
+ }
+ }
+
+ // Draw text
+ var coords = {
+ // translate the x/y away from the cursor
+ x: eventData.currentPoints.image.x + 3,
+ y: eventData.currentPoints.image.y - 3
+ };
+ var textCoords = cornerstone.pixelToCanvas(eventData.element, coords);
+
+ context.font = font;
+ context.fillStyle = color;
+
+ cornerstoneTools.drawTextBox(context, str, textCoords.x, textCoords.y + fontHeight + 5, color);
+ cornerstoneTools.drawTextBox(context, text, textCoords.x, textCoords.y, color);
+ context.restore();
+ }
+
+ function minimalStrategy(eventData) {
+ var element = eventData.element;
+ var enabledElement = cornerstone.getEnabledElement(element);
+ var image = enabledElement.image;
+
+ cornerstone.updateImage(element);
+
+ var context = enabledElement.canvas.getContext('2d');
+ context.setTransform(1, 0, 0, 1, 0, 0);
+
+ var color = cornerstoneTools.toolColors.getActiveColor();
+ var font = cornerstoneTools.textStyle.getFont();
+
+ context.save();
+
+ 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
+ };
+ } else {
+ toolCoords = eventData.currentPoints.image;
+ }
+
+ var storedPixels;
+ var text;
+
+ if (toolCoords.x < 0 || toolCoords.y < 0 ||
+ toolCoords.x >= image.columns || toolCoords.y >= image.rows) {
+ return;
+ }
+
+ if (image.color) {
+ storedPixels = cornerstone.getStoredPixels(element, toolCoords.x, toolCoords.y, 3, 1);
+ text = 'R: ' + storedPixels[0] + ' G: ' + storedPixels[1] + ' B: ' + storedPixels[2];
+ } else {
+ storedPixels = cornerstone.getStoredPixels(element, toolCoords.x, toolCoords.y, 1, 1);
+ var huValue = storedPixels[0] * image.slope + image.intercept;
+ text = parseFloat(huValue.toFixed(3));
+ }
+
+ // Prepare text
+ var textCoords = cornerstone.pixelToCanvas(element, toolCoords);
+ context.font = font;
+ context.fillStyle = color;
+
+ // Translate the x/y away from the cursor
+ var translation;
+ if (eventData.isTouchEvent === true) {
+ var width = context.measureText(text).width;
+ translation = {
+ x: -width / 2 - 5,
+ y: -cornerstoneTools.textStyle.getFontSize() * 1.5
+ };
+
+ var handleRadius = 6;
+
+ context.beginPath();
+ context.strokeStyle = color;
+ context.arc(textCoords.x, textCoords.y, handleRadius, 0, 2 * Math.PI);
+ context.stroke();
+ } else {
+ translation = {
+ x: 4,
+ y: -4
+ };
+ }
+
+ cornerstoneTools.drawTextBox(context, text, textCoords.x + translation.x, textCoords.y + translation.y, color);
+ context.restore();
+ }
+
+ function mouseUpCallback(e, eventData) {
+ $(eventData.element).off('CornerstoneToolsMouseDrag', dragCallback);
+ $(eventData.element).off('CornerstoneToolsMouseUp', mouseUpCallback);
+ cornerstone.updateImage(eventData.element);
+ }
+
+ function mouseDownCallback(e, eventData) {
+ if (cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
+ $(eventData.element).on('CornerstoneToolsMouseDrag', dragCallback);
+ $(eventData.element).on('CornerstoneToolsMouseUp', mouseUpCallback);
+ cornerstoneTools.dragProbe.strategy(eventData);
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+ }
+
+ function dragCallback(e, eventData) {
+ cornerstoneTools.dragProbe.strategy(eventData);
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+
+ cornerstoneTools.dragProbe = cornerstoneTools.simpleMouseButtonTool(mouseDownCallback);
+
+ cornerstoneTools.dragProbe.strategies = {
+ default: defaultStrategy,
+ minimal: minimalStrategy
+ };
+ cornerstoneTools.dragProbe.strategy = defaultStrategy;
+
+ var options = {
+ fireOnTouchStart: true
+ };
+ cornerstoneTools.dragProbeTouch = cornerstoneTools.touchDragTool(dragCallback, options);
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/dragProbe.js
+
+// Begin Source: src/imageTools/ellipticalRoi.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ var toolType = 'ellipticalRoi';
+
+ ///////// BEGIN ACTIVE TOOL ///////
+ function createNewMeasurement(mouseEventData) {
+ // create the measurement data for this tool with the end handle activated
+ var measurementData = {
+ visible: true,
+ active: true,
+ invalidated: 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: true
+ }
+ }
+ };
+
+ return measurementData;
+ }
+ ///////// END ACTIVE TOOL ///////
+
+ ///////// BEGIN IMAGE RENDERING ///////
+ function pointInEllipse(ellipse, location) {
+ var xRadius = ellipse.width / 2;
+ var yRadius = ellipse.height / 2;
+
+ if (xRadius <= 0.0 || yRadius <= 0.0) {
+ return false;
+ }
+
+ var center = {
+ x: ellipse.left + xRadius,
+ y: ellipse.top + yRadius
+ };
+
+ /* This is a more general form of the circle equation
+ *
+ * X^2/a^2 + Y^2/b^2 <= 1
+ */
+
+ var normalized = {
+ x: location.x - center.x,
+ y: location.y - center.y
+ };
+
+ var inEllipse = ((normalized.x * normalized.x) / (xRadius * xRadius)) + ((normalized.y * normalized.y) / (yRadius * yRadius)) <= 1.0;
+ return inEllipse;
+ }
+
+ function calculateMeanStdDev(sp, ellipse) {
+ // TODO: Get a real statistics library here that supports large counts
+
+ var sum = 0;
+ var sumSquared = 0;
+ var count = 0;
+ var index = 0;
+
+ for (var y = ellipse.top; y < ellipse.top + ellipse.height; y++) {
+ for (var x = ellipse.left; x < ellipse.left + ellipse.width; x++) {
+ if (pointInEllipse(ellipse, {
+ x: x,
+ y: y
+ }) === true) {
+ sum += sp[index];
+ sumSquared += sp[index] * sp[index];
+ count++;
+ }
+
+ index++;
+ }
+ }
+
+ if (count === 0) {
+ return {
+ count: count,
+ mean: 0.0,
+ variance: 0.0,
+ stdDev: 0.0
+ };
+ }
+
+ var mean = sum / count;
+ var variance = sumSquared / count - mean * mean;
+
+ return {
+ count: count,
+ mean: mean,
+ variance: variance,
+ stdDev: Math.sqrt(variance)
+ };
+ }
+
+ function pointNearEllipse(element, data, coords, distance) {
+ var startCanvas = cornerstone.pixelToCanvas(element, data.handles.start);
+ var endCanvas = cornerstone.pixelToCanvas(element, data.handles.end);
+
+ var minorEllipse = {
+ left: Math.min(startCanvas.x, endCanvas.x) + distance / 2,
+ top: Math.min(startCanvas.y, endCanvas.y) + distance / 2 ,
+ width: Math.abs(startCanvas.x - endCanvas.x) - distance,
+ height: Math.abs(startCanvas.y - endCanvas.y) - distance
+ };
+
+ var majorEllipse = {
+ left: Math.min(startCanvas.x, endCanvas.x) - distance / 2,
+ top: Math.min(startCanvas.y, endCanvas.y) - distance / 2 ,
+ width: Math.abs(startCanvas.x - endCanvas.x) + distance,
+ height: Math.abs(startCanvas.y - endCanvas.y) + distance
+ };
+
+ var pointInMinorEllipse = pointInEllipse(minorEllipse, coords);
+ var pointInMajorEllipse = pointInEllipse(majorEllipse, coords);
+
+ if (pointInMajorEllipse && !pointInMinorEllipse) {
+ return true;
+ }
+
+ return false;
+ }
+
+ function pointNearTool(element, data, coords) {
+ return pointNearEllipse(element, data, coords, 15);
+ }
+
+ function pointNearToolTouch(element, data, coords) {
+ return pointNearEllipse(element, data, coords, 25);
+ }
+
+ function onImageRendered(e, eventData) {
+
+ // 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;
+ }
+
+ // 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);
+
+ //activation color
+ var color;
+ var lineWidth = cornerstoneTools.toolStyle.getToolWidth();
+ var font = cornerstoneTools.textStyle.getFont();
+ var fontHeight = cornerstoneTools.textStyle.getFontSize();
+
+ for (var i = 0; i < toolData.data.length; i++) {
+ context.save();
+
+ var data = toolData.data[i];
+
+ //differentiate the color of activation tool
+ if (data.active) {
+ color = cornerstoneTools.toolColors.getActiveColor();
+ } else {
+ color = cornerstoneTools.toolColors.getToolColor();
+ }
+
+ // draw the ellipse
+ var handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.start);
+ var handleEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.end);
+
+ var widthCanvas = Math.abs(handleStartCanvas.x - handleEndCanvas.x);
+ var heightCanvas = Math.abs(handleStartCanvas.y - handleEndCanvas.y);
+ var leftCanvas = Math.min(handleStartCanvas.x, handleEndCanvas.x);
+ var topCanvas = Math.min(handleStartCanvas.y, handleEndCanvas.y);
+ var centerX = (handleStartCanvas.x + handleEndCanvas.x) / 2;
+ var centerY = (handleStartCanvas.y + handleEndCanvas.y) / 2;
+
+ context.beginPath();
+ context.strokeStyle = color;
+ context.lineWidth = lineWidth;
+ cornerstoneTools.drawEllipse(context, leftCanvas, topCanvas, widthCanvas, heightCanvas);
+ context.closePath();
+
+ // draw the handles
+ cornerstoneTools.drawHandles(context, eventData, data.handles, color);
+
+ context.font = font;
+
+ var textX,
+ textY,
+ area,
+ meanStdDev;
+
+ if (!data.invalidated) {
+ textX = data.textX;
+ textY = data.textY;
+ meanStdDev = data.meanStdDev;
+ area = data.area;
+ } else {
+ // TODO: calculate this in web worker for large pixel counts...
+ var width = Math.abs(data.handles.start.x - data.handles.end.x);
+ var height = Math.abs(data.handles.start.y - data.handles.end.y);
+ var left = Math.min(data.handles.start.x, data.handles.end.x);
+ var top = Math.min(data.handles.start.y, data.handles.end.y);
+
+ var pixels = cornerstone.getPixels(eventData.element, left, top, width, height);
+
+ var ellipse = {
+ left: left,
+ top: top,
+ width: width,
+ height: height
+ };
+
+ // Calculate the mean, stddev, and area
+ meanStdDev = calculateMeanStdDev(pixels, ellipse);
+ area = Math.PI * (width * eventData.image.columnPixelSpacing / 2) * (height * eventData.image.rowPixelSpacing / 2);
+
+ data.invalidated = false;
+ if (!isNaN(area)) {
+ data.area = area;
+ }
+
+ if (!isNaN(meanStdDev.mean) && !isNaN(meanStdDev.stdDev)) {
+ data.meanStdDev = meanStdDev;
+ }
+ }
+
+ // Draw text
+
+ var areaText,
+ areaTextWidth = 0;
+ if (area !== undefined) {
+ areaText = 'Area: ' + area.toFixed(2) + ' mm' + String.fromCharCode(178);
+ areaTextWidth = context.measureText(areaText).width;
+ }
+
+ var meanText = 'Mean: ' + meanStdDev.mean.toFixed(2);
+ var meanTextWidth = context.measureText(meanText).width;
+
+ var stdDevText = 'StdDev: ' + meanStdDev.stdDev.toFixed(2);
+ var stdDevTextWidth = context.measureText(stdDevText).width;
+
+ var longestTextWidth = Math.max(meanTextWidth, areaTextWidth, stdDevTextWidth);
+
+ textX = centerX < (eventData.image.columns / 2) ? centerX + (widthCanvas / 2) + longestTextWidth: centerX - (widthCanvas / 2) - longestTextWidth - 15;
+ textY = centerY < (eventData.image.rows / 2) ? centerY + (heightCanvas / 2): centerY - (heightCanvas / 2);
+
+ context.fillStyle = color;
+ if (meanStdDev) {
+ cornerstoneTools.drawTextBox(context, meanText, textX, textY - fontHeight - 5, color);
+ cornerstoneTools.drawTextBox(context, stdDevText, textX, textY, color);
+ }
+
+ // Char code 178 is a superscript 2 for mm^2
+ if (area !== undefined && !isNaN(area)) {
+ cornerstoneTools.drawTextBox(context, areaText, textX, textY + fontHeight + 5, color);
+ }
+
+ context.restore();
+ }
+ }
+ ///////// END IMAGE RENDERING ///////
+
+ // module exports
+ cornerstoneTools.ellipticalRoi = cornerstoneTools.mouseButtonTool({
+ createNewMeasurement: createNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ toolType: toolType
+ });
+ cornerstoneTools.ellipticalRoiTouch = cornerstoneTools.touchTool({
+ createNewMeasurement: createNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearToolTouch,
+ toolType: toolType
+ });
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/imageTools/ellipticalRoi.js
+
+// Begin Source: src/imageTools/freehand.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ var toolType = 'freehand';
+ var configuration = {
+ mouseLocation: {
+ handles: {
+ start: {
+ highlight: true,
+ active: true,
+ }
+ }
+ },
+ freehand: false,
+ modifying: false,
+ currentHandle: 0,
+ currentTool: -1
+ };
+
+ ///////// BEGIN ACTIVE TOOL ///////
+ function addPoint(eventData) {
+ var toolData = cornerstoneTools.getToolState(eventData.element, toolType);
+ if (toolData === undefined) {
+ return;
+ }
+
+ var config = cornerstoneTools.freehand.getConfiguration();
+
+ // Get the toolData from the last-drawn drawing
+ // (this should change when modification is added)
+ var data = toolData.data[config.currentTool];
+
+ var handleData = {
+ x: eventData.currentPoints.image.x,
+ y: eventData.currentPoints.image.y,
+ highlight: true,
+ active: true,
+ lines: []
+ };
+
+ // If this is not the first handle
+ if (data.handles.length){
+ // Add the line from the current handle to the new handle
+ data.handles[config.currentHandle - 1].lines.push(eventData.currentPoints.image);
+ }
+
+ // Add the new handle
+ data.handles.push(handleData);
+
+ // Increment the current handle value
+ config.currentHandle += 1;
+
+ // Reset freehand value
+ config.freehand = false;
+
+ // Force onImageRendered to fire
+ cornerstone.updateImage(eventData.element);
+ }
+
+ function pointNearHandle(eventData, toolIndex) {
+ var toolData = cornerstoneTools.getToolState(eventData.element, toolType);
+ if (toolData === undefined) {
+ return;
+ }
+
+ var data = toolData.data[toolIndex];
+ if (data.handles === undefined) {
+ return;
+ }
+
+ var mousePoint = eventData.currentPoints.canvas;
+ for (var i = 0; i < data.handles.length; i++) {
+ var handleCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles[i]);
+ if (cornerstoneMath.point.distance(handleCanvas, mousePoint) < 5) {
+ return i;
+ }
+ }
+
+ return;
+ }
+
+ function pointNearHandleAllTools(eventData) {
+ var toolData = cornerstoneTools.getToolState(eventData.element, toolType);
+ if (toolData === undefined) {
+ return;
+ }
+
+ var handleNearby;
+ for (var toolIndex = 0; toolIndex < toolData.data.length; toolIndex++) {
+ handleNearby = pointNearHandle(eventData, toolIndex);
+ if (handleNearby !== undefined) {
+ return {
+ handleNearby: handleNearby,
+ toolIndex: toolIndex
+ };
+ }
+ }
+
+ return; // Maybe this should return false?
+ }
+
+ // --- Drawing loop ---
+ // On first click, add point
+ // After first click, on mouse move, record location
+ // If mouse comes close to previous point, snap to it
+ // On next click, add another point -- continuously
+ // On each click, if it intersects with a current point, end drawing loop
+
+ function mouseUpCallback(e, eventData) {
+ $(eventData.element).off('CornerstoneToolsMouseUp', mouseUpCallback);
+
+ // Check if drawing is finished
+ var toolData = cornerstoneTools.getToolState(eventData.element, toolType);
+ if (toolData === undefined) {
+ return;
+ }
+
+ var config = cornerstoneTools.freehand.getConfiguration();
+
+ if (!eventData.event.shiftKey) {
+ config.freehand = false;
+ }
+
+ cornerstone.updateImage(eventData.element);
+ }
+
+ function mouseMoveCallback(e, eventData) {
+ var toolData = cornerstoneTools.getToolState(eventData.element, toolType);
+ if (toolData === undefined) {
+ return;
+ }
+
+ var config = cornerstoneTools.freehand.getConfiguration();
+
+ var data = toolData.data[config.currentTool];
+
+ // Set the mouseLocation handle
+ config.mouseLocation.handles.start.x = eventData.currentPoints.image.x;
+ config.mouseLocation.handles.start.y = eventData.currentPoints.image.y;
+
+ var currentHandle = config.currentHandle;
+
+ if (config.modifying) {
+ // Move the handle
+ data.active = true;
+ data.highlight = true;
+ data.handles[currentHandle].x = config.mouseLocation.handles.start.x;
+ data.handles[currentHandle].y = config.mouseLocation.handles.start.y;
+ if (currentHandle) {
+ var lastLineIndex = data.handles[currentHandle - 1].lines.length - 1;
+ var lastLine = data.handles[currentHandle - 1].lines[lastLineIndex];
+ lastLine.x = config.mouseLocation.handles.start.x;
+ lastLine.y = config.mouseLocation.handles.start.y;
+ }
+ }
+
+ if (config.freehand) {
+ data.handles[currentHandle - 1].lines.push(eventData.currentPoints.image);
+ } else {
+ // No snapping in freehand mode
+ var handleNearby = pointNearHandle(eventData, config.currentTool);
+
+ // If there is a handle nearby to snap to
+ // (and it's not the actual mouse handle)
+ if (handleNearby !== undefined && handleNearby < (data.handles.length - 1)) {
+ config.mouseLocation.handles.start.x = data.handles[handleNearby].x;
+ config.mouseLocation.handles.start.y = data.handles[handleNearby].y;
+ }
+ }
+
+ // Force onImageRendered
+ cornerstone.updateImage(eventData.element);
+ }
+
+ function startDrawing(eventData) {
+ $(eventData.element).on('CornerstoneToolsMouseMove', mouseMoveCallback);
+ $(eventData.element).on('CornerstoneToolsMouseUp', mouseUpCallback);
+
+ var measurementData = {
+ visible: true,
+ active: true,
+ handles: [],
+ };
+
+ var config = cornerstoneTools.freehand.getConfiguration();
+ config.mouseLocation.handles.start.x = eventData.currentPoints.image.x;
+ config.mouseLocation.handles.start.y = eventData.currentPoints.image.y;
+
+ cornerstoneTools.addToolState(eventData.element, toolType, measurementData);
+
+ var toolData = cornerstoneTools.getToolState(eventData.element, toolType);
+ config.currentTool = toolData.data.length - 1;
+ }
+
+ function endDrawing(eventData, handleNearby) {
+ var toolData = cornerstoneTools.getToolState(eventData.element, toolType);
+ if (toolData === undefined) {
+ return;
+ }
+
+ var config = cornerstoneTools.freehand.getConfiguration();
+
+ var data = toolData.data[config.currentTool];
+
+ data.active = false;
+ data.highlight = false;
+
+ // Connect the end of the drawing to the handle nearest to the click
+ if (handleNearby !== undefined){
+ data.handles[config.currentHandle - 1].lines.push(data.handles[handleNearby]);
+ }
+
+ if (config.modifying) {
+ config.modifying = false;
+ }
+
+ // Reset the current handle
+ config.currentHandle = 0;
+ config.currentTool = -1;
+
+ $(eventData.element).off('CornerstoneToolsMouseMove', mouseMoveCallback);
+
+ cornerstone.updateImage(eventData.element);
+ }
+
+ function mouseDownCallback(e, eventData) {
+ if (cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
+ var toolData = cornerstoneTools.getToolState(eventData.element, toolType);
+
+ var handleNearby, toolIndex;
+
+ var config = cornerstoneTools.freehand.getConfiguration();
+ var currentTool = config.currentTool;
+
+ if (config.modifying) {
+ endDrawing(eventData);
+ return;
+ }
+
+ if (currentTool < 0) {
+ var nearby = pointNearHandleAllTools(eventData);
+ if (nearby) {
+ handleNearby = nearby.handleNearby;
+ toolIndex = nearby.toolIndex;
+ // This means the user is trying to modify a point
+ if (handleNearby !== undefined) {
+ $(eventData.element).on('CornerstoneToolsMouseMove', mouseMoveCallback);
+ $(eventData.element).on('CornerstoneToolsMouseUp', mouseUpCallback);
+ config.modifying = true;
+ config.currentHandle = handleNearby;
+ config.currentTool = toolIndex;
+ }
+ } else {
+ startDrawing(eventData);
+ addPoint(eventData);
+ }
+ } else if (currentTool >= 0 && toolData.data[currentTool].active) {
+ handleNearby = pointNearHandle(eventData, currentTool);
+ if (handleNearby !== undefined) {
+ endDrawing(eventData, handleNearby);
+ } else if (eventData.event.shiftKey) {
+ config.freehand = true;
+ } else {
+ addPoint(eventData);
+ }
+ }
+
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+ }
+
+ ///////// END ACTIVE TOOL ///////
+
+ ///////// BEGIN IMAGE RENDERING ///////
+ function onImageRendered(e, eventData) {
+ // 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;
+ }
+
+ var config = cornerstoneTools.freehand.getConfiguration();
+
+ // 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 fillColor = cornerstoneTools.toolColors.getFillColor();
+
+ for (var i = 0; i < toolData.data.length; i++) {
+ context.save();
+
+ var data = toolData.data[i];
+
+ if (data.active) {
+ color = cornerstoneTools.toolColors.getActiveColor();
+ fillColor = cornerstoneTools.toolColors.getFillColor();
+ } else {
+ color = cornerstoneTools.toolColors.getToolColor();
+ fillColor = cornerstoneTools.toolColors.getToolColor();
+ }
+
+ var handleStart;
+
+ if (data.handles.length) {
+ for (var j = 0; j < data.handles.length; j++) {
+ // Draw a line between handle j and j+1
+ handleStart = data.handles[j];
+ var handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, handleStart);
+
+ context.beginPath();
+ context.strokeStyle = color;
+ context.lineWidth = lineWidth;
+ context.moveTo(handleStartCanvas.x, handleStartCanvas.y);
+
+ for (var k = 0; k < data.handles[j].lines.length; k++) {
+ var lineCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles[j].lines[k]);
+ context.lineTo(lineCanvas.x, lineCanvas.y);
+ context.stroke();
+ }
+
+ var mouseLocationCanvas = cornerstone.pixelToCanvas(eventData.element, config.mouseLocation.handles.start);
+ if (j === (data.handles.length - 1)) {
+ if (data.active && !config.freehand && !config.modifying) {
+ // If it's still being actively drawn, keep the last line to
+ // the mouse location
+ context.lineTo(mouseLocationCanvas.x, mouseLocationCanvas.y);
+ context.stroke();
+ }
+ }
+ }
+ }
+
+ // If the tool is active, draw a handle at the cursor location
+ if (data.active){
+ cornerstoneTools.drawHandles(context, eventData, config.mouseLocation.handles, color, fillColor);
+ }
+ // draw the handles
+ cornerstoneTools.drawHandles(context, eventData, data.handles, color, fillColor);
+
+ context.restore();
+ }
+ }
+ ///////// END IMAGE RENDERING ///////
+ function enable(element) {
+ $(element).off('CornerstoneToolsMouseDown', mouseDownCallback);
+ $(element).off('CornerstoneToolsMouseUp', mouseUpCallback);
+ $(element).off('CornerstoneToolsMouseMove', mouseMoveCallback);
+ $(element).off('CornerstoneImageRendered', onImageRendered);
+
+ $(element).on('CornerstoneImageRendered', onImageRendered);
+ cornerstone.updateImage(element);
+ }
+
+ // disables the reference line tool for the given element
+ function disable(element) {
+ $(element).off('CornerstoneToolsMouseDown', mouseDownCallback);
+ $(element).off('CornerstoneToolsMouseUp', mouseUpCallback);
+ $(element).off('CornerstoneToolsMouseMove', mouseMoveCallback);
+ $(element).off('CornerstoneImageRendered', onImageRendered);
+ cornerstone.updateImage(element);
+ }
+
+ // visible and interactive
+ function activate(element, mouseButtonMask) {
+ var eventData = {
+ mouseButtonMask: mouseButtonMask,
+ };
+
+ $(element).off('CornerstoneToolsMouseDown', eventData, mouseDownCallback);
+ $(element).off('CornerstoneToolsMouseUp', mouseUpCallback);
+ $(element).off('CornerstoneToolsMouseMove', mouseMoveCallback);
+ $(element).off('CornerstoneImageRendered', onImageRendered);
+
+ $(element).on('CornerstoneImageRendered', onImageRendered);
+ $(element).on('CornerstoneToolsMouseDown', eventData, mouseDownCallback);
+
+ cornerstone.updateImage(element);
+ }
+
+ // visible, but not interactive
+ function deactivate(element) {
+ $(element).off('CornerstoneToolsMouseDown', mouseDownCallback);
+ $(element).off('CornerstoneToolsMouseUp', mouseUpCallback);
+ $(element).off('CornerstoneToolsMouseMove', mouseMoveCallback);
+ $(element).off('CornerstoneImageRendered', onImageRendered);
+
+ $(element).on('CornerstoneImageRendered', onImageRendered);
+
+ cornerstone.updateImage(element);
+ }
+
+ function getConfiguration() {
+ return configuration;
+ }
+
+ function setConfiguration(config) {
+ configuration = config;
+ }
+
+ // module/private exports
+ cornerstoneTools.freehand = {
+ enable: enable,
+ disable: disable,
+ activate: activate,
+ deactivate: deactivate,
+ getConfiguration: getConfiguration,
+ setConfiguration: setConfiguration
+ };
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/imageTools/freehand.js
+
+// Begin Source: src/imageTools/highlight.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ var toolType = 'highlight';
+
+ ///////// BEGIN ACTIVE TOOL ///////
+ function createNewMeasurement(mouseEventData) {
+ //if already a highlight measurement, creating a new one will be useless
+ var existingToolData = cornerstoneTools.getToolState(mouseEventData.event.currentTarget, toolType);
+ if (existingToolData && existingToolData.data && existingToolData.data.length > 0) {
+ return;
+ }
+
+ // 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: true
+ }
+ }
+ };
+
+ return measurementData;
+ }
+ ///////// END ACTIVE TOOL ///////
+
+ function pointInsideRect(element, data, coords) {
+ var startCanvas = cornerstone.pixelToCanvas(element, data.handles.start);
+ var endCanvas = cornerstone.pixelToCanvas(element, data.handles.end);
+
+ var rect = {
+ left: Math.min(startCanvas.x, endCanvas.x),
+ top: Math.min(startCanvas.y, endCanvas.y),
+ width: Math.abs(startCanvas.x - endCanvas.x),
+ height: Math.abs(startCanvas.y - endCanvas.y)
+ };
+
+ var insideBox = false;
+ if ((coords.x >= rect.left && coords.x <= (rect.left + rect.width)) && coords.y >= rect.top && coords.y <= (rect.top + rect.height)) {
+ insideBox = true;
+ }
+
+ return insideBox;
+ }
+
+ function pointNearTool(element, data, coords) {
+ var startCanvas = cornerstone.pixelToCanvas(element, data.handles.start);
+ var endCanvas = cornerstone.pixelToCanvas(element, data.handles.end);
+
+ var rect = {
+ left: Math.min(startCanvas.x, endCanvas.x),
+ top: Math.min(startCanvas.y, endCanvas.y),
+ width: Math.abs(startCanvas.x - endCanvas.x),
+ height: Math.abs(startCanvas.y - endCanvas.y)
+ };
+
+ var distanceToPoint = cornerstoneMath.rect.distanceToPoint(rect, coords);
+ return (distanceToPoint < 5);
+ }
+
+ ///////// BEGIN IMAGE RENDERING ///////
+
+ function onImageRendered(e, eventData) {
+
+ // 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;
+ }
+
+ // we have tool data for this elemen
+ var context = eventData.canvasContext.canvas.getContext('2d');
+ context.setTransform(1, 0, 0, 1, 0, 0);
+
+ var color;
+ var lineWidth = cornerstoneTools.toolStyle.getToolWidth();
+
+ context.save();
+
+ var data = toolData.data[0];
+
+ if (!data) {
+ return;
+ }
+
+ if (data.active) {
+ color = cornerstoneTools.toolColors.getActiveColor();
+ } else {
+ color = cornerstoneTools.toolColors.getToolColor();
+ }
+
+ var handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.start);
+ var handleEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.end);
+
+ var rect = {
+ left: Math.min(handleStartCanvas.x, handleEndCanvas.x),
+ top: Math.min(handleStartCanvas.y, handleEndCanvas.y),
+ width: Math.abs(handleStartCanvas.x - handleEndCanvas.x),
+ height: Math.abs(handleStartCanvas.y - handleEndCanvas.y)
+ };
+
+ // draw dark fill outside the rectangle
+ context.beginPath();
+ context.strokeStyle = 'transparent';
+
+ context.rect(0, 0, context.canvas.clientWidth, context.canvas.clientHeight);
+
+ context.rect(rect.width + rect.left, rect.top, -rect.width, rect.height);
+ context.stroke();
+ context.fillStyle = 'rgba(0,0,0,0.7)';
+ context.fill();
+ context.closePath();
+
+ // draw dashed stroke rectangle
+ context.beginPath();
+ context.strokeStyle = color;
+ context.lineWidth = lineWidth;
+ context.setLineDash([ 4 ]);
+ context.strokeRect(rect.left, rect.top, rect.width, rect.height);
+
+ // Strange fix, but restore doesn't seem to reset the line dashes?
+ context.setLineDash([]);
+
+ // draw the handles last, so they will be on top of the overlay
+ cornerstoneTools.drawHandles(context, eventData, data.handles, color);
+ context.restore();
+ }
+ ///////// END IMAGE RENDERING ///////
+
+ // module exports
+ var preventHandleOutsideImage = true;
+
+ cornerstoneTools.highlight = cornerstoneTools.mouseButtonRectangleTool({
+ createNewMeasurement: createNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ pointInsideRect: pointInsideRect,
+ toolType: toolType
+ }, preventHandleOutsideImage);
+
+ cornerstoneTools.highlightTouch = cornerstoneTools.touchTool({
+ createNewMeasurement: createNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ pointInsideRect: pointInsideRect,
+ toolType: toolType
+ }, preventHandleOutsideImage);
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/imageTools/highlight.js
+
+// Begin Source: src/imageTools/keyboardTool.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function keyboardTool(keyDownCallback) {
+ var configuration = {};
+
+ var toolInterface = {
+ activate: function(element) {
+ $(element).off('CornerstoneToolsKeyDown', keyDownCallback);
+ $(element).on('CornerstoneToolsKeyDown', keyDownCallback);
+ },
+ disable: function(element) {$(element).off('CornerstoneToolsKeyDown', keyDownCallback);},
+ enable: function(element) {$(element).off('CornerstoneToolsKeyDown', keyDownCallback);},
+ deactivate: function(element) {$(element).off('CornerstoneToolsKeyDown', keyDownCallback);},
+ getConfiguration: function() { return configuration; },
+ setConfiguration: function(config) {configuration = config;}
+ };
+ return toolInterface;
+ }
+
+ // module exports
+ cornerstoneTools.keyboardTool = keyboardTool;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/keyboardTool.js
+
+// Begin Source: src/imageTools/lengthTool.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ var toolType = 'length';
+
+ ///////// BEGIN ACTIVE TOOL ///////
+ 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: true
+ }
+ }
+ };
+
+ 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);
+ return (distanceToPoint < 25);
+ }
+
+ ///////// BEGIN IMAGE RENDERING ///////
+ function onImageRendered(e, eventData) {
+
+ // 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;
+ }
+
+ // 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);
+
+ // Draw the text
+ context.fillStyle = color;
+ context.font = font;
+
+ var suffix = ' mm';
+ if (!eventData.image.rowPixelSpacing || !eventData.image.columnPixelSpacing) {
+ suffix = ' pixels';
+ }
+
+ var vector = {
+ x: handleStartCanvas.x - handleEndCanvas.x,
+ y: handleStartCanvas.y - handleEndCanvas.y,
+ };
+
+ var midpoint = {
+ x: (handleStartCanvas.x + handleEndCanvas.x) / 2,
+ y: (handleStartCanvas.y + handleEndCanvas.y) / 2
+ };
+
+ var magnitude = Math.sqrt(vector.x * vector.x + vector.y * vector.y);
+
+ var normal = {
+ x: -vector.y / magnitude,
+ y: vector.x / magnitude
+ };
+
+ // Set rowPixelSpacing and columnPixelSpacing to 1 if they are undefined (or zero)
+ var dx = (data.handles.end.x - data.handles.start.x) * (eventData.image.columnPixelSpacing || 1);
+ var dy = (data.handles.end.y - data.handles.start.y) * (eventData.image.rowPixelSpacing || 1);
+
+ var length = Math.sqrt(dx * dx + dy * dy);
+ var text = '' + length.toFixed(2) + suffix;
+
+ var fontSize = cornerstoneTools.textStyle.getFontSize();
+ var textWidth = context.measureText(text).width;
+
+ var distance = {
+ x: Math.min(10, textWidth / 2),
+ y: Math.min(25, fontSize),
+ };
+
+ if (handleEndCanvas.y < handleStartCanvas.y) {
+ distance.x = -distance.x;
+ distance.y = -distance.y;
+ }
+
+ var textCoords = {
+ x: midpoint.x + normal.x * distance.x,
+ y: midpoint.y + normal.y * distance.y
+ };
+
+ cornerstoneTools.drawTextBox(context, text, textCoords.x, textCoords.y, color);
+ context.restore();
+ }
+ }
+ ///////// END IMAGE RENDERING ///////
+
+ // module exports
+ cornerstoneTools.length = cornerstoneTools.mouseButtonTool({
+ createNewMeasurement: createNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ toolType: toolType
+ });
+ cornerstoneTools.lengthTouch = cornerstoneTools.touchTool({
+ createNewMeasurement: createNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ toolType: toolType
+ });
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/imageTools/lengthTool.js
+
+// Begin Source: src/imageTools/magnify.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ var configuration = {
+ magnifySize: 100,
+ magnificationLevel: 2,
+ };
+
+ var browserName;
+
+ /** Remove the magnifying glass when the mouse event ends */
+ function mouseUpCallback(e, eventData) {
+ $(eventData.element).off('CornerstoneToolsMouseDrag', dragCallback);
+ $(eventData.element).off('CornerstoneToolsMouseUp', mouseUpCallback);
+ hideTool(eventData);
+ }
+
+ function hideTool(eventData) {
+ $(eventData.element).find('.magnifyTool').hide();
+ // Re-enable the mouse cursor
+ document.body.style.cursor = 'default';
+ }
+
+ /** Draw the magnifying glass on mouseDown, and begin tracking mouse movements */
+ function mouseDownCallback(e, eventData) {
+ if (cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
+ $(eventData.element).on('CornerstoneToolsMouseDrag', eventData, dragCallback);
+ $(eventData.element).on('CornerstoneToolsMouseUp', eventData, mouseUpCallback);
+ drawMagnificationTool(eventData);
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+ }
+
+ function dragEndCallback(e, eventData) {
+ $(eventData.element).off('CornerstoneToolsDragEnd', dragEndCallback);
+ $(eventData.element).off('CornerstoneToolsTouchEnd', dragEndCallback);
+ hideTool(eventData);
+ }
+
+ /** Drag callback is triggered by both the touch and mouse magnify tools */
+ function dragCallback(e, eventData) {
+ drawMagnificationTool(eventData);
+ if (eventData.isTouchEvent === true) {
+ $(eventData.element).on('CornerstoneToolsDragEnd', dragEndCallback);
+ $(eventData.element).on('CornerstoneToolsTouchEnd', dragEndCallback);
+ }
+
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+
+ /** Draws the magnifying glass */
+ function drawMagnificationTool(eventData) {
+ var magnify = $(eventData.element).find('.magnifyTool').get(0);
+
+ if (!magnify) {
+ createMagnificationCanvas(eventData.element);
+ }
+
+ var config = cornerstoneTools.magnify.getConfiguration();
+
+ var magnifySize = config.magnifySize;
+ var magnificationLevel = config.magnificationLevel;
+
+ // The 'not' magnifyTool class here is necessary because cornerstone places
+ // no classes of it's own on the canvas we want to select
+ var canvas = $(eventData.element).find('canvas').not('.magnifyTool').get(0);
+ var context = canvas.getContext('2d');
+ context.setTransform(1, 0, 0, 1, 0, 0);
+
+ var zoomCtx = magnify.getContext('2d');
+ zoomCtx.setTransform(1, 0, 0, 1, 0, 0);
+
+ // Calculate the on-canvas location of the mouse pointer / touch
+ var canvasLocation = cornerstone.pixelToCanvas(eventData.element, eventData.currentPoints.image);
+
+ canvasLocation.x = Math.max(canvasLocation.x, 0);
+ canvasLocation.x = Math.min(canvasLocation.x, canvas.width);
+
+ canvasLocation.y = Math.max(canvasLocation.y, 0);
+ canvasLocation.y = Math.min(canvasLocation.y, canvas.height);
+
+ // Clear the rectangle
+ zoomCtx.clearRect(0, 0, magnifySize, magnifySize);
+ zoomCtx.fillStyle = 'transparent';
+
+ // 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
+ };
+
+ if (browserName === 'Safari') {
+ // Safari breaks when trying to copy pixels with negative indices
+ // This prevents proper Magnify usage
+ copyFrom.x = Math.max(copyFrom.x, 0);
+ copyFrom.y = Math.max(copyFrom.y, 0);
+ }
+
+ copyFrom.x = Math.min(copyFrom.x, canvas.width);
+ copyFrom.y = Math.min(copyFrom.y, canvas.height);
+
+ var scaledMagnify = {
+ x: (canvas.width - copyFrom.x) * magnificationLevel,
+ y: (canvas.height - copyFrom.y) * magnificationLevel
+ };
+ 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';
+ }
+
+ magnify.style.display = 'block';
+
+ // Hide the mouse cursor, so the user can see better
+ document.body.style.cursor = 'none';
+ }
+
+ /** Creates the magnifying glass canvas */
+ function createMagnificationCanvas(element) {
+ // If the magnifying glass canvas doesn't already exist
+ if ($(element).find('.magnifyTool').length === 0) {
+ // Create a canvas and append it as a child to the element
+ var magnify = document.createElement('canvas');
+ // The magnifyTool class is used to find the canvas later on
+ magnify.classList.add('magnifyTool');
+
+ var config = cornerstoneTools.magnify.getConfiguration();
+ magnify.width = config.magnifySize;
+ magnify.height = config.magnifySize;
+
+ // Make sure position is absolute so the canvas can follow the mouse / touch
+ magnify.style.position = 'absolute';
+ element.appendChild(magnify);
+ }
+ }
+
+ /** Find the magnifying glass canvas and remove it */
+ function removeMagnificationCanvas(element) {
+ $(element).find('.magnifyTool').remove();
+ }
+
+ // --- Mouse tool activate / disable --- //
+ function disable(element) {
+ $(element).off('CornerstoneToolsMouseDown', mouseDownCallback);
+ removeMagnificationCanvas(element);
+ }
+
+ function enable(element) {
+ var config = cornerstoneTools.magnify.getConfiguration(config);
+
+ if (!browserName) {
+ var infoString = cornerstoneTools.getBrowserInfo();
+ var info = infoString.split(' ');
+ browserName = info[0];
+ }
+
+ createMagnificationCanvas(element);
+ }
+
+ function activate(element, mouseButtonMask) {
+ var eventData = {
+ mouseButtonMask: mouseButtonMask,
+ };
+
+ $(element).off('CornerstoneToolsMouseDown', mouseDownCallback);
+
+ $(element).on('CornerstoneToolsMouseDown', eventData, mouseDownCallback);
+ createMagnificationCanvas(element);
+ }
+
+ // --- Touch tool activate / disable --- //
+ function getConfiguration() {
+ return configuration;
+ }
+
+ function setConfiguration(config) {
+ configuration = config;
+ }
+
+ // module exports
+ cornerstoneTools.magnify = {
+ enable: enable,
+ activate: activate,
+ deactivate: disable,
+ disable: disable,
+ getConfiguration: getConfiguration,
+ setConfiguration: setConfiguration
+ };
+
+ var options = {
+ fireOnTouchStart: true,
+ activateCallback: createMagnificationCanvas,
+ disableCallback: removeMagnificationCanvas
+ };
+ cornerstoneTools.magnifyTouchDrag = cornerstoneTools.touchDragTool(dragCallback, options);
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/magnify.js
+
+// Begin Source: src/imageTools/multiTouchDragTool.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function multiTouchDragTool(touchDragCallback) {
+ var configuration = {};
+ var events = 'CornerstoneToolsMultiTouchDrag';
+
+ var toolInterface = {
+ activate: function(element) {
+ $(element).off(events, touchDragCallback);
+ $(element).on(events, touchDragCallback);
+ },
+ disable: function(element) {
+ $(element).off(events, touchDragCallback);
+ },
+ enable: function(element) {
+ $(element).off(events, touchDragCallback);
+ },
+ deactivate: function(element) {
+ $(element).off(events, touchDragCallback);
+ },
+ getConfiguration: function() {
+ return configuration;
+ },
+ setConfiguration: function(config) {
+ configuration = config;
+ }
+ };
+ return toolInterface;
+ }
+
+ // module exports
+ cornerstoneTools.multiTouchDragTool = multiTouchDragTool;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/multiTouchDragTool.js
+
+// Begin Source: src/imageTools/orientationMarkers.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function getOrientationMarkers(element) {
+ var enabledElement = cornerstone.getEnabledElement(element);
+ var imagePlaneMetaData = cornerstoneTools.metaData.get('imagePlane', enabledElement.image.imageId);
+
+ if (!imagePlaneMetaData || !imagePlaneMetaData.rowCosines || !imagePlaneMetaData.columnCosines) {
+ return;
+ }
+
+ var rowString = cornerstoneTools.orientation.getOrientationString(imagePlaneMetaData.rowCosines);
+ var columnString = cornerstoneTools.orientation.getOrientationString(imagePlaneMetaData.columnCosines);
+
+ var oppositeRowString = cornerstoneTools.orientation.invertOrientationString(rowString);
+ var oppositeColumnString = cornerstoneTools.orientation.invertOrientationString(columnString);
+
+ return {
+ top: oppositeColumnString,
+ bottom: columnString,
+ left: oppositeRowString,
+ right: rowString
+ };
+ }
+
+ function getOrientationMarkerPositions(element) {
+ var enabledElement = cornerstone.getEnabledElement(element);
+ var coords;
+
+ coords = {
+ x: enabledElement.image.width / 2,
+ y: 5
+ };
+ var topCoords = cornerstone.pixelToCanvas(element, coords);
+
+ coords = {
+ x: enabledElement.image.width / 2,
+ y: enabledElement.image.height - 5
+ };
+ var bottomCoords = cornerstone.pixelToCanvas(element, coords);
+
+ coords = {
+ x: 5,
+ y: enabledElement.image.height / 2
+ };
+ var leftCoords = cornerstone.pixelToCanvas(element, coords);
+
+ coords = {
+ x: enabledElement.image.width - 10,
+ y: enabledElement.image.height / 2
+ };
+ var rightCoords = cornerstone.pixelToCanvas(element, coords);
+
+ return {
+ top: topCoords,
+ bottom: bottomCoords,
+ left: leftCoords,
+ right: rightCoords
+ };
+ }
+
+ function onImageRendered(e, eventData) {
+ var element = eventData.element;
+
+ var markers = getOrientationMarkers(element);
+
+ if (!markers) {
+ return;
+ }
+
+ var coords = getOrientationMarkerPositions(element, markers);
+
+ var context = eventData.canvasContext.canvas.getContext('2d');
+ context.setTransform(1, 0, 0, 1, 0, 0);
+
+ var color = cornerstoneTools.toolColors.getToolColor();
+
+ var textWidths = {
+ top: context.measureText(markers.top).width,
+ left: context.measureText(markers.left).width,
+ right: context.measureText(markers.right).width,
+ bottom: context.measureText(markers.bottom).width
+ };
+
+ cornerstoneTools.drawTextBox(context, markers.top, coords.top.x - textWidths.top / 2, coords.top.y, color);
+ cornerstoneTools.drawTextBox(context, markers.left, coords.left.x - textWidths.left / 2, coords.left.y, color);
+
+ var config = cornerstoneTools.orientationMarkers.getConfiguration();
+ if (config && config.drawAllMarkers) {
+ cornerstoneTools.drawTextBox(context, markers.right, coords.right.x - textWidths.right / 2, coords.right.y, color);
+ cornerstoneTools.drawTextBox(context, markers.bottom, coords.bottom.x - textWidths.bottom / 2, coords.bottom.y, color);
+ }
+ }
+ ///////// END IMAGE RENDERING ///////
+
+ // module exports
+ cornerstoneTools.orientationMarkers = cornerstoneTools.displayTool(onImageRendered);
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/orientationMarkers.js
+
+// Begin Source: src/imageTools/pan.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function mouseUpCallback(e, eventData) {
+ $(eventData.element).off('CornerstoneToolsMouseDrag', dragCallback);
+ $(eventData.element).off('CornerstoneToolsMouseUp', mouseUpCallback);
+ }
+
+ function mouseDownCallback(e, eventData) {
+ if (cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
+ $(eventData.element).on('CornerstoneToolsMouseDrag', dragCallback);
+ $(eventData.element).on('CornerstoneToolsMouseUp', mouseUpCallback);
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+ }
+
+ function dragCallback(e, eventData) {
+ eventData.viewport.translation.x += (eventData.deltaPoints.page.x / eventData.viewport.scale);
+ eventData.viewport.translation.y += (eventData.deltaPoints.page.y / eventData.viewport.scale);
+ cornerstone.setViewport(eventData.element, eventData.viewport);
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+
+ cornerstoneTools.pan = cornerstoneTools.simpleMouseButtonTool(mouseDownCallback);
+ cornerstoneTools.panTouchDrag = cornerstoneTools.touchDragTool(dragCallback);
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/pan.js
+
+// Begin Source: src/imageTools/panMultiTouch.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function touchPanCallback(e, eventData) {
+ var config = cornerstoneTools.panMultiTouch.getConfiguration();
+ if (config && config.testPointers(eventData)) {
+ eventData.viewport.translation.x += (eventData.deltaPoints.page.x / eventData.viewport.scale);
+ eventData.viewport.translation.y += (eventData.deltaPoints.page.y / eventData.viewport.scale);
+ cornerstone.setViewport(eventData.element, eventData.viewport);
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+ }
+
+ var configuration = {
+ testPointers: function(eventData) {
+ return (eventData.numPointers >= 2);
+ }
+ };
+
+ cornerstoneTools.panMultiTouch = cornerstoneTools.multiTouchDragTool(touchPanCallback);
+ cornerstoneTools.panMultiTouch.setConfiguration(configuration);
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/panMultiTouch.js
+
+// Begin Source: src/imageTools/probe.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ var toolType = 'probe';
+
+ ///////// BEGIN ACTIVE TOOL ///////
+ function createNewMeasurement(mouseEventData) {
+ // create the measurement data for this tool with the end handle activated
+ var measurementData = {
+ visible: true,
+ active: true,
+ handles: {
+ end: {
+ x: mouseEventData.currentPoints.image.x,
+ y: mouseEventData.currentPoints.image.y,
+ highlight: true,
+ active: true
+ }
+ }
+ };
+ return measurementData;
+ }
+ ///////// END ACTIVE TOOL ///////
+
+ ///////// BEGIN IMAGE RENDERING ///////
+ function pointNearTool(element, data, coords) {
+ var endCanvas = cornerstone.pixelToCanvas(element, data.handles.end);
+ return cornerstoneMath.point.distance(endCanvas, coords) < 5;
+ }
+
+ function onImageRendered(e, eventData) {
+ // 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;
+ }
+
+ // 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 font = cornerstoneTools.textStyle.getFont();
+ var fontHeight = cornerstoneTools.textStyle.getFontSize();
+
+ for (var i = 0; i < toolData.data.length; i++) {
+
+ context.save();
+ var data = toolData.data[i];
+
+ if (data.active) {
+ color = cornerstoneTools.toolColors.getActiveColor();
+ } else {
+ color = cornerstoneTools.toolColors.getToolColor();
+ }
+
+ // draw the handles
+ cornerstoneTools.drawHandles(context, eventData, data.handles, color);
+
+ var x = Math.round(data.handles.end.x);
+ var y = Math.round(data.handles.end.y);
+ var storedPixels;
+
+ var text,
+ str;
+
+ if (x < 0 || y < 0 || x >= eventData.image.columns || y >= eventData.image.rows) {
+ return;
+ }
+
+ if (eventData.image.color) {
+ text = '' + x + ', ' + y;
+ storedPixels = cornerstoneTools.getRGBPixels(eventData.element, x, y, 1, 1);
+ str = 'R: ' + storedPixels[0] + ' G: ' + storedPixels[1] + ' B: ' + storedPixels[2];
+ } else {
+ storedPixels = cornerstone.getStoredPixels(eventData.element, x, y, 1, 1);
+ var sp = storedPixels[0];
+ var mo = sp * eventData.image.slope + eventData.image.intercept;
+ var suv = cornerstoneTools.calculateSUV(eventData.image, sp);
+
+ // Draw text
+ text = '' + x + ', ' + y;
+ str = 'SP: ' + sp + ' MO: ' + parseFloat(mo.toFixed(3));
+ if (suv) {
+ str += ' SUV: ' + parseFloat(suv.toFixed(3));
+ }
+ }
+
+ var coords = {
+ // translate the x/y away from the cursor
+ x: data.handles.end.x + 3,
+ y: data.handles.end.y - 3
+ };
+ var textCoords = cornerstone.pixelToCanvas(eventData.element, coords);
+
+ context.font = font;
+ context.fillStyle = color;
+
+ cornerstoneTools.drawTextBox(context, str, textCoords.x, textCoords.y + fontHeight + 5, color);
+ cornerstoneTools.drawTextBox(context, text, textCoords.x, textCoords.y, color);
+ context.restore();
+ }
+ }
+ ///////// END IMAGE RENDERING ///////
+
+ // module exports
+ cornerstoneTools.probe = cornerstoneTools.mouseButtonTool({
+ createNewMeasurement: createNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ toolType: toolType
+ });
+ cornerstoneTools.probeTouch = cornerstoneTools.touchTool({
+ createNewMeasurement: createNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ toolType: toolType
+ });
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/probe.js
+
+// Begin Source: src/imageTools/rectangleRoi.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ var toolType = 'rectangleRoi';
+
+ ///////// BEGIN ACTIVE TOOL ///////
+ 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: true
+ }
+ }
+ };
+
+ return measurementData;
+ }
+ ///////// END ACTIVE TOOL ///////
+
+ function pointNearTool(element, data, coords) {
+ var startCanvas = cornerstone.pixelToCanvas(element, data.handles.start);
+ var endCanvas = cornerstone.pixelToCanvas(element, data.handles.end);
+
+ var rect = {
+ left: Math.min(startCanvas.x, endCanvas.x),
+ top: Math.min(startCanvas.y, endCanvas.y),
+ width: Math.abs(startCanvas.x - endCanvas.x),
+ height: Math.abs(startCanvas.y - endCanvas.y)
+ };
+
+ var distanceToPoint = cornerstoneMath.rect.distanceToPoint(rect, coords);
+ return (distanceToPoint < 5);
+ }
+
+ ///////// BEGIN IMAGE RENDERING ///////
+
+ function calculateMeanStdDev(sp, ellipse) {
+ // TODO: Get a real statistics library here that supports large counts
+
+ var sum = 0;
+ var sumSquared = 0;
+ var count = 0;
+ var index = 0;
+
+ for (var y = ellipse.top; y < ellipse.top + ellipse.height; y++) {
+ for (var x = ellipse.left; x < ellipse.left + ellipse.width; x++) {
+ sum += sp[index];
+ sumSquared += sp[index] * sp[index];
+ count++;
+ index++;
+ }
+ }
+
+ if (count === 0) {
+ return {
+ count: count,
+ mean: 0.0,
+ variance: 0.0,
+ stdDev: 0.0
+ };
+ }
+
+ var mean = sum / count;
+ var variance = sumSquared / count - mean * mean;
+
+ return {
+ count: count,
+ mean: mean,
+ variance: variance,
+ stdDev: Math.sqrt(variance)
+ };
+ }
+
+ function onImageRendered(e, eventData) {
+
+ // 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;
+ }
+
+ // 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);
+
+ //activation color
+ var color;
+ var lineWidth = cornerstoneTools.toolStyle.getToolWidth();
+ var font = cornerstoneTools.textStyle.getFont();
+ var fontHeight = cornerstoneTools.textStyle.getFontSize();
+
+ for (var i = 0; i < toolData.data.length; i++) {
+ context.save();
+
+ var data = toolData.data[i];
+
+ //differentiate the color of activation tool
+ if (data.active) {
+ color = cornerstoneTools.toolColors.getActiveColor();
+ } else {
+ color = cornerstoneTools.toolColors.getToolColor();
+ }
+
+ // draw the rectangle
+ var handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.start);
+ var handleEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.end);
+
+ var widthCanvas = Math.abs(handleStartCanvas.x - handleEndCanvas.x);
+ var heightCanvas = Math.abs(handleStartCanvas.y - handleEndCanvas.y);
+ var leftCanvas = Math.min(handleStartCanvas.x, handleEndCanvas.x);
+ var topCanvas = Math.min(handleStartCanvas.y, handleEndCanvas.y);
+ var centerX = (handleStartCanvas.x + handleEndCanvas.x) / 2;
+ var centerY = (handleStartCanvas.y + handleEndCanvas.y) / 2;
+
+ context.beginPath();
+ context.strokeStyle = color;
+ context.lineWidth = lineWidth;
+ context.rect(leftCanvas, topCanvas, widthCanvas, heightCanvas);
+ context.stroke();
+
+ // draw the handles
+ cornerstoneTools.drawHandles(context, eventData, data.handles, color);
+
+ // Calculate the mean, stddev, and area
+ // TODO: calculate this in web worker for large pixel counts...
+
+ var width = Math.abs(data.handles.start.x - data.handles.end.x);
+ var height = Math.abs(data.handles.start.y - data.handles.end.y);
+ var left = Math.min(data.handles.start.x, data.handles.end.x);
+ var top = Math.min(data.handles.start.y, data.handles.end.y);
+ var pixels = cornerstone.getPixels(eventData.element, left, top, width, height);
+
+ var ellipse = {
+ left: left,
+ top: top,
+ width: width,
+ height: height
+ };
+
+ var meanStdDev = calculateMeanStdDev(pixels, ellipse);
+ var area = (width * eventData.image.columnPixelSpacing) * (height * eventData.image.rowPixelSpacing);
+ var areaText = 'Area: ' + area.toFixed(2) + ' mm^2';
+
+ // Draw text
+ context.font = font;
+
+ var textSize = context.measureText(area);
+
+ var textX = centerX < (eventData.image.columns / 2) ? centerX + (widthCanvas / 2): centerX - (widthCanvas / 2) - textSize.width;
+ var textY = centerY < (eventData.image.rows / 2) ? centerY + (heightCanvas / 2): centerY - (heightCanvas / 2);
+
+ context.fillStyle = color;
+ cornerstoneTools.drawTextBox(context, 'Mean: ' + meanStdDev.mean.toFixed(2), textX, textY - fontHeight - 5, color);
+ cornerstoneTools.drawTextBox(context, 'StdDev: ' + meanStdDev.stdDev.toFixed(2), textX, textY, color);
+ cornerstoneTools.drawTextBox(context, areaText, textX, textY + fontHeight + 5, color);
+ context.restore();
+ }
+ }
+ ///////// END IMAGE RENDERING ///////
+
+ // module exports
+ cornerstoneTools.rectangleRoi = cornerstoneTools.mouseButtonTool({
+ createNewMeasurement: createNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ toolType: toolType
+ });
+ cornerstoneTools.rectangleRoiTouch = cornerstoneTools.touchTool({
+ createNewMeasurement: createNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ toolType: toolType
+ });
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/imageTools/rectangleRoi.js
+
+// Begin Source: src/imageTools/rotate.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ // --- Strategies --- //
+ function defaultStrategy(eventData) {
+ // Calculate distance from the center of the image
+ var rect = eventData.element.getBoundingClientRect(eventData.element);
+
+ var points = {
+ x: eventData.currentPoints.client.x, y: eventData.currentPoints.client.y
+ };
+
+ var width = eventData.element.clientWidth;
+ var height = eventData.element.clientHeight;
+
+ var pointsFromCenter = {
+ x: points.x - rect.left - width / 2,
+ // Invert the coordinate system so that up is positive
+ y: -1 * (points.y - rect.top - height / 2)
+ };
+
+ var rotationRadians = Math.atan2(pointsFromCenter.y, pointsFromCenter.x);
+ var rotationDegrees = rotationRadians * (180 / Math.PI);
+ var rotation = -1 * rotationDegrees + 90;
+ eventData.viewport.rotation = rotation;
+ cornerstone.setViewport(eventData.element, eventData.viewport);
+ }
+
+ function horizontalStrategy(eventData) {
+ eventData.viewport.rotation += (eventData.deltaPoints.page.x / eventData.viewport.scale);
+ cornerstone.setViewport(eventData.element, eventData.viewport);
+ }
+
+ function verticalStrategy(eventData) {
+ eventData.viewport.rotation += (eventData.deltaPoints.page.y / eventData.viewport.scale);
+ cornerstone.setViewport(eventData.element, eventData.viewport);
+ }
+
+ // --- Mouse event callbacks --- //
+ function mouseUpCallback(e, eventData) {
+ $(eventData.element).off('CornerstoneToolsMouseDrag', dragCallback);
+ $(eventData.element).off('CornerstoneToolsMouseUp', mouseUpCallback);
+ }
+
+ function mouseDownCallback(e, eventData) {
+ if (cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
+ $(eventData.element).on('CornerstoneToolsMouseDrag', dragCallback);
+ $(eventData.element).on('CornerstoneToolsMouseUp', mouseUpCallback);
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+ }
+
+ function dragCallback(e, eventData) {
+ cornerstoneTools.rotate.strategy(eventData);
+ cornerstone.setViewport(eventData.element, eventData.viewport);
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+
+ cornerstoneTools.rotate = cornerstoneTools.simpleMouseButtonTool(mouseDownCallback);
+ cornerstoneTools.rotate.strategies = {
+ default: defaultStrategy,
+ horizontal: horizontalStrategy,
+ vertical: verticalStrategy
+ };
+
+ cornerstoneTools.rotate.strategy = defaultStrategy;
+
+ cornerstoneTools.rotateTouchDrag = cornerstoneTools.touchDragTool(dragCallback);
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/rotate.js
+
+// Begin Source: src/imageTools/rotateTouch.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function touchRotateCallback(e, eventData) {
+ eventData.viewport.rotation += eventData.rotation;
+ cornerstone.setViewport(eventData.element, eventData.viewport);
+ return false;
+ }
+
+ function disable(element) {
+ $(element).off('CornerstoneToolsTouchRotate', touchRotateCallback);
+ }
+
+ function activate(element) {
+ $(element).off('CornerstoneToolsTouchRotate', touchRotateCallback);
+ $(element).on('CornerstoneToolsTouchRotate', touchRotateCallback);
+ }
+
+ cornerstoneTools.rotateTouch = {
+ activate: activate, disable: disable
+ };
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/rotateTouch.js
+
+// Begin Source: src/imageTools/saveImage.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function saveAs(element, filename) {
+ var canvas = $(element).find('canvas').get(0);
+
+ // Thanks to Ken Fyrstenber
+ // http://stackoverflow.com/questions/18480474/how-to-save-an-image-from-canvas
+ var lnk = document.createElement('a');
+
+ /// the key here is to set the download attribute of the a tag
+ lnk.download = filename;
+
+ /// convert canvas content to data-uri for link. When download
+ /// attribute is set the content pointed to by link will be
+ /// pushed as 'download' in HTML5 capable browsers
+ lnk.href = canvas.toDataURL();
+
+ /// create a 'fake' click-event to trigger the download
+ if (document.createEvent) {
+
+ var e = document.createEvent('MouseEvents');
+ e.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+
+ lnk.dispatchEvent(e);
+
+ } else if (lnk.fireEvent) {
+
+ lnk.fireEvent('onclick');
+ }
+ }
+
+ cornerstoneTools.saveAs = saveAs;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/saveImage.js
+
+// Begin Source: src/imageTools/simpleAngle.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ var toolType = 'simpleAngle';
+
+ ///////// BEGIN ACTIVE TOOL ///////
+ function createNewMeasurement(mouseEventData) {
+ // create the measurement data for this tool with the end handle activated
+ var angleData = {
+ 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
+ }
+ }
+ };
+
+ return angleData;
+ }
+ ///////// END ACTIVE TOOL ///////
+
+ function pointNearTool(element, data, coords) {
+ var lineSegment = {
+ start: cornerstone.pixelToCanvas(element, data.handles.start), end: cornerstone.pixelToCanvas(element, data.handles.middle)
+ };
+
+ var distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords);
+ if (distanceToPoint < 5) {
+ return true;
+ }
+
+ lineSegment.start = cornerstone.pixelToCanvas(element, data.handles.middle);
+ lineSegment.end = cornerstone.pixelToCanvas(element, data.handles.end);
+
+ distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords);
+ return (distanceToPoint < 5);
+ }
+
+ function length(vector) {
+ return Math.sqrt(Math.pow(vector.x, 2) + Math.pow(vector.y, 2));
+ }
+
+ ///////// BEGIN IMAGE RENDERING ///////
+ function onImageRendered(e, eventData) {
+
+ // 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;
+ }
+
+ // 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);
+
+ //activation color
+ var color;
+ var lineWidth = cornerstoneTools.toolStyle.getToolWidth();
+ var font = cornerstoneTools.textStyle.getFont();
+
+ for (var i = 0; i < toolData.data.length; i++) {
+ context.save();
+
+ var data = toolData.data[i];
+
+ //differentiate the color of activation tool
+ if (data.active) {
+ color = cornerstoneTools.toolColors.getActiveColor();
+ } else {
+ color = cornerstoneTools.toolColors.getToolColor();
+ }
+
+ // draw the line
+ context.beginPath();
+ context.strokeStyle = color;
+ context.lineWidth = lineWidth;
+
+ var handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.start);
+ var handleMiddleCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.middle);
+
+ context.moveTo(handleStartCanvas.x, handleStartCanvas.y);
+ context.lineTo(handleMiddleCanvas.x, handleMiddleCanvas.y);
+
+ var handleEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.end);
+
+ context.lineTo(handleEndCanvas.x, handleEndCanvas.y);
+ context.stroke();
+
+ // draw the handles
+ cornerstoneTools.drawHandles(context, eventData, data.handles, color);
+
+ // Draw the text
+ context.fillStyle = color;
+
+ // Default to isotropic pixel size, update suffix to reflect this
+ var columnPixelSpacing = eventData.image.columnPixelSpacing || 1;
+ var rowPixelSpacing = eventData.image.rowPixelSpacing || 1;
+ var suffix = '';
+ if (!eventData.image.rowPixelSpacing || !eventData.image.columnPixelSpacing) {
+ suffix = ' (isotropic)';
+ }
+
+ 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
+ };
+
+ 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
+ };
+
+ 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
+ };
+
+ var sideALength = length(sideA);
+ var sideBLength = length(sideB);
+ var sideCLength = length(sideC);
+
+ // Cosine law
+ var angle = Math.acos((Math.pow(sideALength, 2) + Math.pow(sideBLength, 2) - Math.pow(sideCLength, 2)) / (2 * sideALength * sideBLength));
+ angle = angle * (180 / Math.PI);
+
+ var rAngle = cornerstoneTools.roundToDecimal(angle, 2);
+
+ if (rAngle) {
+ var str = '00B0'; // degrees symbol
+ var text = rAngle.toString() + String.fromCharCode(parseInt(str, 16)) + suffix;
+
+ var distance = 15;
+
+ var textX = handleMiddleCanvas.x + distance;
+ var textY = handleMiddleCanvas.y + distance;
+
+ context.font = font;
+ var textWidth = context.measureText(text).width;
+
+ if ((handleMiddleCanvas.x - handleStartCanvas.x) < 0) {
+ textX = handleMiddleCanvas.x - distance - textWidth - 10;
+ } else {
+ textX = handleMiddleCanvas.x + distance;
+ }
+
+ textY = handleMiddleCanvas.y;
+ cornerstoneTools.drawTextBox(context, text, textX, textY, color);
+ }
+
+ context.restore();
+ }
+ }
+ ///////// END IMAGE RENDERING ///////
+
+ ///////// BEGIN ACTIVE TOOL ///////
+ function addNewMeasurement(mouseEventData) {
+ var measurementData = createNewMeasurement(mouseEventData);
+ var element = mouseEventData.element;
+
+ 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.simpleAngle.mouseMoveCallback);
+ $(element).off('CornerstoneToolsMouseDrag', cornerstoneTools.simpleAngle.mouseMoveCallback);
+ $(element).off('CornerstoneToolsMouseDown', cornerstoneTools.simpleAngle.mouseDownCallback);
+ $(element).off('CornerstoneToolsMouseDownActivate', cornerstoneTools.simpleAngle.mouseDownActivateCallback);
+ cornerstone.updateImage(element);
+
+ cornerstoneTools.moveNewHandle(mouseEventData, measurementData.handles.middle, function() {
+ measurementData.active = false;
+ if (cornerstoneTools.anyHandlesOutsideImage(mouseEventData, measurementData.handles)) {
+ // delete the measurement
+ cornerstoneTools.removeToolState(element, toolType, measurementData);
+ }
+
+ measurementData.handles.end.active = true;
+ cornerstone.updateImage(element);
+
+ cornerstoneTools.moveNewHandle(mouseEventData, measurementData.handles.end, function() {
+ measurementData.active = false;
+ if (cornerstoneTools.anyHandlesOutsideImage(mouseEventData, measurementData.handles)) {
+ // delete the measurement
+ cornerstoneTools.removeToolState(element, toolType, measurementData);
+ }
+
+ $(element).on('CornerstoneToolsMouseMove', cornerstoneTools.simpleAngle.mouseMoveCallback);
+ $(element).on('CornerstoneToolsMouseDrag', cornerstoneTools.simpleAngle.mouseMoveCallback);
+ $(element).on('CornerstoneToolsMouseDown', eventData, cornerstoneTools.simpleAngle.mouseDownCallback);
+ $(element).on('CornerstoneToolsMouseDownActivate', eventData, cornerstoneTools.simpleAngle.mouseDownActivateCallback);
+ cornerstone.updateImage(element);
+ });
+ });
+ }
+
+ function addNewMeasurementTouch(touchEventData) {
+ var measurementData = createNewMeasurement(touchEventData);
+ var element = touchEventData.element;
+
+ // 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.simpleAngleTouch.touchMoveCallback);
+ $(element).off('CornerstoneToolsDragStartActive', cornerstoneTools.simpleAngleTouch.touchDownActivateCallback);
+ $(element).off('CornerstoneToolsTap', cornerstoneTools.simpleAngleTouch.tapCallback);
+ cornerstone.updateImage(element);
+
+ cornerstoneTools.moveNewHandleTouch(touchEventData, measurementData.handles.middle, function() {
+ measurementData.active = false;
+ if (cornerstoneTools.anyHandlesOutsideImage(touchEventData, measurementData.handles)) {
+ // delete the measurement
+ cornerstoneTools.removeToolState(element, toolType, measurementData);
+ }
+
+ measurementData.handles.end.active = true;
+ cornerstone.updateImage(element);
+
+ cornerstoneTools.moveNewHandleTouch(touchEventData, measurementData.handles.end, function() {
+ measurementData.active = false;
+ if (cornerstoneTools.anyHandlesOutsideImage(touchEventData, measurementData.handles)) {
+ // delete the measurement
+ cornerstoneTools.removeToolState(element, toolType, measurementData);
+ }
+
+ $(element).on('CornerstoneToolsTouchDrag', cornerstoneTools.simpleAngleTouch.touchMoveCallback);
+ $(element).on('CornerstoneToolsDragStartActive', cornerstoneTools.simpleAngleTouch.touchDownActivateCallback);
+ $(element).on('CornerstoneToolsTap', cornerstoneTools.simpleAngleTouch.tapCallback);
+ cornerstone.updateImage(element);
+ });
+ });
+ }
+
+ cornerstoneTools.simpleAngle = cornerstoneTools.mouseButtonTool({
+ createNewMeasurement: createNewMeasurement,
+ addNewMeasurement: addNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ toolType: toolType
+ });
+
+ cornerstoneTools.simpleAngleTouch = cornerstoneTools.touchTool({
+ createNewMeasurement: createNewMeasurement,
+ addNewMeasurement: addNewMeasurementTouch,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ toolType: toolType
+ });
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/imageTools/simpleAngle.js
+
+// Begin Source: src/imageTools/textMarker.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ var toolType = 'textMarker';
+
+ ///////// BEGIN ACTIVE TOOL ///////
+ function createNewMeasurement(mouseEventData) {
+ var config = cornerstoneTools.textMarker.getConfiguration();
+
+ if (!config.current) {
+ return;
+ }
+
+ // create the measurement data for this tool with the end handle activated
+ var measurementData = {
+ visible: true, active: true, text: config.current, handles: {
+ end: {
+ x: mouseEventData.currentPoints.image.x,
+ y: mouseEventData.currentPoints.image.y,
+ highlight: true,
+ active: true
+ }
+ }
+ };
+
+ // Update the current marker for the next marker
+ var currentIndex = config.markers.indexOf(config.current);
+ if (config.ascending) {
+ currentIndex += 1;
+ if (currentIndex >= config.markers.length) {
+ if (!config.loop) {
+ currentIndex = -1;
+ } else {
+ currentIndex -= config.markers.length;
+ }
+ }
+ } else {
+ currentIndex -= 1;
+ if (currentIndex < 0) {
+ if (!config.loop) {
+ currentIndex = -1;
+ } else {
+ currentIndex += config.markers.length;
+ }
+ }
+ }
+
+ config.current = config.markers[currentIndex];
+
+ return measurementData;
+ }
+ ///////// END ACTIVE TOOL ///////
+
+ ///////// BEGIN IMAGE RENDERING ///////
+ function pointNearTool(element, data, coords) {
+ var endCanvas = cornerstone.pixelToCanvas(element, data.handles.end);
+
+ var rect = {
+ left: endCanvas.x - data.textWidth / 2,
+ top: endCanvas.y,
+ width: data.textWidth,
+ height: data.textHeight
+ };
+
+ var distanceToPoint = cornerstoneMath.rect.distanceToPoint(rect, coords);
+ return (distanceToPoint < 10);
+ }
+
+ function onImageRendered(e, eventData) {
+ // if we have no toolData for this element, return immediately as there is nothing to do
+ var toolData = cornerstoneTools.getToolState(eventData.element, toolType);
+ if (!toolData) {
+ return;
+ }
+
+ // 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 font = cornerstoneTools.textStyle.getFont();
+ var fontSize = cornerstoneTools.textStyle.getFontSize();
+
+ for (var i = 0; i < toolData.data.length; i++) {
+ var data = toolData.data[i];
+
+ var color = cornerstoneTools.toolColors.getToolColor();
+ if (data.active) {
+ color = cornerstoneTools.toolColors.getActiveColor();
+ }
+
+ context.save();
+
+ // Draw text
+ context.font = font;
+ context.fillStyle = color;
+
+ var measureText = context.measureText(data.text);
+ data.textWidth = measureText.width;
+ data.textHeight = fontSize;
+
+ var coords = {
+ x: data.handles.end.x,
+ y: data.handles.end.y
+ };
+
+ var textCoords = cornerstone.pixelToCanvas(eventData.element, coords);
+
+ cornerstoneTools.drawTextBox(context, data.text, textCoords.x - data.textWidth / 2, textCoords.y, color);
+
+ context.restore();
+ }
+ }
+
+ 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);
+ $(element).off('CornerstoneToolsMouseMove', cornerstoneTools.textMarker.mouseMoveCallback);
+ $(element).off('CornerstoneToolsMouseDown', cornerstoneTools.textMarker.mouseDownCallback);
+ $(element).off('CornerstoneToolsMouseDownActivate', cornerstoneTools.textMarker.mouseDownActivateCallback);
+ }
+
+ if (e.data && e.data.mouseButtonMask && !cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
+ return false;
+ }
+
+ var config = cornerstoneTools.textMarker.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);
+
+ $(element).off('CornerstoneToolsMouseMove', cornerstoneTools.textMarker.mouseMoveCallback);
+ $(element).off('CornerstoneToolsMouseDown', cornerstoneTools.textMarker.mouseDownCallback);
+ $(element).off('CornerstoneToolsMouseDownActivate', cornerstoneTools.textMarker.mouseDownActivateCallback);
+ // 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 touchPressCallback(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('CornerstoneToolsTouchDrag', cornerstoneTools.textMarkerTouch.touchMoveCallback);
+ $(element).on('CornerstoneToolsTouchStartActive', cornerstoneTools.textMarkerTouch.touchDownActivateCallback);
+ $(element).on('CornerstoneToolsTouchStart', cornerstoneTools.textMarkerTouch.touchStartCallback);
+ $(element).on('CornerstoneToolsTap', cornerstoneTools.textMarkerTouch.tapCallback);
+ $(element).on('CornerstoneToolsTouchPress', cornerstoneTools.textMarkerTouch.pressCallback);
+ }
+
+ if (e.data && e.data.mouseButtonMask && !cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
+ return false;
+ }
+
+ var config = cornerstoneTools.textMarker.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);
+
+ $(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);
+ // Allow relabelling via a callback
+ config.changeTextCallback(data, doneChangingTextCallback);
+
+ e.stopImmediatePropagation();
+ return false;
+ }
+ }
+
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+
+ cornerstoneTools.textMarker = cornerstoneTools.mouseButtonTool({
+ createNewMeasurement: createNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ toolType: toolType,
+ mouseDoubleClickCallback: doubleClickCallback
+ });
+
+ cornerstoneTools.textMarkerTouch = cornerstoneTools.touchTool({
+ createNewMeasurement: createNewMeasurement,
+ onImageRendered: onImageRendered,
+ pointNearTool: pointNearTool,
+ toolType: toolType,
+ pressCallback: touchPressCallback
+ });
+
+ ///////// END IMAGE RENDERING ///////
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/textMarker.js
+
+// Begin Source: src/imageTools/wwwc.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function mouseUpCallback(e, eventData) {
+ $(eventData.element).off('CornerstoneToolsMouseDrag', mouseDragCallback);
+ $(eventData.element).off('CornerstoneToolsMouseUp', mouseUpCallback);
+ }
+
+ function mouseDownCallback(e, eventData) {
+ if (cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
+ $(eventData.element).on('CornerstoneToolsMouseDrag', mouseDragCallback);
+ $(eventData.element).on('CornerstoneToolsMouseUp', mouseUpCallback);
+ return false; // false = cases jquery to preventDefault() and stopPropagation() this event
+ }
+ }
+
+ function defaultStrategy(eventData) {
+ // here we normalize the ww/wc adjustments so the same number of on screen pixels
+ // adjusts the same percentage of the dynamic range of the image. This is needed to
+ // provide consistency for the ww/wc tool regardless of the dynamic range (e.g. an 8 bit
+ // image will feel the same as a 16 bit image would)
+ var maxVOI = eventData.image.maxPixelValue * eventData.image.slope + eventData.image.intercept;
+ var minVOI = eventData.image.minPixelValue * eventData.image.slope + eventData.image.intercept;
+ var imageDynamicRange = maxVOI - minVOI;
+ var multiplier = imageDynamicRange / 1024;
+
+ var deltaX = eventData.deltaPoints.page.x * multiplier;
+ var deltaY = eventData.deltaPoints.page.y * multiplier;
+
+ eventData.viewport.voi.windowWidth += (deltaX);
+ eventData.viewport.voi.windowCenter += (deltaY);
+ }
+
+ function mouseDragCallback(e, eventData) {
+ cornerstoneTools.wwwc.strategy(eventData);
+ cornerstone.setViewport(eventData.element, eventData.viewport);
+ return false; // false = cases jquery to preventDefault() and stopPropagation() this event
+ }
+
+ function touchDragCallback(e, eventData) {
+ var dragData = eventData;
+
+ var maxVOI = dragData.image.maxPixelValue * dragData.image.slope + dragData.image.intercept;
+ var minVOI = dragData.image.minPixelValue * dragData.image.slope + dragData.image.intercept;
+ var imageDynamicRange = maxVOI - minVOI;
+ var multiplier = imageDynamicRange / 1024;
+ var deltaX = dragData.deltaPoints.page.x * multiplier;
+ var deltaY = dragData.deltaPoints.page.y * multiplier;
+
+ var config = cornerstoneTools.wwwc.getConfiguration();
+ if (config.orientation) {
+ if (config.orientation === 0) {
+ dragData.viewport.voi.windowWidth += (deltaX);
+ dragData.viewport.voi.windowCenter += (deltaY);
+ } else {
+ dragData.viewport.voi.windowWidth += (deltaY);
+ dragData.viewport.voi.windowCenter += (deltaX);
+ }
+ } else {
+ dragData.viewport.voi.windowWidth += (deltaX);
+ dragData.viewport.voi.windowCenter += (deltaY);
+ }
+
+ cornerstone.setViewport(dragData.element, dragData.viewport);
+ }
+
+ cornerstoneTools.wwwc = cornerstoneTools.simpleMouseButtonTool(mouseDownCallback);
+ cornerstoneTools.wwwc.strategies = {
+ default: defaultStrategy
+ };
+ cornerstoneTools.wwwc.strategy = defaultStrategy;
+ cornerstoneTools.wwwcTouchDrag = cornerstoneTools.touchDragTool(touchDragCallback);
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/wwwc.js
+
+// Begin Source: src/imageTools/wwwcRegion.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ var toolType = 'wwwcRegion';
+
+ var configuration = {
+ minWindowWidth: 10
+ };
+
+ /** Calculates the minimum, maximum, and mean value in the given pixel array */
+ function calculateMinMaxMean(storedPixelLuminanceData, globalMin, globalMax) {
+ var numPixels = storedPixelLuminanceData.length;
+
+ if (numPixels < 2) {
+ return {
+ min: globalMin,
+ max: globalMax,
+ mean: globalMin
+ };
+ }
+
+ var min = globalMax;
+ var max = globalMin;
+ var sum = 0;
+
+ for (var index = 0; index < numPixels; index++) {
+ var spv = storedPixelLuminanceData[index];
+ min = Math.min(min, spv);
+ max = Math.max(max, spv);
+ sum += spv;
+ }
+
+ return {
+ min: min,
+ max: max,
+ mean: sum / numPixels
+ };
+ }
+
+ /* Applies the windowing procedure when the mouse drag ends */
+ function dragEndCallback(e, eventData) {
+ $(eventData.element).off('CornerstoneToolsMouseMove', dragCallback);
+ $(eventData.element).off('CornerstoneToolsMouseDrag', dragCallback);
+ $(eventData.element).off('CornerstoneToolsMouseUp', dragEndCallback);
+ $(eventData.element).off('CornerstoneToolsMouseClick', dragEndCallback);
+
+ var toolData = cornerstoneTools.getToolState(eventData.element, toolType);
+ if (!toolData) {
+ return;
+ }
+
+ if (!toolData.data.length) {
+ return;
+ }
+
+ // Update the endpoint as the mouse/touch is dragged
+ var endPoint = {
+ x: eventData.currentPoints.image.x,
+ y: eventData.currentPoints.image.y
+ };
+
+ toolData.data[0].endPoint = endPoint;
+
+ applyWWWCRegion(eventData);
+
+ var mouseData = {
+ mouseButtonMask: eventData.which
+ };
+
+ $(eventData.element).on('CornerstoneToolsMouseDown', mouseData, mouseDownCallback);
+ }
+
+ /** Calculates the minimum and maximum value in the given pixel array */
+ function applyWWWCRegion(eventData) {
+ var toolData = cornerstoneTools.getToolState(eventData.element, toolType);
+ if (toolData === undefined) {
+ return;
+ }
+
+ if (!toolData.data.length) {
+ return;
+ }
+
+ var startPoint = toolData.data[0].startPoint;
+ var endPoint = toolData.data[0].endPoint;
+
+ // Get the rectangular region defined by the handles
+ var width = Math.abs(startPoint.x - endPoint.x);
+ var height = Math.abs(startPoint.y - endPoint.y);
+
+ var left = Math.min(startPoint.x, endPoint.x);
+ var top = Math.min(startPoint.y, endPoint.y);
+
+ // Bound the rectangle so we don't get undefined pixels
+ left = Math.max(left, 0);
+ left = Math.min(left, eventData.image.width);
+ top = Math.max(top, 0);
+ top = Math.min(top, eventData.image.height);
+ width = Math.floor(Math.min(width, Math.abs(eventData.image.width - left)));
+ height = Math.floor(Math.min(height, Math.abs(eventData.image.height - top)));
+
+ // Get the pixel data in the rectangular region
+ var pixelLuminanceData = cornerstoneTools.getLuminance(eventData.element, left, top, width, height);
+
+ // Calculate the minimum and maximum pixel values
+ var minMaxMean = calculateMinMaxMean(pixelLuminanceData, eventData.image.minPixelValue, eventData.image.maxPixelValue);
+
+ // Adjust the viewport window width and center based on the calculated values
+ var config = cornerstoneTools.wwwcRegion.getConfiguration();
+ var viewport = cornerstone.getViewport(eventData.element);
+ viewport.voi.windowWidth = Math.max(Math.abs(minMaxMean.max - minMaxMean.min), config.minWindowWidth);
+ viewport.voi.windowCenter = minMaxMean.mean;
+ cornerstone.setViewport(eventData.element, viewport);
+
+ // Clear the toolData
+ toolData.data = [];
+
+ cornerstone.updateImage(eventData.element);
+ }
+
+ function whichMovement(e, eventData) {
+ var element = eventData.element;
+
+ $(element).off('CornerstoneToolsMouseMove');
+ $(element).off('CornerstoneToolsMouseDrag');
+
+ $(element).on('CornerstoneToolsMouseMove', dragCallback);
+ $(element).on('CornerstoneToolsMouseDrag', dragCallback);
+
+ $(element).on('CornerstoneToolsMouseClick', dragEndCallback);
+ if (e.type === 'CornerstoneToolsMouseDrag') {
+ $(element).on('CornerstoneToolsMouseUp', dragEndCallback);
+ }
+ }
+
+ /** Records the start point and attaches the drag event handler */
+ function mouseDownCallback(e, eventData) {
+ if (cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
+ $(eventData.element).on('CornerstoneToolsMouseDrag', eventData, whichMovement);
+ $(eventData.element).on('CornerstoneToolsMouseMove', eventData, whichMovement);
+ $(eventData.element).on('CornerstoneToolsMouseUp', dragEndCallback);
+
+ $(eventData.element).off('CornerstoneToolsMouseDown');
+ recordStartPoint(eventData);
+ return false;
+ }
+ }
+
+ /** Records the start point of the click or touch */
+ function recordStartPoint(eventData) {
+ var toolData = cornerstoneTools.getToolState(eventData.element, toolType);
+ if (toolData && toolData.data) {
+ toolData.data = [];
+ }
+
+ var measurementData = {
+ startPoint: {
+ x: eventData.currentPoints.image.x,
+ y: eventData.currentPoints.image.y
+ }
+ };
+
+ cornerstoneTools.addToolState(eventData.element, toolType, measurementData);
+ }
+
+ /** Draws the rectangular region while the touch or mouse event drag occurs */
+ function dragCallback(e, eventData) {
+ // if we have no toolData for this element, return immediately as there is nothing to do
+ var toolData = cornerstoneTools.getToolState(eventData.element, toolType);
+ if (toolData === undefined) {
+ return;
+ }
+
+ if (!toolData.data.length) {
+ return;
+ }
+
+ // Update the endpoint as the mouse/touch is dragged
+ var endPoint = {
+ x: eventData.currentPoints.image.x,
+ y: eventData.currentPoints.image.y
+ };
+
+ toolData.data[0].endPoint = endPoint;
+ cornerstone.updateImage(eventData.element);
+ }
+
+ function onImageRendered(e, eventData) {
+ var toolData = cornerstoneTools.getToolState(eventData.element, toolType);
+ if (toolData === undefined) {
+ return;
+ }
+
+ if (!toolData.data.length) {
+ return;
+ }
+
+ var startPoint = toolData.data[0].startPoint;
+ var endPoint = toolData.data[0].endPoint;
+
+ if (!startPoint || !endPoint) {
+ return;
+ }
+
+ // Get the current element's canvas
+ var canvas = $(eventData.element).find('canvas').get(0);
+ var context = canvas.getContext('2d');
+ context.setTransform(1, 0, 0, 1, 0, 0);
+
+ // Set to the active tool color
+ var color = cornerstoneTools.toolColors.getActiveColor();
+
+ // Calculate the rectangle parameters
+ var startPointCanvas = cornerstone.pixelToCanvas(eventData.element, startPoint);
+ var endPointCanvas = cornerstone.pixelToCanvas(eventData.element, endPoint);
+
+ var left = Math.min(startPointCanvas.x, endPointCanvas.x);
+ var top = Math.min(startPointCanvas.y, endPointCanvas.y);
+ var width = Math.abs(startPointCanvas.x - endPointCanvas.x);
+ var height = Math.abs(startPointCanvas.y - endPointCanvas.y);
+
+ var lineWidth = cornerstoneTools.toolStyle.getToolWidth();
+
+ // Draw the rectangle
+ context.save();
+
+ context.beginPath();
+ context.strokeStyle = color;
+ context.lineWidth = lineWidth;
+ context.rect(left, top, width, height);
+ context.stroke();
+
+ context.restore();
+ }
+
+ // --- Mouse tool enable / disable --- ///
+ function disable(element) {
+ $(element).off('CornerstoneToolsMouseDown', mouseDownCallback);
+ $(element).off('CornerstoneImageRendered', onImageRendered);
+
+ cornerstone.updateImage(element);
+ }
+
+ function activate(element, mouseButtonMask) {
+ var eventData = {
+ mouseButtonMask: mouseButtonMask,
+ };
+
+ var toolData = cornerstoneTools.getToolState(element, toolType);
+ if (!toolData) {
+ var data = [];
+ cornerstoneTools.addToolState(element, toolType, data);
+ }
+
+ $(element).off('CornerstoneToolsMouseDown', mouseDownCallback);
+ $(element).off('CornerstoneToolsMouseUp', dragEndCallback);
+ $(element).off('CornerstoneToolsMouseDrag', dragCallback);
+ $(element).off('CornerstoneImageRendered', onImageRendered);
+
+ $(element).on('CornerstoneToolsMouseDown', eventData, mouseDownCallback);
+ $(element).on('CornerstoneImageRendered', onImageRendered);
+ cornerstone.updateImage(element);
+ }
+
+ // --- Touch tool enable / disable --- //
+ function disableTouchDrag(element) {
+ $(element).off('CornerstoneToolsTouchDrag', dragCallback);
+ $(element).off('CornerstoneToolsTouchStart', recordStartPoint);
+ $(element).off('CornerstoneToolsDragEnd', applyWWWCRegion);
+ $(element).off('CornerstoneImageRendered', onImageRendered);
+ }
+
+ function activateTouchDrag(element) {
+ var toolData = cornerstoneTools.getToolState(element, toolType);
+ if (toolData === undefined) {
+ var data = [];
+ cornerstoneTools.addToolState(element, toolType, data);
+ }
+
+ $(element).off('CornerstoneToolsTouchDrag', dragCallback);
+ $(element).off('CornerstoneToolsTouchStart', recordStartPoint);
+ $(element).off('CornerstoneToolsDragEnd', applyWWWCRegion);
+ $(element).off('CornerstoneImageRendered', onImageRendered);
+
+ $(element).on('CornerstoneToolsTouchDrag', dragCallback);
+ $(element).on('CornerstoneToolsTouchStart', recordStartPoint);
+ $(element).on('CornerstoneToolsDragEnd', applyWWWCRegion);
+ $(element).on('CornerstoneImageRendered', onImageRendered);
+ }
+
+ function getConfiguration() {
+ return configuration;
+ }
+
+ function setConfiguration(config) {
+ configuration = config;
+ }
+
+ // module exports
+ cornerstoneTools.wwwcRegion = {
+ activate: activate,
+ deactivate: disable,
+ disable: disable,
+ setConfiguration: setConfiguration,
+ getConfiguration: getConfiguration
+ };
+
+ cornerstoneTools.wwwcRegionTouch = {
+ activate: activateTouchDrag,
+ deactivate: disableTouchDrag,
+ disable: disableTouchDrag
+ };
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/imageTools/wwwcRegion.js
+
+// Begin Source: src/imageTools/zoom.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ var startPoints;
+
+ function changeViewportScale(viewport, ticks) {
+ var config = cornerstoneTools.zoom.getConfiguration();
+ var pow = 1.7;
+
+ var oldFactor = Math.log(viewport.scale) / Math.log(pow);
+ var factor = oldFactor + ticks;
+
+ var scale = Math.pow(pow, factor);
+ if (config.maxScale && scale > config.maxScale) {
+ viewport.scale = config.maxScale;
+ } else if (config.minScale && scale < config.minScale) {
+ viewport.scale = config.minScale;
+ } else {
+ viewport.scale = scale;
+ }
+
+ return viewport;
+ }
+
+ function boundPosition(position, width, height) {
+ position.x = Math.max(position.x, 0);
+ position.y = Math.max(position.y, 0);
+ position.x = Math.min(position.x, width);
+ position.y = Math.min(position.y, height);
+ return position;
+ }
+
+ function correctShift(shift, viewport) {
+ // Apply rotations
+ if (viewport.rotation !== 0) {
+ var angle = viewport.rotation * Math.PI / 180;
+
+ var cosA = Math.cos(angle);
+ var sinA = Math.sin(angle);
+
+ var newX = shift.x * cosA - shift.y * sinA;
+ var newY = shift.x * sinA + shift.y * cosA;
+
+ shift.x = newX;
+ shift.y = newY;
+ }
+
+ // Apply Flips
+ if (viewport.hflip) {
+ shift.x *= -1;
+ }
+
+ if (viewport.vflip) {
+ shift.y *= -1;
+ }
+
+ return shift;
+ }
+
+ function defaultStrategy(eventData, ticks) {
+ var element = eventData.element;
+
+ // Calculate the new scale factor based on how far the mouse has changed
+ var viewport = changeViewportScale(eventData.viewport, ticks);
+ cornerstone.setViewport(element, viewport);
+
+ // Now that the scale has been updated, determine the offset we need to apply to the center so we can
+ // keep the original start location in the same position
+ var newCoords = cornerstone.pageToPixel(element, eventData.startPoints.page.x, eventData.startPoints.page.y);
+ var shift = {
+ x: eventData.startPoints.image.x - newCoords.x,
+ y: eventData.startPoints.image.y - newCoords.y
+ };
+
+ shift = correctShift(shift, viewport);
+ viewport.translation.x -= shift.x;
+ viewport.translation.y -= shift.y;
+ cornerstone.setViewport(element, viewport);
+ }
+
+ function translateStrategy(eventData, ticks) {
+ var element = eventData.element;
+ var image = eventData.image;
+
+ // Calculate the new scale factor based on how far the mouse has changed
+ var viewport = changeViewportScale(eventData.viewport, ticks);
+ cornerstone.setViewport(element, viewport);
+
+ var config = cornerstoneTools.zoom.getConfiguration();
+ var shift,
+ newCoords;
+
+ var outwardsTranslateSpeed = 8;
+ var inwardsTranslateSpeed = 8;
+ var outwardsMinScaleToTranslate = 3;
+ var minTranslation = 0.01;
+
+ if (ticks < 0) {
+ // Zoom outwards from the image center
+ shift = {
+ x: viewport.scale < outwardsMinScaleToTranslate ? viewport.translation.x / outwardsTranslateSpeed : 0,
+ y: viewport.scale < outwardsMinScaleToTranslate ? viewport.translation.y / outwardsTranslateSpeed : 0
+ };
+
+ if (Math.abs(viewport.translation.x) < minTranslation) {
+ viewport.translation.x = 0;
+ shift.x = 0;
+ } else if (Math.abs(viewport.translation.y) < minTranslation) {
+ viewport.translation.y = 0;
+ shift.y = 0;
+ } else if (Math.abs(viewport.translation.x) < minTranslation &&
+ Math.abs(viewport.translation.y) < minTranslation) {
+ cornerstone.setViewport(element, viewport);
+ return false;
+ }
+ } else {
+ newCoords = cornerstone.pageToPixel(element, startPoints.page.x, startPoints.page.y);
+ if (config && config.preventZoomOutsideImage) {
+ startPoints.image = boundPosition(startPoints.image, image.width, image.height);
+ newCoords = boundPosition(newCoords, image.width, image.height);
+ }
+ // Zoom inwards to the current image point
+ var desiredTranslation = {
+ x: image.width / 2 - startPoints.image.x,
+ y: image.height / 2 - startPoints.image.y
+ };
+
+ var distanceToDesired = {
+ x: viewport.translation.x - desiredTranslation.x,
+ y: viewport.translation.y - desiredTranslation.y
+ };
+
+ shift = {
+ x: distanceToDesired.x / inwardsTranslateSpeed,
+ y: distanceToDesired.y / inwardsTranslateSpeed
+ };
+
+ if (Math.abs(distanceToDesired.x) < minTranslation) {
+ viewport.translation.x = desiredTranslation.x;
+ shift.x = 0;
+ } else if (Math.abs(distanceToDesired.y) < minTranslation) {
+ viewport.translation.y = desiredTranslation.y;
+ shift.y = 0;
+ } else if (Math.abs(distanceToDesired.x) < minTranslation &&
+ Math.abs(distanceToDesired.y) < minTranslation) {
+ cornerstone.setViewport(element, viewport);
+ return false;
+ }
+ }
+
+ shift = correctShift(shift, viewport);
+ if (!shift.x && !shift.y) {
+ return false;
+ }
+
+ viewport.translation.x -= shift.x;
+ viewport.translation.y -= shift.y;
+ cornerstone.setViewport(element, viewport);
+ }
+
+ function mouseUpCallback(e, eventData) {
+ $(eventData.element).off('CornerstoneToolsMouseDrag', dragCallback);
+ $(eventData.element).off('CornerstoneToolsMouseUp', mouseUpCallback);
+ }
+
+ function mouseDownCallback(e, eventData) {
+ if (cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
+ startPoints = eventData.startPoints; // Used for translateStrategy
+ $(eventData.element).on('CornerstoneToolsMouseDrag', dragCallback);
+ $(eventData.element).on('CornerstoneToolsMouseUp', mouseUpCallback);
+ return false; // false = cases jquery to preventDefault() and stopPropagation() this event
+ }
+ }
+
+ function dragCallback(e, eventData) {
+ if (!eventData.deltaPoints.page.y) {
+ return false;
+ }
+
+ var ticks = eventData.deltaPoints.page.y / 100;
+ cornerstoneTools.zoom.strategy(eventData, ticks);
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+
+ function mouseWheelCallback(e, eventData) {
+ var ticks = -eventData.direction / 4;
+ var viewport = changeViewportScale(eventData.viewport, ticks);
+ cornerstone.setViewport(eventData.element, viewport);
+ }
+
+ function touchPinchCallback(e, eventData) {
+ 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 = config.maxScale;
+ } else if (config.minScale && scale < config.minScale) {
+ viewport.scale = config.minScale;
+ } else {
+ viewport.scale = scale;
+ }
+
+ cornerstone.setViewport(element, viewport);
+
+ // Now that the scale has been updated, determine the offset we need to apply to the center so we can
+ // keep the original start location in the same position
+ var newCoords = cornerstone.pageToPixel(element, eventData.startPoints.page.x, eventData.startPoints.page.y);
+ var shift = {
+ x: eventData.startPoints.image.x - newCoords.x,
+ y: eventData.startPoints.image.y - newCoords.y
+ };
+
+ shift = correctShift(shift, viewport);
+ viewport.translation.x -= shift.x;
+ viewport.translation.y -= shift.y;
+ cornerstone.setViewport(element, viewport);
+ }
+
+ cornerstoneTools.zoom = cornerstoneTools.simpleMouseButtonTool(mouseDownCallback);
+ cornerstoneTools.zoom.strategies = {
+ default: defaultStrategy,
+ translate: translateStrategy
+ };
+ cornerstoneTools.zoom.strategy = defaultStrategy;
+
+ cornerstoneTools.zoomWheel = cornerstoneTools.mouseWheelTool(mouseWheelCallback);
+ cornerstoneTools.zoomTouchPinch = cornerstoneTools.touchPinchTool(touchPinchCallback);
+ cornerstoneTools.zoomTouchDrag = cornerstoneTools.touchDragTool(dragCallback);
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/imageTools/zoom.js
+
+// Begin Source: src/inputSources/keyboardInput.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ var mouseX, mouseY;
+
+ function keyPress(e) {
+ var element = e.currentTarget;
+ var startingCoords = cornerstone.pageToPixel(element, mouseX, mouseY);
+
+ 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
+ };
+
+ if (e.type === 'keydown') {
+ $(element).trigger('CornerstoneToolsKeyDown', keyPressData);
+ } else if (e.type === 'keypress') {
+ $(element).trigger('CornerstoneToolsKeyPress', keyPressData);
+ } else if (e.type === 'keyup') {
+ $(element).trigger('CornerstoneToolsKeyUp', keyPressData);
+ }
+ }
+
+ function mouseMove(e) {
+ mouseX = e.pageX || e.originalEvent.pageX;
+ mouseY = e.pageY || e.originalEvent.pageY;
+ }
+
+ var keyboardEvent = 'keydown keypress keyup';
+
+ function enable(element) {
+ $(element).bind(keyboardEvent, keyPress);
+ $(element).on('mousemove', mouseMove);
+ }
+
+ function disable(element) {
+ $(element).unbind(keyboardEvent, keyPress);
+ }
+
+ // module exports
+ cornerstoneTools.keyboardInput = {
+ enable: enable, disable: disable
+ };
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/inputSources/keyboardInput.js
+
+// Begin Source: src/inputSources/preventGhostClick.js
+(function(cornerstoneTools) {
+
+ 'use strict';
+
+ // Functions to prevent ghost clicks following a touch
+ // All credit to @kosich
+ // https://gist.github.com/kosich/23188dd86633b6c2efb7
+
+ var antiGhostDelay = 2000;
+
+ 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;
+ }
+
+ lastInteractionTime = now;
+ }
+
+ 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);
+ }
+
+ cornerstoneTools.preventGhostClick = preventGhostClick;
+
+})(cornerstoneTools);
+
+// End Source; src/inputSources/preventGhostClick.js
+
+// Begin Source: src/manipulators/anyHandlesOutsideImage.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ function anyHandlesOutsideImage(renderData, handles) {
+ var image = renderData.image;
+ var imageRect = {
+ left: 0,
+ top: 0,
+ width: image.width,
+ height: image.height
+ };
+
+ var handleOutsideImage = false;
+
+ Object.keys(handles).forEach(function(name) {
+ var handle = handles[name];
+ if (cornerstoneMath.point.insideRect(handle, imageRect) === false) {
+ handleOutsideImage = true;
+ }
+ });
+
+ return handleOutsideImage;
+ }
+
+ // module/private exports
+ cornerstoneTools.anyHandlesOutsideImage = anyHandlesOutsideImage;
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/manipulators/anyHandlesOutsideImage.js
+
+// Begin Source: src/manipulators/drawHandles.js
+(function(cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ var handleRadius = 6;
+
+ function drawHandles(context, renderData, handles, color, fill) {
+ context.strokeStyle = color;
+
+ Object.keys(handles).forEach(function(name) {
+ var handle = handles[name];
+ context.beginPath();
+
+ if (handle.active) {
+ context.lineWidth = cornerstoneTools.toolStyle.getActiveWidth();
+ } else {
+ context.lineWidth = cornerstoneTools.toolStyle.getToolWidth();
+ }
+
+ var handleCanvasCoords = cornerstone.pixelToCanvas(renderData.element, handle);
+ context.arc(handleCanvasCoords.x, handleCanvasCoords.y, handleRadius, 0, 2 * Math.PI);
+
+ if (fill) {
+ context.fillStyle = fill;
+ context.fill();
+ }
+
+ context.stroke();
+ });
+ }
+
+ // module/private exports
+ cornerstoneTools.drawHandles = drawHandles;
+
+})(cornerstone, cornerstoneTools);
+
+// End Source; src/manipulators/drawHandles.js
+
+// Begin Source: src/manipulators/getHandleNearImagePoint.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ function getHandleNearImagePoint(element, data, coords, distanceSq) {
+ var nearbyHandle;
+
+ if (!data.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;
+ }
+ });
+
+ return nearbyHandle;
+ }
+
+ // module exports
+ cornerstoneTools.getHandleNearImagePoint = getHandleNearImagePoint;
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/manipulators/getHandleNearImagePoint.js
+
+// Begin Source: src/manipulators/handleActivator.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ '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;
+
+ Object.keys(handles).forEach(function(name) {
+ var handle = handles[name];
+ if (handle.active === true) {
+ activeHandle = handle;
+ return;
+ }
+ });
+
+ return activeHandle;
+ }
+
+ function handleActivator(element, handles, canvasPoint, distanceThreshold) {
+ if (!distanceThreshold) {
+ distanceThreshold = 36;
+ }
+
+ var activeHandle = getActiveHandle(handles);
+ var nearbyHandle = findHandleNear(element, handles, canvasPoint, distanceThreshold);
+ if (activeHandle !== nearbyHandle) {
+ if (nearbyHandle !== undefined) {
+ nearbyHandle.active = true;
+ }
+
+ if (activeHandle !== undefined) {
+ activeHandle.active = false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ // module/private exports
+ cornerstoneTools.handleActivator = handleActivator;
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/manipulators/handleActivator.js
+
+// Begin Source: src/manipulators/moveAllHandles.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ function moveAllHandles(mouseEventData, data, toolData, options, doneMovingCallback) {
+ var element = mouseEventData.element;
+
+ function mouseDragCallback(e, eventData) {
+ data.active = true;
+
+ Object.keys(data.handles).forEach(function(name) {
+ var handle = data.handles[name];
+ handle.x += eventData.deltaPoints.image.x;
+ handle.y += eventData.deltaPoints.image.y;
+
+ if (options.preventHandleOutsideImage === true) {
+ handle.x = Math.max(handle.x, 0);
+ handle.x = Math.min(handle.x, eventData.image.width);
+
+ handle.y = Math.max(handle.y, 0);
+ handle.y = Math.min(handle.y, eventData.image.height);
+ }
+ });
+ cornerstone.updateImage(element);
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+
+ $(element).on('CornerstoneToolsMouseDrag', mouseDragCallback);
+
+ function mouseUpCallback(e, eventData) {
+ data.active = false;
+ data.invalidated = true;
+
+ $(element).off('CornerstoneToolsMouseDrag', mouseDragCallback);
+ $(element).off('CornerstoneToolsMouseUp', mouseUpCallback);
+ $(element).off('CornerstoneToolsMouseClick', mouseUpCallback);
+
+ // If any handle is outside the image, delete the tool data
+ if (options.deleteIfHandleOutsideImage === true) {
+ var image = eventData.image;
+ var handleOutsideImage = false;
+ var rect = {
+ top: 0, left: 0, width: image.width, height: image.height
+ };
+
+ Object.keys(data.handles).forEach(function(name) {
+ var handle = data.handles[name];
+ handle.active = false;
+ if (cornerstoneMath.point.insideRect(handle, rect) === false) {
+ handleOutsideImage = true;
+ return;
+ }
+ });
+
+ if (handleOutsideImage) {
+ // find this tool data
+ var indexOfData = -1;
+ toolData.data.forEach(function(thisToolData, index) {
+ if (thisToolData === data) {
+ indexOfData = index;
+ return;
+ }
+ });
+
+ if (indexOfData !== -1) {
+ toolData.data.splice(indexOfData, 1);
+ }
+ }
+ }
+
+ cornerstone.updateImage(element);
+ doneMovingCallback();
+ }
+
+ $(element).on('CornerstoneToolsMouseUp', mouseUpCallback);
+ $(element).on('CornerstoneToolsMouseClick', mouseUpCallback);
+ return true;
+ }
+
+ // module/private exports
+ cornerstoneTools.moveAllHandles = moveAllHandles;
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/manipulators/moveAllHandles.js
+
+// Begin Source: src/manipulators/moveHandle.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function moveHandle(mouseEventData, handle, doneMovingCallback, preventHandleOutsideImage) {
+ var element = mouseEventData.element;
+
+ function mouseDragCallback(e, eventData) {
+ handle.active = true;
+ handle.x = eventData.currentPoints.image.x;
+ handle.y = eventData.currentPoints.image.y;
+ if (preventHandleOutsideImage) {
+ handle.x = Math.max(handle.x, 0);
+ handle.x = Math.min(handle.x, eventData.image.width);
+
+ handle.y = Math.max(handle.y, 0);
+ handle.y = Math.min(handle.y, eventData.image.height);
+ }
+
+ cornerstone.updateImage(element);
+ }
+
+ $(element).on('CornerstoneToolsMouseDrag', mouseDragCallback);
+
+ function mouseUpCallback() {
+ handle.active = false;
+ $(element).off('CornerstoneToolsMouseDrag', mouseDragCallback);
+ $(element).off('CornerstoneToolsMouseUp', mouseUpCallback);
+ $(element).off('CornerstoneToolsMouseClick', mouseUpCallback);
+ cornerstone.updateImage(element);
+
+ doneMovingCallback();
+ }
+
+ $(element).on('CornerstoneToolsMouseUp', mouseUpCallback);
+ $(element).on('CornerstoneToolsMouseClick', mouseUpCallback);
+ }
+
+ // module/private exports
+ cornerstoneTools.moveHandle = moveHandle;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/manipulators/moveHandle.js
+
+// Begin Source: src/manipulators/moveNewHandle.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function moveNewHandle(mouseEventData, handle, doneMoveCallback, preventHandleOutsideImage) {
+ var element = mouseEventData.element;
+
+ function moveCallback(e, eventData) {
+ handle.active = true;
+ handle.x = eventData.currentPoints.image.x;
+ handle.y = eventData.currentPoints.image.y;
+
+ if (preventHandleOutsideImage) {
+ handle.x = Math.max(handle.x, 0);
+ handle.x = Math.min(handle.x, eventData.image.width);
+
+ handle.y = Math.max(handle.y, 0);
+ handle.y = Math.min(handle.y, eventData.image.height);
+ }
+
+ cornerstone.updateImage(element);
+ }
+
+ function whichMovement(e) {
+ $(element).off('CornerstoneToolsMouseMove');
+ $(element).off('CornerstoneToolsMouseDrag');
+
+ $(element).on('CornerstoneToolsMouseMove', moveCallback);
+ $(element).on('CornerstoneToolsMouseDrag', moveCallback);
+
+ $(element).on('CornerstoneToolsMouseClick', moveEndCallback);
+ if (e.type === 'CornerstoneToolsMouseDrag') {
+ $(element).on('CornerstoneToolsMouseUp', moveEndCallback);
+ }
+ }
+
+ $(element).on('CornerstoneToolsMouseDrag', whichMovement);
+ $(element).on('CornerstoneToolsMouseMove', whichMovement);
+
+ function moveEndCallback() {
+ $(element).off('CornerstoneToolsMouseMove', moveCallback);
+ $(element).off('CornerstoneToolsMouseDrag', moveCallback);
+ $(element).off('CornerstoneToolsMouseClick', moveEndCallback);
+ $(element).off('CornerstoneToolsMouseUp', moveEndCallback);
+
+ handle.active = false;
+ cornerstone.updateImage(element);
+
+ doneMoveCallback();
+ }
+ }
+
+ // module/private exports
+ cornerstoneTools.moveNewHandle = moveNewHandle;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/manipulators/moveNewHandle.js
+
+// Begin Source: src/manipulators/moveNewHandleTouch.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function moveNewHandleTouch(eventData, handle, doneMoveCallback, preventHandleOutsideImage) {
+ var element = eventData.element;
+ var distanceFromTouch = {
+ x: handle.x - eventData.currentPoints.image.x,
+ y: handle.y - eventData.currentPoints.image.y
+ };
+
+ function moveCallback(e, eventData) {
+ handle.active = true;
+ handle.x = eventData.currentPoints.image.x + distanceFromTouch.x;
+ handle.y = eventData.currentPoints.image.y + distanceFromTouch.y;
+
+ if (preventHandleOutsideImage) {
+ handle.x = Math.max(handle.x, 0);
+ handle.x = Math.min(handle.x, eventData.image.width);
+
+ handle.y = Math.max(handle.y, 0);
+ handle.y = Math.min(handle.y, eventData.image.height);
+ }
+
+ cornerstone.updateImage(element);
+ }
+
+ function moveEndCallback(e, eventData) {
+ $(element).off('CornerstoneToolsTouchDrag', moveCallback);
+ $(element).off('CornerstoneToolsTouchEnd', moveEndCallback);
+ $(element).off('CornerstoneToolsDragEnd', moveEndCallback);
+ $(element).off('CornerstoneToolsTap', moveEndCallback);
+
+ handle.active = false;
+ handle.x = eventData.currentPoints.image.x + distanceFromTouch.x;
+ handle.y = eventData.currentPoints.image.y + distanceFromTouch.y;
+
+ if (preventHandleOutsideImage) {
+ handle.x = Math.max(handle.x, 0);
+ handle.x = Math.min(handle.x, eventData.image.width);
+
+ handle.y = Math.max(handle.y, 0);
+ handle.y = Math.min(handle.y, eventData.image.height);
+ }
+
+ cornerstone.updateImage(element);
+
+ doneMoveCallback();
+ }
+
+ $(element).on('CornerstoneToolsTouchDrag', moveCallback);
+ $(element).on('CornerstoneToolsTouchEnd', moveEndCallback);
+ $(element).on('CornerstoneToolsDragEnd', moveEndCallback);
+ $(element).on('CornerstoneToolsTap', moveEndCallback);
+ }
+
+ // module/private exports
+ cornerstoneTools.moveNewHandleTouch = moveNewHandleTouch;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/manipulators/moveNewHandleTouch.js
+
+// Begin Source: src/manipulators/touchMoveAllHandles.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ function touchMoveAllHandles(touchEventData, data, toolData, deleteIfHandleOutsideImage, doneMovingCallback) {
+ var element = touchEventData.element;
+
+ function touchDragCallback(e, eventData) {
+ data.active = true;
+
+ Object.keys(data.handles).forEach(function(name) {
+ var handle = data.handles[name];
+ handle.x += eventData.deltaPoints.image.x;
+ handle.y += eventData.deltaPoints.image.y;
+ });
+ cornerstone.updateImage(element);
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+
+ $(element).on('CornerstoneToolsTouchDrag', touchDragCallback);
+
+ function touchEndCallback(e, eventData) {
+ data.active = false;
+ data.invalidated = false;
+
+ $(element).off('CornerstoneToolsTouchDrag', touchDragCallback);
+ $(element).off('CornerstoneToolsDragEnd', touchEndCallback);
+ $(element).off('CornerstoneToolsTap', touchEndCallback);
+
+ // If any handle is outside the image, delete the tool data
+ if (deleteIfHandleOutsideImage === true) {
+ var image = eventData.image;
+ var handleOutsideImage = false;
+ var rect = {
+ top: 0, left: 0, width: image.width, height: image.height
+ };
+
+ Object.keys(data.handles).forEach(function(name) {
+ var handle = data.handles[name];
+ if (cornerstoneMath.point.insideRect(handle, rect) === false) {
+ handleOutsideImage = true;
+ return;
+ }
+ });
+
+ 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);
+ }
+ }
+ }
+
+ doneMovingCallback();
+ cornerstone.updateImage(element);
+ }
+
+ $(element).on('CornerstoneToolsDragEnd', touchEndCallback);
+ $(element).on('CornerstoneToolsTap', touchEndCallback);
+ return true;
+ }
+
+ // module/private exports
+ cornerstoneTools.touchMoveAllHandles = touchMoveAllHandles;
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/manipulators/touchMoveAllHandles.js
+
+// Begin Source: src/manipulators/touchMoveHandle.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function touchMoveHandle(touchEventData, handle, doneMovingCallback) {
+ var element = touchEventData.element;
+ var distanceFromTouch = {
+ x: handle.x - touchEventData.currentPoints.image.x,
+ y: handle.y - touchEventData.currentPoints.image.y
+ };
+
+ function touchDragCallback(e, eventData) {
+ handle.active = true;
+ var touchMoveData = eventData;
+ handle.x = touchMoveData.currentPoints.image.x + distanceFromTouch.x;
+ handle.y = touchMoveData.currentPoints.image.y + distanceFromTouch.y;
+ cornerstone.updateImage(element);
+ }
+
+ $(element).on('CornerstoneToolsTouchDrag', touchDragCallback);
+
+ function touchEndCallback() {
+ handle.active = false;
+ $(element).off('CornerstoneToolsTouchDrag', touchDragCallback);
+ $(element).off('CornerstoneToolsTouchEnd', touchEndCallback);
+ $(element).off('CornerstoneToolsDragEnd', touchEndCallback);
+ $(element).off('CornerstoneToolsTap', touchEndCallback);
+ cornerstone.updateImage(element);
+
+ doneMovingCallback();
+ }
+
+ $(element).on('CornerstoneToolsTouchEnd', touchEndCallback);
+ $(element).on('CornerstoneToolsDragEnd', touchEndCallback);
+ $(element).on('CornerstoneToolsTap', touchEndCallback);
+ }
+
+ // module/private exports
+ cornerstoneTools.touchMoveHandle = touchMoveHandle;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/manipulators/touchMoveHandle.js
+
+// Begin Source: src/measurementManager/lineSample.js
+(function($, cornerstoneTools) {
+
+ 'use strict';
+
+ // This object manages a collection of measurements
+ function LineSampleMeasurement() {
+
+ var that = this;
+ that.samples = [];
+
+ // adds an element as both a source and a target
+ this.set = function(samples) {
+ that.samples = samples;
+ // fire event
+ $(that).trigger('CornerstoneLineSampleUpdated');
+ };
+ }
+
+ // module/private exports
+ cornerstoneTools.LineSampleMeasurement = LineSampleMeasurement;
+
+})($, cornerstoneTools);
+
+// End Source; src/measurementManager/lineSample.js
+
+// Begin Source: src/measurementManager/measurementManager.js
+(function($, cornerstoneTools) {
+
+ 'use strict';
+
+ // This object manages a collection of measurements
+ function MeasurementManager() {
+
+ var that = this;
+ that.measurements = [];
+
+ // adds an element as both a source and a target
+ this.add = function(measurement) {
+ var index = that.measurements.push(measurement);
+ // fire event
+ var eventDetail = {
+ index: index, measurement: measurement
+ };
+ $(that).trigger('CornerstoneMeasurementAdded', eventDetail);
+ };
+
+ this.remove = function(index) {
+ var measurement = that.measurements[index];
+ that.measurements.splice(index, 1);
+ // fire event
+ var eventDetail = {
+ index: index, measurement: measurement
+ };
+ $(that).trigger('CornerstoneMeasurementRemoved', eventDetail);
+ };
+
+ }
+
+ // module/private exports
+ cornerstoneTools.MeasurementManager = new MeasurementManager();
+
+})($, cornerstoneTools);
+
+// End Source; src/measurementManager/measurementManager.js
+
+// Begin Source: src/metaData.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ // this module defines a way for tools to access various metadata about an imageId. This layer of abstraction exists
+ // so metadata can be provided to the tools in different ways (e.g. by parsing DICOM P10 or by a WADO-RS document)
+ // NOTE: We may want to push this function down into the cornerstone core library, not sure yet...
+
+ var providers = [];
+
+ function addProvider( provider) {
+ providers.push(provider);
+ }
+
+ function removeProvider( provider) {
+ var index = providers.indexOf(provider);
+ if (index === -1) {
+ return;
+ }
+
+ providers.splice(index, 1);
+ }
+
+ function getMetaData(type, imageId) {
+ var result;
+ $.each(providers, function(index, provider) {
+ result = provider(type, imageId);
+ if (result !== undefined) {
+ return true;
+ }
+ });
+ return result;
+ }
+
+ // module/private exports
+ cornerstoneTools.metaData = {
+ addProvider: addProvider,
+ removeProvider: removeProvider,
+ get: getMetaData
+ };
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/metaData.js
+
+// Begin Source: src/orientation/getOrientationString.js
+(function(cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ function getOrientationString(vector) {
+ // Thanks to David Clunie
+ // https://sites.google.com/site/dicomnotes/
+
+ var orientation = '',
+ orientationX = vector.x < 0 ? 'R' : 'L',
+ orientationY = vector.y < 0 ? 'A' : 'P',
+ orientationZ = vector.z < 0 ? 'F' : 'H';
+
+ // Should probably make this a function vector3.abs
+ var abs = new cornerstoneMath.Vector3(Math.abs(vector.x), Math.abs(vector.y), Math.abs(vector.z));
+
+ for (var i = 0; i < 3; i++) {
+ if (abs.x > 0.0001 && abs.x > abs.y && abs.x > abs.z) {
+ orientation += orientationX;
+ abs.x = 0;
+ } else if (abs.y > 0.0001 && abs.y > abs.x && abs.y > abs.z) {
+ orientation += orientationY;
+ abs.y = 0;
+ } else if (abs.z > 0.0001 && abs.z > abs.x && abs.z > abs.y) {
+ orientation += orientationZ;
+ abs.z = 0;
+ } else {
+ break;
+ }
+ }
+
+ return orientation;
+ }
+
+ // module/private exports
+ cornerstoneTools.orientation.getOrientationString = getOrientationString;
+
+})(cornerstoneMath, cornerstoneTools);
+
+// End Source; src/orientation/getOrientationString.js
+
+// Begin Source: src/orientation/invertOrientationString.js
+(function(cornerstoneTools) {
+
+ 'use strict';
+
+ function invertOrientationString(string) {
+ var inverted = string.replace('H', 'f');
+ inverted = inverted.replace('F', 'h');
+ inverted = inverted.replace('R', 'l');
+ inverted = inverted.replace('L', 'r');
+ inverted = inverted.replace('A', 'p');
+ inverted = inverted.replace('P', 'a');
+ inverted = inverted.toUpperCase();
+ return inverted;
+ }
+
+ // module/private exports
+ cornerstoneTools.orientation.invertOrientationString = invertOrientationString;
+
+})(cornerstoneTools);
+
+// End Source; src/orientation/invertOrientationString.js
+
+// Begin Source: src/referenceLines/calculateReferenceLine.js
+(function(cornerstoneTools) {
+
+ 'use strict';
+
+ // calculates a reference line between two planes by projecting the top left hand corner and bottom right hand corner
+ // of the reference image onto the target image. Ideally we would calculate the intersection between the planes but
+ // that requires a bit more math and this works fine for most cases
+ function calculateReferenceLine(targetImagePlane, referenceImagePlane) {
+ var points = cornerstoneTools.planePlaneIntersection(targetImagePlane, referenceImagePlane);
+ if (!points) {
+ return;
+ }
+
+ return {
+ start: cornerstoneTools.projectPatientPointToImagePlane(points.start, targetImagePlane),
+ end: cornerstoneTools.projectPatientPointToImagePlane(points.end, targetImagePlane)
+ };
+ }
+
+ // module/private exports
+ cornerstoneTools.referenceLines.calculateReferenceLine = calculateReferenceLine;
+
+})(cornerstoneTools);
+
+// End Source; src/referenceLines/calculateReferenceLine.js
+
+// Begin Source: src/referenceLines/referenceLinesTool.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ var toolType = 'referenceLines';
+
+ function onImageRendered(e, eventData) {
+ // 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;
+ }
+
+ // Get the enabled elements associated with this synchronization context and draw them
+ var syncContext = toolData.data[0].synchronizationContext;
+ var enabledElements = syncContext.getSourceElements();
+
+ var renderer = toolData.data[0].renderer;
+
+ // Create the canvas context and reset it to the pixel coordinate system
+ var context = eventData.canvasContext.canvas.getContext('2d');
+ cornerstone.setToPixelCoordinateSystem(eventData.enabledElement, context);
+
+ // Iterate over each referenced element
+ $.each(enabledElements, function(index, referenceEnabledElement) {
+
+ // don't draw ourselves
+ if (referenceEnabledElement === e.currentTarget) {
+ return;
+ }
+
+ // render it
+ renderer(context, eventData, e.currentTarget, referenceEnabledElement);
+ });
+ }
+
+ // enables the reference line tool for a given element. Note that a custom renderer
+ // can be provided if you want different rendering (e.g. all reference lines, first/last/active, etc)
+ function enable(element, synchronizationContext, renderer) {
+ renderer = renderer || cornerstoneTools.referenceLines.renderActiveReferenceLine;
+
+ cornerstoneTools.addToolState(element, toolType, {
+ synchronizationContext: synchronizationContext, renderer: renderer
+ });
+ $(element).on('CornerstoneImageRendered', onImageRendered);
+ cornerstone.updateImage(element);
+ }
+
+ // disables the reference line tool for the given element
+ function disable(element) {
+ $(element).off('CornerstoneImageRendered', onImageRendered);
+ cornerstone.updateImage(element);
+ }
+
+ // module/private exports
+ cornerstoneTools.referenceLines.tool = {
+ enable: enable, disable: disable
+
+ };
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/referenceLines/referenceLinesTool.js
+
+// Begin Source: src/referenceLines/renderActiveReferenceLine.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ // renders the active reference line
+ function renderActiveReferenceLine(context, eventData, targetElement, referenceElement) {
+ var targetImage = cornerstone.getEnabledElement(targetElement).image;
+ var referenceImage = cornerstone.getEnabledElement(referenceElement).image;
+
+ // make sure the images are actually loaded for the target and reference
+ if (!targetImage || !referenceImage) {
+ return;
+ }
+
+ var targetImagePlane = cornerstoneTools.metaData.get('imagePlane', targetImage.imageId);
+ var referenceImagePlane = cornerstoneTools.metaData.get('imagePlane', referenceImage.imageId);
+
+ // the image planes must be in the same frame of reference
+ if (targetImagePlane.frameOfReferenceUID !== referenceImagePlane.frameOfReferenceUID) {
+ return;
+ }
+
+ // the image plane normals must be > 30 degrees apart
+ var targetNormal = targetImagePlane.rowCosines.clone().cross(targetImagePlane.columnCosines);
+ var referenceNormal = referenceImagePlane.rowCosines.clone().cross(referenceImagePlane.columnCosines);
+ var angleInRadians = targetNormal.angleTo(referenceNormal);
+
+ angleInRadians = Math.abs(angleInRadians);
+ if (angleInRadians < 0.5) { // 0.5 radians = ~30 degrees
+ return;
+ }
+
+ var referenceLine = cornerstoneTools.referenceLines.calculateReferenceLine(targetImagePlane, referenceImagePlane);
+ if (!referenceLine) {
+ return;
+ }
+
+ var refLineStartCanvas = cornerstone.pixelToCanvas(eventData.element, referenceLine.start);
+ var refLineEndCanvas = cornerstone.pixelToCanvas(eventData.element, referenceLine.end);
+
+ var color = cornerstoneTools.toolColors.getActiveColor();
+ var lineWidth = cornerstoneTools.toolStyle.getToolWidth();
+
+ // draw the referenceLines
+ context.setTransform(1, 0, 0, 1, 0, 0);
+
+ context.save();
+ context.beginPath();
+ context.strokeStyle = color;
+ context.lineWidth = lineWidth;
+ context.moveTo(refLineStartCanvas.x, refLineStartCanvas.y);
+ context.lineTo(refLineEndCanvas.x, refLineEndCanvas.y);
+ context.stroke();
+ context.restore();
+ }
+
+ // module/private exports
+ cornerstoneTools.referenceLines.renderActiveReferenceLine = renderActiveReferenceLine;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/referenceLines/renderActiveReferenceLine.js
+
+// Begin Source: src/requestPool/requestPoolManager.js
+(function(cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ var requestPool = {
+ interaction: [],
+ thumbnail: [],
+ prefetch: []
+ };
+
+ var numRequests = {
+ interaction: 0,
+ thumbnail: 0,
+ prefetch: 0
+ };
+
+ var maxNumRequests = {
+ interaction: 6,
+ thumbnail: 6,
+ prefetch: 5
+ };
+
+ var lastElementInteracted;
+ var awake = false;
+ var grabDelay = 20;
+
+ function requestPoolManager() {
+
+ function addRequest(element, imageId, type, doneCallback, failCallback) {
+ if (!requestPool.hasOwnProperty(type)) {
+ throw 'Request type must be one of interaction, thumbnail, or prefetch';
+ }
+
+ if (!element || !imageId) {
+ return;
+ }
+
+ // Describe the request
+ var requestDetails = {
+ type: type,
+ imageId: imageId,
+ doneCallback: doneCallback,
+ failCallback: failCallback
+ };
+
+ // If this imageId is in the cache, resolve it immediately
+ var imagePromise = cornerstone.imageCache.getImagePromise(imageId);
+ if (imagePromise) {
+ imagePromise.then(function(image) {
+ doneCallback(image);
+ }, function(error) {
+ failCallback(error);
+ });
+ return;
+ }
+
+ // Add it to the end of the stack
+ requestPool[type].push(requestDetails);
+
+ // Store the last element interacted with,
+ // So we know which images to prefetch
+ //
+ // ---- Not used for now ----
+ if (type === 'interaction') {
+ lastElementInteracted = element;
+ }
+ }
+
+ function clearRequestStack(type) {
+ //console.log('clearRequestStack');
+ if (!requestPool.hasOwnProperty(type)) {
+ throw 'Request type must be one of interaction, thumbnail, or prefetch';
+ }
+
+ requestPool[type] = [];
+ }
+
+ function startAgain() {
+ if (!awake) {
+ return;
+ }
+
+ setTimeout(function() {
+ var requestDetails = getNextRequest();
+ if (!requestDetails) {
+ awake = false;
+ return;
+ }
+
+ sendRequest(requestDetails);
+ }, grabDelay);
+ }
+
+ function sendRequest(requestDetails) {
+ // Increment the number of current requests of this type
+ var type = requestDetails.type;
+ numRequests[type]++;
+
+ awake = true;
+ var imageId = requestDetails.imageId;
+ var doneCallback = requestDetails.doneCallback;
+ var failCallback = requestDetails.failCallback;
+
+ // Check if we already have this image promise in the cache
+ var imagePromise = cornerstone.imageCache.getImagePromise(imageId);
+ if (imagePromise) {
+ // If we do, remove from list (when resolved, as we could have
+ // pending prefetch requests) and stop processing this iteration
+ imagePromise.then(function(image) {
+ numRequests[type]--;
+ // console.log(numRequests);
+
+ doneCallback(image);
+ startAgain();
+ }, function(error) {
+ numRequests[type]--;
+ // console.log(numRequests);
+ failCallback(error);
+ });
+ return;
+ }
+
+ // Load and cache the image
+ cornerstone.loadAndCacheImage(imageId).then(function(image) {
+ numRequests[type]--;
+ // console.log(numRequests);
+ doneCallback(image);
+ startAgain();
+ }, function(error) {
+ numRequests[type]--;
+ // console.log(numRequests);
+ failCallback(error);
+ });
+ }
+
+ function startGrabbing() {
+ // Begin by grabbing X images
+ if (awake) {
+ return;
+ }
+
+ var maxSimultaneousRequests = cornerstoneTools.getMaxSimultaneousRequests();
+
+ maxNumRequests = {
+ interaction: maxSimultaneousRequests,
+ thumbnail: maxSimultaneousRequests - 2,
+ prefetch: maxSimultaneousRequests - 1
+ };
+
+ for (var i = 0; i < maxSimultaneousRequests; i++) {
+ var requestDetails = getNextRequest();
+ if (requestDetails) {
+ sendRequest(requestDetails);
+ }
+ }
+
+ //console.log("startGrabbing");
+ //console.log(requestPool);
+ }
+
+ function getNextRequest() {
+ if (requestPool.interaction.length && numRequests.interaction < maxNumRequests.interaction) {
+ return requestPool.interaction.shift();
+ }
+
+ if (requestPool.thumbnail.length && numRequests.thumbnail < maxNumRequests.thumbnail) {
+ return requestPool.thumbnail.shift();
+ }
+
+ if (requestPool.prefetch.length && numRequests.prefetch < maxNumRequests.prefetch) {
+ return requestPool.prefetch.shift();
+ }
+
+ if (!requestPool.interaction.length &&
+ !requestPool.thumbnail.length &&
+ !requestPool.prefetch.length) {
+ awake = false;
+ }
+
+ return false;
+ }
+
+ function getRequestPool() {
+ return requestPool;
+ }
+
+ var requestManager = {
+ addRequest: addRequest,
+ clearRequestStack: clearRequestStack,
+ startGrabbing: startGrabbing,
+ getRequestPool: getRequestPool
+ };
+
+ return requestManager;
+ }
+
+ // module/private exports
+ cornerstoneTools.requestPoolManager = requestPoolManager();
+
+})(cornerstone, cornerstoneTools);
+
+// End Source; src/requestPool/requestPoolManager.js
+
+// Begin Source: src/stackTools/playClip.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ var toolType = 'playClip';
+
+ /**
+ * Starts playing a clip or adjusts the frame rate of an already playing clip. framesPerSecond is
+ * optional and defaults to 30 if not specified. A negative framesPerSecond will play the clip in reverse.
+ * The element must be a stack of images
+ * @param element
+ * @param framesPerSecond
+ */
+ function playClip(element, framesPerSecond) {
+ if (element === undefined) {
+ throw 'playClip: element must not be undefined';
+ }
+
+ if (framesPerSecond === undefined) {
+ framesPerSecond = 30;
+ }
+
+ var stackToolData = cornerstoneTools.getToolState(element, 'stack');
+ if (stackToolData === undefined || stackToolData.data === undefined || stackToolData.data.length === 0) {
+ return;
+ }
+
+ var stackData = stackToolData.data[0];
+
+ var playClipToolData = cornerstoneTools.getToolState(element, toolType);
+ var playClipData;
+
+ if (playClipToolData === undefined || playClipToolData.data.length === 0) {
+ playClipData = {
+ intervalId: undefined, framesPerSecond: framesPerSecond, lastFrameTimeStamp: undefined, frameRate: 0, loop: true
+ };
+ cornerstoneTools.addToolState(element, toolType, playClipData);
+ } else {
+ playClipData = playClipToolData.data[0];
+ playClipData.framesPerSecond = framesPerSecond;
+ }
+
+ // if already playing, do not set a new interval
+ if (playClipData.intervalId !== undefined) {
+ return;
+ }
+
+ playClipData.intervalId = setInterval(function() {
+
+ var newImageIdIndex = stackData.currentImageIdIndex;
+
+ if (playClipData.framesPerSecond > 0) {
+ newImageIdIndex++;
+ } else {
+ newImageIdIndex--;
+ }
+
+ if (!playClipData.loop && (newImageIdIndex >= stackData.imageIds.length || newImageIdIndex < 0)) {
+
+ var eventDetail = {
+ element: element
+ };
+ var event = $.Event('CornerstoneToolsClipStopped', eventDetail);
+ $(element).trigger(event, eventDetail);
+
+ clearInterval(playClipData.intervalId);
+ playClipData.intervalId = undefined;
+ return;
+ }
+
+ // loop around if we go outside the stack
+ if (newImageIdIndex >= stackData.imageIds.length) {
+ newImageIdIndex = 0;
+ }
+
+ if (newImageIdIndex < 0) {
+ newImageIdIndex = stackData.imageIds.length - 1;
+ }
+
+ if (newImageIdIndex !== stackData.currentImageIdIndex) {
+ var startLoadingHandler = cornerstoneTools.loadHandlerManager.getStartLoadHandler();
+ var endLoadingHandler = cornerstoneTools.loadHandlerManager.getEndLoadHandler();
+ var errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler();
+
+ if (startLoadingHandler) {
+ startLoadingHandler(element);
+ }
+
+ var viewport = cornerstone.getViewport(element);
+ cornerstone.loadAndCacheImage(stackData.imageIds[newImageIdIndex]).then(function(image) {
+ stackData.currentImageIdIndex = newImageIdIndex;
+ cornerstone.displayImage(element, image, viewport);
+ if (endLoadingHandler) {
+ endLoadingHandler(element);
+ }
+ }, function(error) {
+ var imageId = stackData.imageIds[newImageIdIndex];
+ if (errorLoadingHandler) {
+ errorLoadingHandler(element, imageId, error);
+ }
+ });
+ }
+ }, 1000 / Math.abs(playClipData.framesPerSecond));
+ }
+
+ /**
+ * Stops an already playing clip.
+ * * @param element
+ */
+ function stopClip(element) {
+ var playClipToolData = cornerstoneTools.getToolState(element, toolType);
+ var playClipData;
+ if (playClipToolData === undefined || playClipToolData.data.length === 0) {
+ return;
+ } else {
+ playClipData = playClipToolData.data[0];
+ }
+
+ clearInterval(playClipData.intervalId);
+ playClipData.intervalId = undefined;
+ }
+
+ // module/private exports
+ cornerstoneTools.playClip = playClip;
+ cornerstoneTools.stopClip = stopClip;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/stackTools/playClip.js
+
+// Begin Source: src/stackTools/scrollIndicator.js
+/*
+Display scroll progress bar across bottom of image.
+ */
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ var scrollBarHeight = 6;
+
+ var configuration = {
+ backgroundColor: 'rgb(19, 63, 141)',
+ fillColor: 'white'
+ };
+
+ function onImageRendered(e, eventData){
+ var element = eventData.element;
+ var width = eventData.enabledElement.canvas.width;
+ var height = eventData.enabledElement.canvas.height;
+
+ if (!width || !height) {
+ return false;
+ }
+
+ var context = eventData.enabledElement.canvas.getContext('2d');
+ context.setTransform(1, 0, 0, 1, 0, 0);
+ context.save();
+
+ var config = cornerstoneTools.scrollIndicator.getConfiguration();
+
+ // draw indicator background
+ context.fillStyle = config.backgroundColor;
+ context.fillRect(0, height - scrollBarHeight, width, scrollBarHeight);
+
+ // get current image index
+ var stackData = cornerstoneTools.getToolState(element, 'stack');
+ if (!stackData || !stackData.data || !stackData.data.length) {
+ return;
+ }
+
+ var imageIds = stackData.data[0].imageIds;
+ var currentImageIdIndex = stackData.data[0].currentImageIdIndex;
+
+ // draw current image cursor
+ var cursorWidth = width / imageIds.length;
+ var xPosition = cursorWidth * currentImageIdIndex;
+
+ context.fillStyle = config.fillColor;
+ context.fillRect(xPosition, height - scrollBarHeight, cursorWidth, scrollBarHeight);
+
+ context.restore();
+ }
+
+ cornerstoneTools.scrollIndicator = cornerstoneTools.displayTool(onImageRendered);
+ cornerstoneTools.scrollIndicator.setConfiguration(configuration);
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/stackTools/scrollIndicator.js
+
+// Begin Source: src/stackTools/stackPrefetch.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ var toolType = 'stackPrefetch';
+ var requestType = 'prefetch';
+
+ var configuration = {};
+
+ var resetPrefetchTimeout,
+ resetPrefetchDelay;
+
+ function sortNumber(a, b) {
+ // http://stackoverflow.com/questions/1063007/how-to-sort-an-array-of-integers-correctly
+ return a - b;
+ }
+
+ function range(lowEnd, highEnd) {
+ // Javascript version of Python's range function
+ // http://stackoverflow.com/questions/3895478/does-javascript-have-a-method-like-range-to-generate-an-array-based-on-suppl
+ lowEnd = Math.round(lowEnd) || 0;
+ highEnd = Math.round(highEnd) || 0;
+
+ var arr = [],
+ c = highEnd - lowEnd + 1;
+
+ if (c <= 0) {
+ return arr;
+ }
+
+ while ( c-- ) {
+ arr[c] = highEnd--;
+ }
+
+ return arr;
+ }
+
+ var max = function(arr) {
+ return Math.max.apply(null, arr);
+ };
+
+ var min = function(arr) {
+ return Math.min.apply(null, arr);
+ };
+
+ function nearestIndex(arr, x) {
+ // Return index of nearest values in array
+ // http://stackoverflow.com/questions/25854212/return-index-of-nearest-values-in-an-array
+ var l = [],
+ h = [];
+
+ arr.forEach(function(v) {
+ if (v < x) {
+ l.push(v);
+ } else if (v > x) {
+ h.push(v);
+ }
+ });
+
+ return {
+ low: arr.indexOf(max(l)),
+ high: arr.indexOf(min(h))
+ };
+ }
+
+ function prefetch(element) {
+ // Check to make sure stack data exists
+ var stackData = cornerstoneTools.getToolState(element, 'stack');
+ if (!stackData || !stackData.data || !stackData.data.length) {
+ return;
+ }
+
+ var stack = stackData.data[0];
+
+ // Get the stackPrefetch tool data
+ var stackPrefetchData = cornerstoneTools.getToolState(element, toolType);
+ if (!stackPrefetchData) {
+ return;
+ }
+
+ var stackPrefetch = stackPrefetchData.data[0];
+
+ // If all the requests are complete, disable the stackPrefetch tool
+ if (!stackPrefetch || !stackPrefetch.indicesToRequest || !stackPrefetch.indicesToRequest.length) {
+ stackPrefetch.enabled = false;
+ }
+
+ // Make sure the tool is still enabled
+ if (stackPrefetch.enabled === false) {
+ return;
+ }
+
+ // Remove an imageIdIndex from the list of indices to request
+ // This fires when the individual image loading deferred is resolved
+ function removeFromList(imageIdIndex) {
+ var index = stackPrefetch.indicesToRequest.indexOf(imageIdIndex);
+ if (index > -1) { // don't remove last element if imageIdIndex not found
+ stackPrefetch.indicesToRequest.splice(index, 1);
+ }
+ }
+
+ // Remove all already cached images from the
+ // indicesToRequest array
+ stackPrefetchData.data[0].indicesToRequest.sort(sortNumber);
+ var indicesToRequestCopy = stackPrefetch.indicesToRequest.slice();
+
+ indicesToRequestCopy.forEach(function(imageIdIndex) {
+ var imageId = stack.imageIds[imageIdIndex];
+
+ if (!imageId) {
+ return;
+ }
+
+ var imagePromise = cornerstone.imageCache.getImagePromise(imageId);
+ if (imagePromise && imagePromise.state() === 'resolved'){
+ removeFromList(imageIdIndex);
+ }
+ });
+
+ // Stop here if there are no images left to request
+ // After those in the cache have been removed
+ if (!stackPrefetch.indicesToRequest.length) {
+ return;
+ }
+
+ function doneCallback(image) {
+ //console.log('prefetch done: ' + image.imageId);
+ var imageIdIndex = stack.imageIds.indexOf(image.imageId);
+ removeFromList(imageIdIndex);
+ }
+
+ function failCallback(error) {
+ console.log('prefetch errored: ' + error);
+ }
+
+ // Clear the requestPool of prefetch requests
+ var requestPoolManager = cornerstoneTools.requestPoolManager;
+ requestPoolManager.clearRequestStack(requestType);
+
+ // Identify the nearest imageIdIndex to the currentImageIdIndex
+ var nearest = nearestIndex(stackPrefetch.indicesToRequest, stack.currentImageIdIndex);
+
+ var imageId,
+ nextImageIdIndex;
+
+ // Prefetch images around the current image (before and after)
+ var lowerIndex = nearest.low;
+ var higherIndex = nearest.high;
+ while (lowerIndex > 0 || higherIndex < stackPrefetch.indicesToRequest.length) {
+ if (lowerIndex >= 0 ) {
+ nextImageIdIndex = stackPrefetch.indicesToRequest[lowerIndex--];
+ imageId = stack.imageIds[nextImageIdIndex];
+ requestPoolManager.addRequest(element, imageId, requestType, doneCallback, failCallback);
+ }
+
+ if (higherIndex < stackPrefetch.indicesToRequest.length) {
+ nextImageIdIndex = stackPrefetch.indicesToRequest[higherIndex++];
+ imageId = stack.imageIds[nextImageIdIndex];
+ requestPoolManager.addRequest(element, imageId, requestType, doneCallback, failCallback);
+ }
+ }
+
+ // Try to start the requestPool's grabbing procedure
+ // in case it isn't already running
+ requestPoolManager.startGrabbing();
+ }
+
+ function handleCacheFull(e) {
+ // Stop prefetching if the ImageCacheFull event is fired from cornerstone
+ // console.log('CornerstoneImageCacheFull full, stopping');
+ var element = e.data.element;
+
+ var stackPrefetchData = cornerstoneTools.getToolState(element, toolType);
+ if (!stackPrefetchData || !stackPrefetchData.data || !stackPrefetchData.data.length) {
+ return;
+ }
+
+ // Disable the stackPrefetch tool
+ // stackPrefetchData.data[0].enabled = false;
+
+ // Clear current prefetch requests from the requestPool
+ cornerstoneTools.requestPoolManager.clearRequestStack(requestType);
+ }
+
+ function promiseRemovedHandler(e, eventData) {
+ // When an imagePromise has been pushed out of the cache, re-add its index
+ // it to the indicesToRequest list so that it will be retrieved later if the
+ // currentImageIdIndex is changed to an image nearby
+ var element = e.data.element;
+ var stackData = cornerstoneTools.getToolState(element, 'stack');
+ if (!stackData || !stackData.data || !stackData.data.length) {
+ return;
+ }
+
+ var stack = stackData.data[0];
+ var imageIdIndex = stack.imageIds.indexOf(eventData.imageId);
+
+ // Make sure the image that was removed is actually in this stack
+ // before adding it to the indicesToRequest array
+ if (imageIdIndex < 0) {
+ return;
+ }
+
+ var stackPrefetchData = cornerstoneTools.getToolState(element, toolType);
+ if (!stackPrefetchData || !stackPrefetchData.data || !stackPrefetchData.data.length) {
+ return;
+ }
+
+ stackPrefetchData.data[0].indicesToRequest.push(imageIdIndex);
+ }
+
+ function onImageUpdated(e) {
+ // Start prefetching again (after a delay)
+ // When the user has scrolled to a new image
+ clearTimeout(resetPrefetchTimeout);
+ resetPrefetchTimeout = setTimeout(function() {
+ var element = e.currentTarget;
+ prefetch(element);
+ }, resetPrefetchDelay);
+ }
+
+ function enable(element) {
+ // Clear old prefetch data. Skipping this can cause problems when changing the series inside an element
+ var stackPrefetchDataArray = cornerstoneTools.getToolState(element, toolType);
+ stackPrefetchDataArray.data = [];
+
+ // First check that there is stack data available
+ var stackData = cornerstoneTools.getToolState(element, 'stack');
+ if (!stackData || !stackData.data || !stackData.data.length) {
+ return;
+ }
+
+ var stack = stackData.data[0];
+
+ // Use the currentImageIdIndex from the stack as the initalImageIdIndex
+ var stackPrefetchData = {
+ indicesToRequest: range(0, stack.imageIds.length - 1),
+ enabled: true,
+ direction: 1
+ };
+
+ // Remove the currentImageIdIndex from the list to request
+ var indexOfCurrentImage = stackPrefetchData.indicesToRequest.indexOf(stack.currentImageIdIndex);
+ stackPrefetchData.indicesToRequest.splice(indexOfCurrentImage, 1);
+
+ cornerstoneTools.addToolState(element, toolType, stackPrefetchData);
+
+ prefetch(element);
+
+ $(element).off('CornerstoneNewImage', onImageUpdated);
+ $(element).on('CornerstoneNewImage', onImageUpdated);
+
+ $(cornerstone).off('CornerstoneImageCacheFull', handleCacheFull);
+ $(cornerstone).on('CornerstoneImageCacheFull', {
+ element: element
+ }, handleCacheFull);
+
+ $(cornerstone).off('CornerstoneImageCachePromiseRemoved', promiseRemovedHandler);
+ $(cornerstone).on('CornerstoneImageCachePromiseRemoved', {
+ element: element
+ }, promiseRemovedHandler);
+ }
+
+ function disable(element) {
+ $(element).off('CornerstoneNewImage', onImageUpdated);
+
+ $(cornerstone).off('CornerstoneImageCacheFull', handleCacheFull);
+ $(cornerstone).off('CornerstoneImageCachePromiseRemoved', promiseRemovedHandler);
+
+ var stackPrefetchData = cornerstoneTools.getToolState(element, toolType);
+ // If there is actually something to disable, disable it
+ if (stackPrefetchData && stackPrefetchData.data.length) {
+ stackPrefetchData.data[0].enabled = false;
+
+ // Clear current prefetch requests from the requestPool
+ cornerstoneTools.requestPoolManager.clearRequestStack(requestType);
+ }
+ }
+
+ function getConfiguration () {
+ return configuration;
+ }
+
+ function setConfiguration(config) {
+ configuration = config;
+ }
+
+ // module/private exports
+ cornerstoneTools.stackPrefetch = {
+ enable: enable,
+ disable: disable,
+ getConfiguration: getConfiguration,
+ setConfiguration: setConfiguration
+ };
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/stackTools/stackPrefetch.js
+
+// Begin Source: src/stackTools/stackScroll.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function mouseUpCallback(e, eventData) {
+ $(eventData.element).off('CornerstoneToolsMouseDrag', dragCallback);
+ $(eventData.element).off('CornerstoneToolsMouseUp', mouseUpCallback);
+ }
+
+ function mouseDownCallback(e, eventData) {
+ if (cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
+ var mouseDragEventData = {
+ deltaY: 0
+ };
+ $(eventData.element).on('CornerstoneToolsMouseDrag', mouseDragEventData, dragCallback);
+ $(eventData.element).on('CornerstoneToolsMouseUp', mouseUpCallback);
+ e.stopImmediatePropagation();
+ return false;
+ }
+ }
+
+ function mouseWheelCallback(e, eventData) {
+ var images = -eventData.direction;
+ cornerstoneTools.scroll(eventData.element, images);
+ }
+
+ function dragCallback(e, eventData) {
+ var element = eventData.element;
+
+ var toolData = cornerstoneTools.getToolState(element, 'stack');
+ if (!toolData || !toolData.data || !toolData.data.length) {
+ return;
+ }
+
+ var stackData = toolData.data[0];
+
+ var config = cornerstoneTools.stackScroll.getConfiguration();
+
+ // The Math.max here makes it easier to mouseDrag-scroll small image stacks
+ var pixelsPerImage = $(element).height() / Math.max(stackData.imageIds.length, 8);
+ if (config && config.stackScrollSpeed) {
+ pixelsPerImage = config.stackScrollSpeed;
+ }
+
+ e.data.deltaY = e.data.deltaY || 0;
+ e.data.deltaY += eventData.deltaPoints.page.y;
+ if (Math.abs(e.data.deltaY) >= pixelsPerImage) {
+ var imageDelta = e.data.deltaY / pixelsPerImage;
+ var imageIdIndexOffset = Math.round(imageDelta);
+ var imageDeltaMod = e.data.deltaY % pixelsPerImage;
+ e.data.deltaY = imageDeltaMod;
+ cornerstoneTools.scroll(element, imageIdIndexOffset);
+ }
+
+ return false; // false = causes jquery to preventDefault() and stopPropagation() this event
+ }
+
+ // module/private exports
+ cornerstoneTools.stackScroll = cornerstoneTools.simpleMouseButtonTool(mouseDownCallback);
+ cornerstoneTools.stackScrollWheel = cornerstoneTools.mouseWheelTool(mouseWheelCallback);
+
+ var options = {
+ eventData: {
+ deltaY: 0
+ }
+ };
+ cornerstoneTools.stackScrollTouchDrag = cornerstoneTools.touchDragTool(dragCallback, options);
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/stackTools/stackScroll.js
+
+// Begin Source: src/stackTools/stackScrollKeyboard.js
+(function(cornerstoneTools) {
+
+ 'use strict';
+
+ var keys = {
+ UP: 38, DOWN: 40
+ };
+
+ function keyDownCallback(e, eventData) {
+ var keyCode = eventData.keyCode;
+ if (keyCode !== keys.UP && keyCode !== keys.DOWN) {
+ return;
+ }
+
+ var images = 1;
+ if (keyCode === keys.DOWN) {
+ images = -1;
+ }
+
+ cornerstoneTools.scroll(eventData.element, images);
+ }
+
+ // module/private exports
+ cornerstoneTools.stackScrollKeyboard = cornerstoneTools.keyboardTool(keyDownCallback);
+
+})(cornerstoneTools);
+
+// End Source; src/stackTools/stackScrollKeyboard.js
+
+// Begin Source: src/stateManagement/applicationState.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function saveApplicationState(elements) {
+ // Save imageId-specific tool state data
+ var appState = {
+ imageIdToolState: cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState(),
+ elementToolState: {},
+ elementViewport: {}
+ };
+
+ // For each of the given elements, save the viewport and any stack-specific tool data
+ elements.forEach(function(element) {
+ var toolStateManager = cornerstoneTools.getElementToolStateManager(element);
+ if (toolStateManager === cornerstoneTools.globalImageIdSpecificToolStateManager) {
+ return;
+ }
+
+ appState.elementToolState[element.id] = toolStateManager.saveToolState();
+
+ appState.elementViewport[element.id] = cornerstone.getViewport(element);
+ });
+ return appState;
+ }
+
+ function restoreApplicationState(appState) {
+ // Make sure t
+ if (!appState.hasOwnProperty('imageIdToolState') ||
+ !appState.hasOwnProperty('elementToolState') ||
+ !appState.hasOwnProperty('elementViewport')) {
+ return;
+ }
+
+ // Restore all the imageId specific tool data
+ cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState(appState.imageIdToolState);
+
+ Object.keys(appState.elementViewport).forEach(function(elementId) {
+ // Restore any stack specific tool data
+ var element = document.getElementById(elementId);
+ if (!element) {
+ return;
+ }
+
+ if (!appState.elementToolState.hasOwnProperty(elementId)) {
+ return;
+ }
+
+ var toolStateManager = cornerstoneTools.getElementToolStateManager(element);
+ if (toolStateManager === cornerstoneTools.globalImageIdSpecificToolStateManager) {
+ return;
+ }
+
+ toolStateManager.restoreToolState(appState.elementToolState[elementId]);
+
+ // Restore the saved viewport information
+ var savedViewport = appState.elementViewport[elementId];
+ cornerstone.setViewport(element, savedViewport);
+
+ // Update the element to apply the viewport and tool changes
+ cornerstone.updateImage(element);
+ });
+ return appState;
+ }
+
+ cornerstoneTools.appState = {
+ save: saveApplicationState,
+ restore: restoreApplicationState
+ };
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/stateManagement/applicationState.js
+
+// Begin Source: src/stateManagement/frameOfReferenceStateManager.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ // This implements a frame-of-reference specific tool state management strategy. This means that
+ // measurement data are tied to a specific frame of reference UID and only visible to objects using
+ // that frame-of-reference UID
+
+ function newFrameOfReferenceSpecificToolStateManager() {
+ var toolState = {};
+
+ // here we add tool state, this is done by tools as well
+ // as modules that restore saved state
+ function addFrameOfReferenceSpecificToolState(frameOfReference, toolType, data) {
+ // if we don't have any tool state for this frameOfReference, add an empty object
+ if (toolState.hasOwnProperty(frameOfReference) === false) {
+ toolState[frameOfReference] = {};
+ }
+
+ var frameOfReferenceToolState = toolState[frameOfReference];
+
+ // if we don't have tool state for this type of tool, add an empty object
+ if (frameOfReferenceToolState.hasOwnProperty(toolType) === false) {
+ frameOfReferenceToolState[toolType] = {
+ data: []
+ };
+ }
+
+ var toolData = frameOfReferenceToolState[toolType];
+
+ // finally, add this new tool to the state
+ toolData.data.push(data);
+ }
+
+ // here you can get state - used by tools as well as modules
+ // that save state persistently
+ function getFrameOfReferenceSpecificToolState(frameOfReference, toolType) {
+ // if we don't have any tool state for this frame of reference, return undefined
+ if (toolState.hasOwnProperty(frameOfReference) === false) {
+ return;
+ }
+
+ var frameOfReferenceToolState = toolState[frameOfReference];
+
+ // if we don't have tool state for this type of tool, return undefined
+ if (frameOfReferenceToolState.hasOwnProperty(toolType) === false) {
+ return;
+ }
+
+ var toolData = frameOfReferenceToolState[toolType];
+ return toolData;
+ }
+
+ function removeFrameOfReferenceSpecificToolState(frameOfReference, toolType, data) {
+ // if we don't have any tool state for this frame of reference, return undefined
+ if (toolState.hasOwnProperty(frameOfReference) === false) {
+ return;
+ }
+
+ var frameOfReferenceToolState = toolState[frameOfReference];
+
+ // if we don't have tool state for this type of tool, return undefined
+ if (frameOfReferenceToolState.hasOwnProperty(toolType) === false) {
+ return;
+ }
+
+ var toolData = frameOfReferenceToolState[toolType];
+ // 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);
+ }
+ }
+
+ var frameOfReferenceToolStateManager = {
+ get: getFrameOfReferenceSpecificToolState,
+ add: addFrameOfReferenceSpecificToolState,
+ remove: removeFrameOfReferenceSpecificToolState
+ };
+ return frameOfReferenceToolStateManager;
+ }
+
+ // a global frameOfReferenceSpecificToolStateManager - the most common case is to share 3d information
+ // between stacks of images
+ var globalFrameOfReferenceSpecificToolStateManager = newFrameOfReferenceSpecificToolStateManager();
+
+ // module/private exports
+ cornerstoneTools.newFrameOfReferenceSpecificToolStateManager = newFrameOfReferenceSpecificToolStateManager;
+ cornerstoneTools.globalFrameOfReferenceSpecificToolStateManager = globalFrameOfReferenceSpecificToolStateManager;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/stateManagement/frameOfReferenceStateManager.js
+
+// Begin Source: src/stateManagement/imageIdSpecificStateManager.js
+(function(cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ // This implements an imageId specific tool state management strategy. This means that
+ // measurements data is tied to a specific imageId and only visible for enabled elements
+ // that are displaying that imageId.
+
+ function newImageIdSpecificToolStateManager() {
+ var toolState = {};
+
+ // here we add tool state, this is done by tools as well
+ // as modules that restore saved state
+
+ function saveImageIdToolState(imageId) {
+ return toolState[imageId];
+ }
+
+ function restoreImageIdToolState(imageId, imageIdToolState) {
+ toolState[imageId] = imageIdToolState;
+ }
+
+ function saveToolState() {
+ return toolState;
+ }
+
+ function restoreToolState(savedToolState) {
+ toolState = savedToolState;
+ }
+
+ // here we add tool state, this is done by tools as well
+ // as modules that restore saved state
+ function addImageIdSpecificToolState(element, toolType, data) {
+ var enabledImage = cornerstone.getEnabledElement(element);
+ // if we don't have any tool state for this imageId, add an empty object
+ if (!enabledImage.image || toolState.hasOwnProperty(enabledImage.image.imageId) === false) {
+ toolState[enabledImage.image.imageId] = {};
+ }
+
+ var imageIdToolState = toolState[enabledImage.image.imageId];
+
+ // if we don't have tool state for this type of tool, add an empty object
+ if (imageIdToolState.hasOwnProperty(toolType) === false) {
+ imageIdToolState[toolType] = {
+ data: []
+ };
+ }
+
+ var toolData = imageIdToolState[toolType];
+
+ // finally, add this new tool to the state
+ toolData.data.push(data);
+ }
+
+ // here you can get state - used by tools as well as modules
+ // that save state persistently
+ function getImageIdSpecificToolState(element, toolType) {
+ var enabledImage = cornerstone.getEnabledElement(element);
+ // if we don't have any tool state for this imageId, return undefined
+ if (!enabledImage.image || toolState.hasOwnProperty(enabledImage.image.imageId) === false) {
+ return;
+ }
+
+ var imageIdToolState = toolState[enabledImage.image.imageId];
+
+ // if we don't have tool state for this type of tool, return undefined
+ if (imageIdToolState.hasOwnProperty(toolType) === false) {
+ return;
+ }
+
+ var toolData = imageIdToolState[toolType];
+ return toolData;
+ }
+
+ // Clears all tool data from this toolStateManager.
+ function clearImageIdSpecificToolStateManager(element) {
+ var enabledImage = cornerstone.getEnabledElement(element);
+ if (!enabledImage.image || toolState.hasOwnProperty(enabledImage.image.imageId) === false) {
+ return;
+ }
+
+ delete toolState[enabledImage.image.imageId];
+ }
+
+ var imageIdToolStateManager = {
+ get: getImageIdSpecificToolState,
+ add: addImageIdSpecificToolState,
+ clear: clearImageIdSpecificToolStateManager,
+ saveImageIdToolState: saveImageIdToolState,
+ restoreImageIdToolState: restoreImageIdToolState,
+ saveToolState: saveToolState,
+ restoreToolState: restoreToolState,
+ toolState: toolState
+ };
+ return imageIdToolStateManager;
+ }
+
+ // a global imageIdSpecificToolStateManager - the most common case is to share state between all
+ // visible enabled images
+ var globalImageIdSpecificToolStateManager = newImageIdSpecificToolStateManager();
+
+ // module/private exports
+ cornerstoneTools.newImageIdSpecificToolStateManager = newImageIdSpecificToolStateManager;
+ cornerstoneTools.globalImageIdSpecificToolStateManager = globalImageIdSpecificToolStateManager;
+
+})(cornerstone, cornerstoneTools);
+
+// End Source; src/stateManagement/imageIdSpecificStateManager.js
+
+// Begin Source: src/stateManagement/loadHandlerManager.js
+(function(cornerstoneTools) {
+
+ 'use strict';
+
+ function loadHandlerManager() {
+ var defaultStartLoadHandler,
+ defaultEndLoadHandler,
+ defaultErrorLoadingHandler;
+
+ function setStartLoadHandler(handler) {
+ defaultStartLoadHandler = handler;
+ }
+
+ function getStartLoadHandler() {
+ return defaultStartLoadHandler;
+ }
+
+ function setEndLoadHandler(handler) {
+ defaultEndLoadHandler = handler;
+ }
+
+ function getEndLoadHandler(){
+ return defaultEndLoadHandler;
+ }
+
+ function setErrorLoadingHandler(handler) {
+ defaultErrorLoadingHandler = handler;
+ }
+
+ function getErrorLoadingHandler() {
+ return defaultErrorLoadingHandler;
+ }
+
+ var loadHandlers = {
+ setStartLoadHandler: setStartLoadHandler,
+ getStartLoadHandler: getStartLoadHandler,
+ setEndLoadHandler: setEndLoadHandler,
+ getEndLoadHandler: getEndLoadHandler,
+ setErrorLoadingHandler: setErrorLoadingHandler,
+ getErrorLoadingHandler: getErrorLoadingHandler
+ };
+
+ return loadHandlers;
+ }
+
+ // module/private exports
+ cornerstoneTools.loadHandlerManager = loadHandlerManager();
+
+})(cornerstoneTools);
+
+// End Source; src/stateManagement/loadHandlerManager.js
+
+// Begin Source: src/stateManagement/stackSpecificStateManager.js
+(function(cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ // This implements an Stack specific tool state management strategy. This means
+ // that tool data is shared between all imageIds in a given stack
+ function newStackSpecificToolStateManager(toolTypes, oldStateManager) {
+ var toolState = {};
+
+ function saveToolState() {
+ return toolState;
+ }
+
+ function restoreToolState(stackToolState) {
+ toolState = stackToolState;
+ }
+
+ // here we add tool state, this is done by tools as well
+ // as modules that restore saved state
+ function addStackSpecificToolState(element, toolType, data) {
+ // if this is a tool type to apply to the stack, do so
+ if (toolTypes.indexOf(toolType) >= 0) {
+
+ // if we don't have tool state for this type of tool, add an empty object
+ if (toolState.hasOwnProperty(toolType) === false) {
+ toolState[toolType] = {
+ data: []
+ };
+ }
+
+ var toolData = toolState[toolType];
+
+ // finally, add this new tool to the state
+ toolData.data.push(data);
+ } else {
+ // call the imageId specific tool state manager
+ return oldStateManager.add(element, toolType, data);
+ }
+ }
+
+ // here you can get state - used by tools as well as modules
+ // that save state persistently
+ function getStackSpecificToolState(element, toolType) {
+ // if this is a tool type to apply to the stack, do so
+ if (toolTypes.indexOf(toolType) >= 0) {
+ // if we don't have tool state for this type of tool, add an empty object
+ if (toolState.hasOwnProperty(toolType) === false) {
+ toolState[toolType] = {
+ data: []
+ };
+ }
+
+ var toolData = toolState[toolType];
+ return toolData;
+ } else {
+ // call the imageId specific tool state manager
+ return oldStateManager.get(element, toolType);
+ }
+ }
+
+ var stackSpecificToolStateManager = {
+ get: getStackSpecificToolState,
+ add: addStackSpecificToolState,
+ saveToolState: saveToolState,
+ restoreToolState: restoreToolState,
+ toolState: toolState,
+ };
+ return stackSpecificToolStateManager;
+ }
+
+ var stackStateManagers = [];
+
+ function addStackStateManager(element) {
+ var oldStateManager = cornerstoneTools.getElementToolStateManager(element);
+ if (!oldStateManager) {
+ oldStateManager = cornerstoneTools.globalImageIdSpecificToolStateManager;
+ }
+
+ var stackTools = [ 'stack', 'stackPrefetch', 'playClip', 'volume', 'slab', 'referenceLines', 'crosshairs' ];
+ var stackSpecificStateManager = cornerstoneTools.newStackSpecificToolStateManager(stackTools, oldStateManager);
+ stackStateManagers.push(stackSpecificStateManager);
+ cornerstoneTools.setElementToolStateManager(element, stackSpecificStateManager);
+ }
+
+ // module/private exports
+ cornerstoneTools.newStackSpecificToolStateManager = newStackSpecificToolStateManager;
+ cornerstoneTools.addStackStateManager = addStackStateManager;
+
+})(cornerstone, cornerstoneTools);
+
+// End Source; src/stateManagement/stackSpecificStateManager.js
+
+// Begin Source: src/stateManagement/textStyleManager.js
+(function(cornerstoneTools) {
+
+ 'use strict';
+
+ function textStyleManager() {
+ var defaultFontSize = 15,
+ defaultFont = defaultFontSize + 'px Arial',
+ defaultBackgroundColor = 'transparent';
+
+ function setFont(font) {
+ defaultFont = font;
+ }
+
+ function getFont() {
+ return defaultFont;
+ }
+
+ function setFontSize(fontSize) {
+ defaultFontSize = fontSize;
+ }
+
+ function getFontSize() {
+ return defaultFontSize;
+ }
+
+ function setBackgroundColor(backgroundColor) {
+ defaultBackgroundColor = backgroundColor;
+ }
+
+ function getBackgroundColor() {
+ return defaultBackgroundColor;
+ }
+
+ var textStyle = {
+ setFont: setFont,
+ getFont: getFont,
+ setFontSize: setFontSize,
+ getFontSize: getFontSize,
+ setBackgroundColor: setBackgroundColor,
+ getBackgroundColor: getBackgroundColor
+ };
+
+ return textStyle;
+ }
+
+ // module/private exports
+ cornerstoneTools.textStyle = textStyleManager();
+
+})(cornerstoneTools);
+
+// End Source; src/stateManagement/textStyleManager.js
+
+// Begin Source: src/stateManagement/timeSeriesSpecificStateManager.js
+(function(cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ // This implements an Stack specific tool state management strategy. This means
+ // that tool data is shared between all imageIds in a given stack
+ function newTimeSeriesSpecificToolStateManager(toolTypes, oldStateManager) {
+ var toolState = {};
+
+ // here we add tool state, this is done by tools as well
+ // as modules that restore saved state
+ function addStackSpecificToolState(element, toolType, data) {
+ // if this is a tool type to apply to the stack, do so
+ if (toolTypes.indexOf(toolType) >= 0) {
+
+ // if we don't have tool state for this type of tool, add an empty object
+ if (toolState.hasOwnProperty(toolType) === false) {
+ toolState[toolType] = {
+ data: []
+ };
+ }
+
+ var toolData = toolState[toolType];
+
+ // finally, add this new tool to the state
+ toolData.data.push(data);
+ } else {
+ // call the imageId specific tool state manager
+ return oldStateManager.add(element, toolType, data);
+ }
+ }
+
+ // here you can get state - used by tools as well as modules
+ // that save state persistently
+ function getStackSpecificToolState(element, toolType) {
+ // if this is a tool type to apply to the stack, do so
+ if (toolTypes.indexOf(toolType) >= 0) {
+ // if we don't have tool state for this type of tool, add an empty object
+ if (toolState.hasOwnProperty(toolType) === false) {
+ toolState[toolType] = {
+ data: []
+ };
+ }
+
+ var toolData = toolState[toolType];
+ return toolData;
+ } else {
+ // call the imageId specific tool state manager
+ return oldStateManager.get(element, toolType);
+ }
+ }
+
+ var imageIdToolStateManager = {
+ get: getStackSpecificToolState, add: addStackSpecificToolState
+ };
+ return imageIdToolStateManager;
+ }
+
+ var timeSeriesStateManagers = [];
+
+ function addTimeSeriesStateManager(element, tools) {
+ tools = tools || [ 'timeSeries' ];
+ var oldStateManager = cornerstoneTools.getElementToolStateManager(element);
+ if (oldStateManager === undefined) {
+ oldStateManager = cornerstoneTools.globalImageIdSpecificToolStateManager;
+ }
+
+ var timeSeriesSpecificStateManager = cornerstoneTools.newTimeSeriesSpecificToolStateManager(tools, oldStateManager);
+ timeSeriesStateManagers.push(timeSeriesSpecificStateManager);
+ cornerstoneTools.setElementToolStateManager(element, timeSeriesSpecificStateManager);
+ }
+
+ // module/private exports
+ cornerstoneTools.newTimeSeriesSpecificToolStateManager = newTimeSeriesSpecificToolStateManager;
+ cornerstoneTools.addTimeSeriesStateManager = addTimeSeriesStateManager;
+
+})(cornerstone, cornerstoneTools);
+
+// End Source; src/stateManagement/timeSeriesSpecificStateManager.js
+
+// Begin Source: src/stateManagement/toolColorManager.js
+(function(cornerstoneTools) {
+
+ 'use strict';
+
+ function toolColorManager(){
+ var defaultColor = 'white',
+ activeColor = 'greenyellow',
+ fillColor = 'transparent';
+
+ function setFillColor(color){
+ fillColor = color;
+ }
+
+ function getFillColor(){
+ return fillColor;
+ }
+
+ function setToolColor(color){
+ defaultColor = color;
+ }
+
+ function getToolColor(){
+ return defaultColor;
+ }
+
+ function setActiveToolColor(color){
+ activeColor = color;
+ }
+
+ function getActiveToolColor(){
+ return activeColor;
+ }
+
+ var toolColors = {
+ setFillColor: setFillColor,
+ getFillColor: getFillColor,
+ setToolColor: setToolColor,
+ getToolColor: getToolColor,
+ setActiveColor: setActiveToolColor,
+ getActiveColor: getActiveToolColor
+ };
+
+ return toolColors;
+ }
+
+ // module/private exports
+ cornerstoneTools.toolColors = toolColorManager();
+
+})(cornerstoneTools);
+
+// End Source; src/stateManagement/toolColorManager.js
+
+// Begin Source: src/stateManagement/toolCoordinateManager.js
+(function(cornerstoneTools) {
+
+ 'use strict';
+
+ function toolCoordinateManager(){
+ var cooordsData = '';
+
+ function setActiveToolCoords(eventData){
+ cooordsData = eventData.currentPoints.canvas;
+ }
+
+ function getActiveToolCoords(){
+ return cooordsData;
+ }
+
+ var toolCoords = {
+ setCoords: setActiveToolCoords, getCoords: getActiveToolCoords
+ };
+
+ return toolCoords;
+ }
+
+ // module/private exports
+ cornerstoneTools.toolCoordinates = toolCoordinateManager();
+
+})(cornerstoneTools);
+
+// End Source; src/stateManagement/toolCoordinateManager.js
+
+// Begin Source: src/stateManagement/toolStateManager.js
+(function(cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function getElementToolStateManager(element) {
+ var enabledImage = cornerstone.getEnabledElement(element);
+ // if the enabledImage has no toolStateManager, create a default one for it
+ // NOTE: This makes state management element specific
+ if (enabledImage.toolStateManager === undefined) {
+ enabledImage.toolStateManager = cornerstoneTools.globalImageIdSpecificToolStateManager;
+ }
+
+ return enabledImage.toolStateManager;
+ }
+
+ // here we add tool state, this is done by tools as well
+ // as modules that restore saved state
+ function addToolState(element, toolType, data) {
+ var toolStateManager = getElementToolStateManager(element);
+ toolStateManager.add(element, toolType, data);
+ // TODO: figure out how to broadcast this change to all enabled elements so they can update the image
+ // if this change effects them
+ }
+
+ // here you can get state - used by tools as well as modules
+ // that save state persistently
+ function getToolState(element, toolType) {
+ var toolStateManager = getElementToolStateManager(element);
+ return toolStateManager.get(element, toolType);
+ }
+
+ function removeToolState(element, toolType, data) {
+ var toolStateManager = getElementToolStateManager(element);
+ var toolData = toolStateManager.get(element, toolType);
+ // 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);
+ }
+ }
+
+ function clearToolState(element, toolType) {
+ var toolStateManager = getElementToolStateManager(element);
+ var toolData = toolStateManager.get(element, toolType);
+
+ // If any toolData actually exists, clear it
+ if (toolData !== undefined) {
+ toolData.data = [];
+ }
+ }
+
+ // sets the tool state manager for an element
+ function setElementToolStateManager(element, toolStateManager) {
+ var enabledImage = cornerstone.getEnabledElement(element);
+ enabledImage.toolStateManager = toolStateManager;
+ }
+
+ // module/private exports
+ cornerstoneTools.addToolState = addToolState;
+ cornerstoneTools.getToolState = getToolState;
+ cornerstoneTools.removeToolState = removeToolState;
+ cornerstoneTools.clearToolState = clearToolState;
+ cornerstoneTools.setElementToolStateManager = setElementToolStateManager;
+ cornerstoneTools.getElementToolStateManager = getElementToolStateManager;
+
+})(cornerstone, cornerstoneTools);
+
+// End Source; src/stateManagement/toolStateManager.js
+
+// Begin Source: src/stateManagement/toolStyleManager.js
+(function(cornerstoneTools) {
+
+ 'use strict';
+
+ function toolStyleManager() {
+ var defaultWidth = 1,
+ activeWidth = 2;
+
+ function setToolWidth(width){
+ defaultWidth = width;
+ }
+
+ function getToolWidth(){
+ return defaultWidth;
+ }
+
+ function setActiveToolWidth(width){
+ activeWidth = width;
+ }
+
+ function getActiveToolWidth(){
+ return activeWidth;
+ }
+
+ var toolStyle = {
+ setToolWidth: setToolWidth,
+ getToolWidth: getToolWidth,
+ setActiveWidth: setActiveToolWidth,
+ getActiveWidth: getActiveToolWidth
+ };
+
+ return toolStyle;
+ }
+
+ // module/private exports
+ cornerstoneTools.toolStyle = toolStyleManager();
+
+})(cornerstoneTools);
+
+// End Source; src/stateManagement/toolStyleManager.js
+
+// Begin Source: src/stateManagement/touchSettingsManager.js
+(function(cornerstoneTools) {
+
+ 'use strict';
+
+ function touchSettingsManager() {
+ var defaultDistanceFromTouch = {
+ x: 0,
+ y: -45
+ };
+
+ function setToolDistanceFromTouch(distance){
+ defaultDistanceFromTouch = distance;
+ }
+
+ function getToolDistanceFromTouch(){
+ return defaultDistanceFromTouch;
+ }
+
+ var touchSettings = {
+ setToolDistanceFromTouch: setToolDistanceFromTouch,
+ getToolDistanceFromTouch: getToolDistanceFromTouch,
+ };
+
+ return touchSettings;
+ }
+
+ // module/private exports
+ cornerstoneTools.touchSettings = touchSettingsManager();
+
+})(cornerstoneTools);
+
+// End Source; src/stateManagement/touchSettingsManager.js
+
+// Begin Source: src/synchronization/panZoomSynchronizer.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ // This function synchronizes the target zoom and pan to match the source
+ function panZoomSynchronizer(synchronizer, sourceElement, targetElement) {
+
+ // ignore the case where the source and target are the same enabled element
+ if (targetElement === sourceElement) {
+ return;
+ }
+ // get the source and target viewports
+ var sourceViewport = cornerstone.getViewport(sourceElement);
+ var targetViewport = cornerstone.getViewport(targetElement);
+
+ // do nothing if the scale and translation are the same
+ if (targetViewport.scale === sourceViewport.scale && targetViewport.translation.x === sourceViewport.translation.x && targetViewport.translation.y === sourceViewport.translation.y) {
+ return;
+ }
+
+ // scale and/or translation are different, sync them
+ targetViewport.scale = sourceViewport.scale;
+ targetViewport.translation.x = sourceViewport.translation.x;
+ targetViewport.translation.y = sourceViewport.translation.y;
+ synchronizer.setViewport(targetElement, targetViewport);
+ }
+
+ // module/private exports
+ cornerstoneTools.panZoomSynchronizer = panZoomSynchronizer;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/synchronization/panZoomSynchronizer.js
+
+// Begin Source: src/synchronization/stackImageIndexSynchronizer.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ // This function causes the image in the target stack to be set to the one closest
+ // to the image in the source stack by image position
+ function stackImageIndexSynchronizer(synchronizer, sourceElement, targetElement) {
+
+ // ignore the case where the source and target are the same enabled element
+ if (targetElement === sourceElement) {
+ return;
+ }
+
+ var sourceStackToolDataSource = cornerstoneTools.getToolState(sourceElement, 'stack');
+ var sourceStackData = sourceStackToolDataSource.data[0];
+ var targetStackToolDataSource = cornerstoneTools.getToolState(targetElement, 'stack');
+ var targetStackData = targetStackToolDataSource.data[0];
+
+ var newImageIdIndex = sourceStackData.currentImageIdIndex;
+
+ // clamp the index
+ newImageIdIndex = Math.min(Math.max(newImageIdIndex, 0), targetStackData.imageIds.length - 1);
+
+ // Do nothing if the index has not changed
+ if (newImageIdIndex === targetStackData.currentImageIdIndex) {
+ return;
+ }
+
+ var startLoadingHandler = cornerstoneTools.loadHandlerManager.getStartLoadHandler();
+ var endLoadingHandler = cornerstoneTools.loadHandlerManager.getEndLoadHandler();
+ var errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler();
+
+ if (startLoadingHandler) {
+ startLoadingHandler(targetElement);
+ }
+
+ cornerstone.loadAndCacheImage(targetStackData.imageIds[newImageIdIndex]).then(function(image) {
+ var viewport = cornerstone.getViewport(targetElement);
+ targetStackData.currentImageIdIndex = newImageIdIndex;
+ synchronizer.displayImage(targetElement, image, viewport);
+ if (endLoadingHandler) {
+ endLoadingHandler(targetElement);
+ }
+ }, function(error) {
+ var imageId = targetStackData.imageIds[newImageIdIndex];
+ if (errorLoadingHandler) {
+ errorLoadingHandler(targetElement, imageId, error);
+ }
+ });
+ }
+
+ // module/private exports
+ cornerstoneTools.stackImageIndexSynchronizer = stackImageIndexSynchronizer;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/synchronization/stackImageIndexSynchronizer.js
+
+// Begin Source: src/synchronization/stackImagePositionOffsetSynchronizer.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ // This function causes the image in the target stack to be set to the one closest
+ // to the image in the source stack by image position
+
+ // In the future we will want to have a way to manually register links sets of the same orientation (e.g. an axial link set from a prior with an axial link set of a current). The user could do this by scrolling the two stacks to a similar location and then doing a user action (e.g. right click link) at which point the system will capture the delta between the image position (patient) of both stacks and use that to sync them. This offset will need to be adjustable.
+
+ function stackImagePositionOffsetSynchronizer(synchronizer, sourceElement, targetElement, eventData, positionDifference) {
+
+ // ignore the case where the source and target are the same enabled element
+ if (targetElement === sourceElement) {
+ return;
+ }
+
+ var sourceEnabledElement = cornerstone.getEnabledElement(sourceElement);
+ var sourceImagePlane = cornerstoneTools.metaData.get('imagePlane', sourceEnabledElement.image.imageId);
+ var sourceImagePosition = sourceImagePlane.imagePositionPatient;
+
+ var stackToolDataSource = cornerstoneTools.getToolState(targetElement, 'stack');
+ var stackData = stackToolDataSource.data[0];
+
+ var minDistance = Number.MAX_VALUE;
+ var newImageIdIndex = -1;
+
+ if (!positionDifference) {
+ return;
+ }
+
+ var finalPosition = sourceImagePosition.clone().add(positionDifference);
+
+ stackData.imageIds.forEach(function(imageId, index) {
+ var imagePlane = cornerstoneTools.metaData.get('imagePlane', imageId);
+ var imagePosition = imagePlane.imagePositionPatient;
+ var distance = finalPosition.distanceToSquared(imagePosition);
+
+ if (distance < minDistance) {
+ minDistance = distance;
+ newImageIdIndex = index;
+ }
+ });
+
+ if (newImageIdIndex === stackData.currentImageIdIndex || newImageIdIndex === -1) {
+ return;
+ }
+
+ var startLoadingHandler = cornerstoneTools.loadHandlerManager.getStartLoadHandler();
+ var endLoadingHandler = cornerstoneTools.loadHandlerManager.getEndLoadHandler();
+ var errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler();
+
+ if (startLoadingHandler) {
+ startLoadingHandler(targetElement);
+ }
+
+ cornerstone.loadAndCacheImage(stackData.imageIds[newImageIdIndex]).then(function(image) {
+ var viewport = cornerstone.getViewport(targetElement);
+ stackData.currentImageIdIndex = newImageIdIndex;
+ synchronizer.displayImage(targetElement, image, viewport);
+ if (endLoadingHandler) {
+ endLoadingHandler(targetElement);
+ }
+ }, function(error) {
+ var imageId = stackData.imageIds[newImageIdIndex];
+ if (errorLoadingHandler) {
+ errorLoadingHandler(targetElement, imageId, error);
+ }
+ });
+ }
+
+ // module/private exports
+ cornerstoneTools.stackImagePositionOffsetSynchronizer = stackImagePositionOffsetSynchronizer;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/synchronization/stackImagePositionOffsetSynchronizer.js
+
+// Begin Source: src/synchronization/stackImagePositionSynchronizer.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ // This function causes the image in the target stack to be set to the one closest
+ // to the image in the source stack by image position
+ function stackImagePositionSynchronizer(synchronizer, sourceElement, targetElement) {
+
+ // ignore the case where the source and target are the same enabled element
+ if (targetElement === sourceElement) {
+ return;
+ }
+
+ var sourceImage = cornerstone.getEnabledElement(sourceElement).image;
+ var sourceImagePlane = cornerstoneTools.metaData.get('imagePlane', sourceImage.imageId);
+ var sourceImagePosition = sourceImagePlane.imagePositionPatient;
+
+ var stackToolDataSource = cornerstoneTools.getToolState(targetElement, 'stack');
+ var stackData = stackToolDataSource.data[0];
+
+ var minDistance = Number.MAX_VALUE;
+ var newImageIdIndex = -1;
+
+ $.each(stackData.imageIds, function(index, imageId) {
+ var imagePlane = cornerstoneTools.metaData.get('imagePlane', imageId);
+ var imagePosition = imagePlane.imagePositionPatient;
+ var distance = imagePosition.distanceToSquared(sourceImagePosition);
+ //console.log(index + '=' + distance);
+ if (distance < minDistance) {
+ minDistance = distance;
+ newImageIdIndex = index;
+ }
+ });
+
+ if (newImageIdIndex === stackData.currentImageIdIndex) {
+ return;
+ }
+
+ var startLoadingHandler = cornerstoneTools.loadHandlerManager.getStartLoadHandler();
+ var endLoadingHandler = cornerstoneTools.loadHandlerManager.getEndLoadHandler();
+ var errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler();
+
+ if (startLoadingHandler) {
+ startLoadingHandler(targetElement);
+ }
+
+ if (newImageIdIndex !== -1) {
+ cornerstone.loadAndCacheImage(stackData.imageIds[newImageIdIndex]).then(function(image) {
+ var viewport = cornerstone.getViewport(targetElement);
+ stackData.currentImageIdIndex = newImageIdIndex;
+ synchronizer.displayImage(targetElement, image, viewport);
+ if (endLoadingHandler) {
+ endLoadingHandler(targetElement);
+ }
+ }, function(error) {
+ var imageId = stackData.imageIds[newImageIdIndex];
+ if (errorLoadingHandler) {
+ errorLoadingHandler(targetElement, imageId, error);
+ }
+ });
+ }
+ }
+
+ // module/private exports
+ cornerstoneTools.stackImagePositionSynchronizer = stackImagePositionSynchronizer;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/synchronization/stackImagePositionSynchronizer.js
+
+// Begin Source: src/synchronization/stackScrollSynchronizer.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ // This function causes any scrolling actions within the stack to propagate to
+ // all of the other viewports that are synced
+ function stackScrollSynchronizer(synchronizer, sourceElement, targetElement, eventData) {
+ // If the target and source are the same, stop
+ if (sourceElement === targetElement) {
+ return;
+ }
+
+ // If there is no event, or direction is 0, stop
+ if (!eventData || !eventData.direction) {
+ return;
+ }
+
+ // Get the stack of the target viewport
+ var stackToolDataSource = cornerstoneTools.getToolState(targetElement, 'stack');
+ var stackData = stackToolDataSource.data[0];
+
+ // Get the new index for the stack
+ var newImageIdIndex = stackData.currentImageIdIndex + eventData.direction;
+
+ // Ensure the index does not exceed the bounds of the stack
+ newImageIdIndex = Math.min(Math.max(newImageIdIndex, 0), stackData.imageIds.length - 1);
+
+ // If the index has not changed, stop here
+ if (stackData.currentImageIdIndex === newImageIdIndex) {
+ return;
+ }
+
+ var startLoadingHandler = cornerstoneTools.loadHandlerManager.getStartLoadHandler();
+ var endLoadingHandler = cornerstoneTools.loadHandlerManager.getEndLoadHandler();
+ var errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler();
+
+ if (startLoadingHandler) {
+ startLoadingHandler(targetElement);
+ }
+
+ cornerstone.loadAndCacheImage(stackData.imageIds[newImageIdIndex]).then(function(image) {
+ var viewport = cornerstone.getViewport(targetElement);
+ stackData.currentImageIdIndex = newImageIdIndex;
+ synchronizer.displayImage(targetElement, image, viewport);
+ if (endLoadingHandler) {
+ endLoadingHandler(targetElement);
+ }
+ }, function(error) {
+ var imageId = stackData.imageIds[newImageIdIndex];
+ if (errorLoadingHandler) {
+ errorLoadingHandler(targetElement, imageId, error);
+ }
+ });
+ }
+
+ // module/private exports
+ cornerstoneTools.stackScrollSynchronizer = stackScrollSynchronizer;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/synchronization/stackScrollSynchronizer.js
+
+// Begin Source: src/synchronization/synchronizer.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ // This object is responsible for synchronizing target elements when an event fires on a source
+ // element
+ function Synchronizer(event, handler) {
+
+ var that = this;
+ var sourceElements = []; // source elements fire the events we want to synchronize to
+ var targetElements = []; // target elements we want to synchronize to source elements
+
+ var ignoreFiredEvents = false;
+ var initialData = {};
+ var eventHandler = handler;
+
+ this.setHandler = function(handler) {
+ eventHandler = handler;
+ };
+
+ this.getHandler = function() {
+ return eventHandler;
+ };
+
+ this.getDistances = function() {
+ if (!sourceElements.length || !targetElements.length) {
+ return;
+ }
+
+ initialData.distances = {};
+ initialData.imageIds = {
+ sourceElements: [],
+ targetElements: []
+ };
+
+ sourceElements.forEach(function(sourceElement) {
+ var sourceEnabledElement = cornerstone.getEnabledElement(sourceElement);
+ var sourceImageId = sourceEnabledElement.image.imageId;
+ var sourceImagePlane = cornerstoneTools.metaData.get('imagePlane', sourceImageId);
+ var sourceImagePosition = sourceImagePlane.imagePositionPatient;
+
+ if (initialData.hasOwnProperty(sourceEnabledElement)) {
+ return;
+ } else {
+ initialData.distances[sourceImageId] = {};
+ }
+
+ initialData.imageIds.sourceElements.push(sourceImageId);
+
+ targetElements.forEach(function(targetElement) {
+ var targetEnabledElement = cornerstone.getEnabledElement(targetElement);
+ var targetImageId = targetEnabledElement.image.imageId;
+
+ initialData.imageIds.targetElements.push(targetImageId);
+
+ if (sourceElement === targetElement) {
+ return;
+ }
+
+ if (sourceImageId === targetImageId) {
+ return;
+ }
+
+ if (initialData.distances[sourceImageId].hasOwnProperty(targetImageId)) {
+ return;
+ }
+
+ var targetImagePlane = cornerstoneTools.metaData.get('imagePlane', targetImageId);
+ var targetImagePosition = targetImagePlane.imagePositionPatient;
+
+ initialData.distances[sourceImageId][targetImageId] = targetImagePosition.clone().sub(sourceImagePosition);
+ });
+
+ if (!Object.keys(initialData.distances[sourceImageId]).length) {
+ delete initialData.distances[sourceImageId];
+ }
+ });
+ };
+
+ function fireEvent(sourceElement, eventData) {
+ // Broadcast an event that something changed
+ if (!sourceElements.length || !targetElements.length) {
+ return;
+ }
+
+ ignoreFiredEvents = true;
+ targetElements.forEach(function(targetElement) {
+ var targetIndex = targetElements.indexOf(targetElement);
+ if (targetIndex === -1) {
+ return;
+ }
+
+ var targetImageId = initialData.imageIds.targetElements[targetIndex];
+ var sourceIndex = sourceElements.indexOf(sourceElement);
+ if (sourceIndex === -1) {
+ return;
+ }
+
+ var sourceImageId = initialData.imageIds.sourceElements[sourceIndex];
+
+ var positionDifference;
+ if (sourceImageId === targetImageId) {
+ positionDifference = 0;
+ } else {
+ positionDifference = initialData.distances[sourceImageId][targetImageId];
+ }
+
+ eventHandler(that, sourceElement, targetElement, eventData, positionDifference);
+ });
+ ignoreFiredEvents = false;
+ }
+
+ function onEvent(e, eventData) {
+ if (ignoreFiredEvents === true) {
+ return;
+ }
+
+ fireEvent(e.currentTarget, eventData);
+ }
+
+ // adds an element as a source
+ this.addSource = function(element) {
+ // Return if this element was previously added
+ var index = sourceElements.indexOf(element);
+ if (index !== -1) {
+ return;
+ }
+
+ // Add to our list of enabled elements
+ sourceElements.push(element);
+
+ // subscribe to the event
+ $(element).on(event, onEvent);
+
+ // Update the inital distances between elements
+ that.getDistances();
+
+ that.updateDisableHandlers();
+ };
+
+ // adds an element as a target
+ this.addTarget = function(element) {
+ // Return if this element was previously added
+ var index = targetElements.indexOf(element);
+ if (index !== -1) {
+ return;
+ }
+
+ // Add to our list of enabled elements
+ targetElements.push(element);
+
+ // Update the inital distances between elements
+ that.getDistances();
+
+ // Invoke the handler for this new target element
+ eventHandler(that, element, element, 0);
+
+ that.updateDisableHandlers();
+ };
+
+ // adds an element as both a source and a target
+ this.add = function(element) {
+ that.addSource(element);
+ that.addTarget(element);
+ };
+
+ // removes an element as a source
+ this.removeSource = function(element) {
+ // Find the index of this element
+ var index = sourceElements.indexOf(element);
+ if (index === -1) {
+ return;
+ }
+
+ // remove this element from the array
+ sourceElements.splice(index, 1);
+
+ // stop listening for the event
+ $(element).off(event, onEvent);
+
+ // Update the inital distances between elements
+ that.getDistances();
+
+ // Update everyone listening for events
+ fireEvent(element);
+ that.updateDisableHandlers();
+ };
+
+ // removes an element as a target
+ this.removeTarget = function(element) {
+ // Find the index of this element
+ var index = targetElements.indexOf(element);
+ if (index === -1) {
+ return;
+ }
+
+ // remove this element from the array
+ targetElements.splice(index, 1);
+
+ // Update the inital distances between elements
+ that.getDistances();
+
+ // Invoke the handler for the removed target
+ eventHandler(that, element, element, 0);
+ that.updateDisableHandlers();
+ };
+
+ // removes an element as both a source and target
+ this.remove = function(element) {
+ that.removeTarget(element);
+ that.removeSource(element);
+ };
+
+ // returns the source elements
+ this.getSourceElements = function() {
+ return sourceElements;
+ };
+
+ // returns the target elements
+ this.getTargetElements = function() {
+ return targetElements;
+ };
+
+ this.displayImage = function(element, image, viewport) {
+ ignoreFiredEvents = true;
+ cornerstone.displayImage(element, image, viewport);
+ ignoreFiredEvents = false;
+ };
+
+ this.setViewport = function(element, viewport) {
+ ignoreFiredEvents = true;
+ cornerstone.setViewport(element, viewport);
+ ignoreFiredEvents = false;
+ };
+
+ function disableHandler(e, eventData) {
+ var element = eventData.element;
+ that.remove(element);
+ }
+
+ this.updateDisableHandlers = function() {
+ var elements = $.unique(sourceElements.concat(targetElements));
+ elements.forEach(function(element) {
+ $(element).off('CornerstoneElementDisabled', disableHandler);
+ $(element).on('CornerstoneElementDisabled', disableHandler);
+ });
+ };
+
+ this.destroy = function() {
+ var elements = $.unique(sourceElements.concat(targetElements));
+ elements.forEach(function(element) {
+ that.remove(element);
+ });
+ };
+ }
+
+ // module/private exports
+ cornerstoneTools.Synchronizer = Synchronizer;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/synchronization/synchronizer.js
+
+// Begin Source: src/synchronization/updateImageSynchronizer.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ // This function causes the target image to be drawn immediately
+ function updateImageSynchronizer(synchronizer, sourceElement, targetElement) {
+
+ // ignore the case where the source and target are the same enabled element
+ if (targetElement === sourceElement) {
+ return;
+ }
+
+ cornerstone.updateImage(targetElement);
+ }
+
+ // module/private exports
+ cornerstoneTools.updateImageSynchronizer = updateImageSynchronizer;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/synchronization/updateImageSynchronizer.js
+
+// Begin Source: src/synchronization/wwwcSynchronizer.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ // This function synchronizes the target element ww/wc to match the source element
+ function wwwcSynchronizer(synchronizer, sourceElement, targetElement) {
+
+ // ignore the case where the source and target are the same enabled element
+ if (targetElement === sourceElement) {
+ return;
+ }
+ // get the source and target viewports
+ var sourceViewport = cornerstone.getViewport(sourceElement);
+ var targetViewport = cornerstone.getViewport(targetElement);
+
+ // do nothing if the ww/wc already match
+ if (targetViewport.voi.windowWidth === sourceViewport.voi.windowWidth && targetViewport.voi.windowCenter === sourceViewport.voi.windowCenter && targetViewport.invert === sourceViewport.invert) {
+ return;
+ }
+
+ // www/wc are different, sync them
+ targetViewport.voi.windowWidth = sourceViewport.voi.windowWidth;
+ targetViewport.voi.windowCenter = sourceViewport.voi.windowCenter;
+ targetViewport.invert = sourceViewport.invert;
+ synchronizer.setViewport(targetElement, targetViewport);
+ }
+
+ // module/private exports
+ cornerstoneTools.wwwcSynchronizer = wwwcSynchronizer;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/synchronization/wwwcSynchronizer.js
+
+// Begin Source: src/timeSeriesTools/ProbeTool4D.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ var toolType = 'probe4D';
+
+ function updateLineSample(measurementData) {
+ var samples = [];
+
+ measurementData.timeSeries.stacks.forEach(function(stack) {
+ cornerstone.loadAndCacheImage(stack.imageIds[measurementData.imageIdIndex]).then(function(image) {
+ var offset = Math.round(measurementData.handles.end.x) + Math.round(measurementData.handles.end.y) * image.width;
+ var sample = image.getPixelData()[offset];
+ samples.push(sample);
+ //console.log(sample);
+ });
+ });
+ measurementData.lineSample.set(samples);
+ }
+
+ ///////// BEGIN ACTIVE TOOL ///////
+ function createNewMeasurement(mouseEventData) {
+ var timeSeriestoolData = cornerstoneTools.getToolState(mouseEventData.element, 'timeSeries');
+ if (timeSeriestoolData === undefined || timeSeriestoolData.data === undefined || timeSeriestoolData.data.length === 0) {
+ return;
+ }
+
+ var timeSeries = timeSeriestoolData.data[0];
+
+ // 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: {
+ end: {
+ x: mouseEventData.currentPoints.image.x,
+ y: mouseEventData.currentPoints.image.y,
+ highlight: true,
+ active: true
+ }
+ }
+ };
+ updateLineSample(measurementData);
+ cornerstoneTools.MeasurementManager.add(measurementData);
+ return measurementData;
+ }
+ ///////// END ACTIVE TOOL ///////
+
+ ///////// BEGIN IMAGE RENDERING ///////
+
+ function onImageRendered(e, eventData) {
+
+ // 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;
+ }
+
+ // we have tool data for this element - iterate over each one and draw it
+ var context = eventData.canvasContext.canvas.getContext('2d');
+ cornerstone.setToPixelCoordinateSystem(eventData.enabledElement, context);
+ var color = 'white';
+
+ for (var i = 0; i < toolData.data.length; i++) {
+ context.save();
+ var data = toolData.data[i];
+
+ // draw the handles
+ context.beginPath();
+ cornerstoneTools.drawHandles(context, eventData, data.handles, color);
+ context.stroke();
+
+ // Draw text
+ var fontParameters = cornerstoneTools.setContextToDisplayFontSize(eventData.enabledElement, eventData.canvasContext, 15);
+ context.font = '' + fontParameters.fontSize + 'px Arial';
+
+ // translate the x/y away from the cursor
+ var x = Math.round(data.handles.end.x);
+ var y = Math.round(data.handles.end.y);
+ var textX = data.handles.end.x + 3;
+ var textY = data.handles.end.y - 3;
+
+ context.fillStyle = color;
+
+ context.fillText('' + x + ',' + y, textX, textY);
+
+ context.restore();
+ }
+ }
+ ///////// END IMAGE RENDERING ///////
+
+ // module exports
+ cornerstoneTools.probeTool4D = cornerstoneTools.mouseButtonTool({
+ createNewMeasurement: createNewMeasurement, onImageRendered: onImageRendered, toolType: toolType
+ });
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/timeSeriesTools/ProbeTool4D.js
+
+// Begin Source: src/timeSeriesTools/timeSeries.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function incrementTimePoint(element, timePoints, wrap) {
+ var toolData = cornerstoneTools.getToolState(element, 'timeSeries');
+ if (toolData === undefined || toolData.data === undefined || toolData.data.length === 0) {
+ return;
+ }
+
+ var timeSeriesData = toolData.data[0];
+ var currentStack = timeSeriesData.stacks[timeSeriesData.currentStackIndex];
+ var currentImageIdIndex = currentStack.currentImageIdIndex;
+ var newStackIndex = timeSeriesData.currentStackIndex + timePoints;
+
+ // loop around if we go outside the stack
+ if (wrap) {
+ if (newStackIndex >= timeSeriesData.stacks.length) {
+ newStackIndex = 0;
+ }
+
+ if (newStackIndex < 0) {
+ newStackIndex = timeSeriesData.stacks.length - 1;
+ }
+ } else {
+ newStackIndex = Math.min(timeSeriesData.stacks.length - 1, newStackIndex);
+ newStackIndex = Math.max(0, newStackIndex);
+ }
+
+ if (newStackIndex !== timeSeriesData.currentStackIndex) {
+ var viewport = cornerstone.getViewport(element);
+ var newStack = timeSeriesData.stacks[newStackIndex];
+
+ var startLoadingHandler = cornerstoneTools.loadHandlerManager.getStartLoadHandler();
+ var endLoadingHandler = cornerstoneTools.loadHandlerManager.getEndLoadHandler();
+ var errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler();
+
+ if (startLoadingHandler) {
+ startLoadingHandler(element);
+ }
+
+ cornerstone.loadAndCacheImage(newStack.imageIds[currentImageIdIndex]).then(function(image) {
+ if (timeSeriesData.currentImageIdIndex !== currentImageIdIndex) {
+ newStack.currentImageIdIndex = currentImageIdIndex;
+ timeSeriesData.currentStackIndex = newStackIndex;
+ cornerstone.displayImage(element, image, viewport);
+ if (endLoadingHandler) {
+ endLoadingHandler(element);
+ }
+ }
+ }, function(error) {
+ var imageId = newStack.imageIds[currentImageIdIndex];
+ if (errorLoadingHandler) {
+ errorLoadingHandler(element, imageId, error);
+ }
+ });
+ }
+ }
+
+ // module/private exports
+ cornerstoneTools.incrementTimePoint = incrementTimePoint;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/timeSeriesTools/timeSeries.js
+
+// Begin Source: src/timeSeriesTools/timeSeriesPlayer.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ var toolType = 'timeSeriesPlayer';
+
+ /**
+ * Starts playing a clip or adjusts the frame rate of an already playing clip. framesPerSecond is
+ * optional and defaults to 30 if not specified. A negative framesPerSecond will play the clip in reverse.
+ * The element must be a stack of images
+ * @param element
+ * @param framesPerSecond
+ */
+ function playClip(element, framesPerSecond) {
+ if (element === undefined) {
+ throw 'playClip: element must not be undefined';
+ }
+
+ if (framesPerSecond === undefined) {
+ framesPerSecond = 30;
+ }
+
+ var timeSeriesToolData = cornerstoneTools.getToolState(element, 'timeSeries');
+ if (timeSeriesToolData === undefined || timeSeriesToolData.data === undefined || timeSeriesToolData.data.length === 0) {
+ return;
+ }
+
+ var playClipToolData = cornerstoneTools.getToolState(element, toolType);
+ var playClipData;
+ if (playClipToolData === undefined || playClipToolData.data.length === 0) {
+ playClipData = {
+ intervalId: undefined, framesPerSecond: framesPerSecond, lastFrameTimeStamp: undefined, frameRate: 0
+ };
+ cornerstoneTools.addToolState(element, toolType, playClipData);
+ } else {
+ playClipData = playClipToolData.data[0];
+ playClipData.framesPerSecond = framesPerSecond;
+ }
+
+ // if already playing, do not set a new interval
+ if (playClipData.intervalId !== undefined) {
+ return;
+ }
+
+ playClipData.intervalId = setInterval(function() {
+ if (playClipData.framesPerSecond > 0) {
+ cornerstoneTools.incrementTimePoint(element, 1, true);
+ } else {
+ cornerstoneTools.incrementTimePoint(element, -1, true);
+ }
+ }, 1000 / Math.abs(playClipData.framesPerSecond));
+ }
+
+ /**
+ * Stops an already playing clip.
+ * * @param element
+ */
+ function stopClip(element) {
+ var playClipToolData = cornerstoneTools.getToolState(element, toolType);
+ var playClipData;
+ if (playClipToolData === undefined || playClipToolData.data.length === 0) {
+ return;
+ } else {
+ playClipData = playClipToolData.data[0];
+ }
+
+ clearInterval(playClipData.intervalId);
+ playClipData.intervalId = undefined;
+ }
+
+ // module/private exports
+ cornerstoneTools.timeSeriesPlayer = {
+ start: playClip, stop: stopClip
+ };
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/timeSeriesTools/timeSeriesPlayer.js
+
+// Begin Source: src/timeSeriesTools/timeSeriesScroll.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function mouseUpCallback(e, eventData) {
+ $(eventData.element).off('CornerstoneToolsMouseDrag', mouseDragCallback);
+ $(eventData.element).off('CornerstoneToolsMouseUp', mouseUpCallback);
+ }
+
+ function mouseDownCallback(e, eventData) {
+ if (cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
+
+ var mouseDragEventData = {
+ deltaY: 0,
+ options: e.data.options
+ };
+ $(eventData.element).on('CornerstoneToolsMouseDrag', mouseDragEventData, mouseDragCallback);
+ $(eventData.element).on('CornerstoneToolsMouseUp', mouseUpCallback);
+ e.stopImmediatePropagation();
+ return false;
+ }
+ }
+
+ function mouseDragCallback(e, eventData) {
+ e.data.deltaY += eventData.deltaPoints.page.y;
+
+ var toolData = cornerstoneTools.getToolState(eventData.element, 'timeSeries');
+ if (toolData === undefined || toolData.data === undefined || toolData.data.length === 0) {
+ return;
+ }
+
+ var timeSeriesData = toolData.data[0];
+
+ var pixelsPerTimeSeries = $(eventData.element).height() / timeSeriesData.stacks.length ;
+ if (e.data.options !== undefined && e.data.options.timeSeriesScrollSpeed !== undefined) {
+ pixelsPerTimeSeries = e.data.options.timeSeriesScrollSpeed;
+ }
+
+ if (e.data.deltaY >= pixelsPerTimeSeries || e.data.deltaY <= -pixelsPerTimeSeries) {
+ var timeSeriesDelta = Math.round(e.data.deltaY / pixelsPerTimeSeries);
+ var timeSeriesDeltaMod = e.data.deltaY % pixelsPerTimeSeries;
+ cornerstoneTools.incrementTimePoint(eventData.element, timeSeriesDelta);
+ e.data.deltaY = timeSeriesDeltaMod;
+ }
+
+ return false; // false = cases jquery to preventDefault() and stopPropagation() this event
+ }
+
+ function mouseWheelCallback(e, eventData) {
+ var images = -eventData.direction;
+ cornerstoneTools.incrementTimePoint(eventData.element, images);
+ }
+
+ function onDrag(e) {
+ var mouseMoveData = e.originalEvent.detail;
+ var eventData = {
+ deltaY: 0
+ };
+ eventData.deltaY += mouseMoveData.deltaPoints.page.y;
+
+ var toolData = cornerstoneTools.getToolState(mouseMoveData.element, 'stack');
+ if (toolData === undefined || toolData.data === undefined || toolData.data.length === 0) {
+ return;
+ }
+
+ if (eventData.deltaY >= 3 || eventData.deltaY <= -3) {
+ var timeSeriesDelta = eventData.deltaY / 3;
+ var timeSeriesDeltaMod = eventData.deltaY % 3;
+ cornerstoneTools.setTimePoint(eventData.element, timeSeriesDelta);
+ eventData.deltaY = timeSeriesDeltaMod;
+ }
+
+ return false; // false = cases jquery to preventDefault() and stopPropagation() this event
+ }
+
+ // module/private exports
+ cornerstoneTools.timeSeriesScroll = cornerstoneTools.simpleMouseButtonTool(mouseDownCallback);
+ cornerstoneTools.timeSeriesScrollWheel = cornerstoneTools.mouseWheelTool(mouseWheelCallback);
+ cornerstoneTools.timeSeriesScrollTouchDrag = cornerstoneTools.touchDragTool(onDrag);
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/timeSeriesTools/timeSeriesScroll.js
+
+// Begin Source: src/util/RoundToDecimal.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function roundToDecimal(value, precision) {
+ var multiplier = Math.pow(10, precision);
+ return (Math.round(value * multiplier) / multiplier);
+ }
+
+ // module exports
+ cornerstoneTools.roundToDecimal = roundToDecimal;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/util/RoundToDecimal.js
+
+// Begin Source: src/util/calculateSUV.js
+(function(cornerstoneTools) {
+
+ 'use strict';
+
+ function calculateSUV(image, storedPixelValue) {
+ // if no dicom data set, return undefined
+ if (image.data === undefined) {
+ return undefined;
+ }
+
+ // image must be PET
+ if (image.data.string('x00080060') !== 'PT') {
+ return undefined;
+ }
+
+ var modalityPixelValue = storedPixelValue * image.slope + image.intercept;
+
+ var patientWeight = image.data.floatString('x00101030'); // in kg
+ if (patientWeight === undefined) {
+ return undefined;
+ }
+
+ var petSequence = image.data.elements.x00540016;
+ if (petSequence === undefined) {
+ return undefined;
+ }
+
+ petSequence = petSequence.items[0].dataSet;
+ var startTime = petSequence.time('x00181072');
+ var totalDose = petSequence.floatString('x00181074');
+ var halfLife = petSequence.floatString('x00181075');
+ var acquisitionTime = image.data.time('x00080032');
+
+ if (!startTime || !totalDose || !halfLife || !acquisitionTime) {
+ return undefined;
+ }
+
+ var acquisitionTimeInSeconds = acquisitionTime.fractionalSeconds + acquisitionTime.seconds + acquisitionTime.minutes * 60 + acquisitionTime.hours * 60 * 60;
+ var injectionStartTimeInSeconds = startTime.fractionalSeconds + startTime.seconds + startTime.minutes * 60 + startTime.hours * 60 * 60;
+ var durationInSeconds = acquisitionTimeInSeconds - injectionStartTimeInSeconds;
+ var correctedDose = totalDose * Math.exp(-durationInSeconds * Math.log(2) / halfLife);
+ var suv = modalityPixelValue * patientWeight / correctedDose * 1000;
+
+ return suv;
+ }
+
+ // module exports
+ cornerstoneTools.calculateSUV = calculateSUV;
+
+})(cornerstoneTools);
+
+// End Source; src/util/calculateSUV.js
+
+// Begin Source: src/util/copyPoints.js
+(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
+
+ 'use strict';
+
+ function copyPoints(points) {
+ var page = cornerstoneMath.point.copy(points.page);
+ var image = cornerstoneMath.point.copy(points.image);
+ var client = cornerstoneMath.point.copy(points.client);
+ var canvas = cornerstoneMath.point.copy(points.canvas);
+ return {
+ page: page, image: image, client: client, canvas: canvas
+ };
+ }
+
+ // module exports
+ cornerstoneTools.copyPoints = copyPoints;
+
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
+
+// End Source; src/util/copyPoints.js
+
+// Begin Source: src/util/drawEllipse.js
+(function(cornerstoneTools) {
+
+ 'use strict';
+
+ // http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
+ function drawEllipse(context, x, y, w, h) {
+ var kappa = 0.5522848,
+ ox = (w / 2) * kappa, // control point offset horizontal
+ oy = (h / 2) * kappa, // control point offset vertical
+ xe = x + w, // x-end
+ ye = y + h, // y-end
+ xm = x + w / 2, // x-middle
+ ym = y + h / 2; // y-middle
+
+ context.beginPath();
+ context.moveTo(x, ym);
+ context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
+ context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
+ context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
+ context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
+ context.closePath();
+ context.stroke();
+ }
+
+ // Module exports
+ cornerstoneTools.drawEllipse = drawEllipse;
+
+})(cornerstoneTools);
+
+// End Source; src/util/drawEllipse.js
+
+// Begin Source: src/util/drawTextBox.js
+(function(cornerstoneTools) {
+
+ 'use strict';
+
+ function drawTextBox(context, text, x, y, color) {
+ 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;
+
+ // 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);
+
+ context.restore();
+ }
+
+ // module exports
+ cornerstoneTools.drawTextBox = drawTextBox;
+
+})(cornerstoneTools);
+
+// End Source; src/util/drawTextBox.js
+
+// Begin Source: src/util/getLuminance.js
+(function(cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function getLuminance(element, x, y, width, height) {
+ if (!element) {
+ throw 'getLuminance: parameter element must not be undefined';
+ }
+
+ x = Math.round(x);
+ y = Math.round(y);
+ var enabledElement = cornerstone.getEnabledElement(element);
+ var luminance = [];
+ var index = 0;
+ var pixelData = enabledElement.image.getPixelData();
+ var spIndex,
+ row,
+ column;
+
+ if (enabledElement.image.color) {
+ for (row = 0; row < height; row++) {
+ for (column = 0; column < width; column++) {
+ spIndex = (((row + y) * enabledElement.image.columns) + (column + x)) * 4;
+ var red = pixelData[spIndex];
+ var green = pixelData[spIndex + 1];
+ var blue = pixelData[spIndex + 2];
+ luminance[index++] = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
+ }
+ }
+ } else {
+ for (row = 0; row < height; row++) {
+ for (column = 0; column < width; column++) {
+ spIndex = ((row + y) * enabledElement.image.columns) + (column + x);
+ luminance[index++] = pixelData[spIndex];
+ }
+ }
+ }
+
+ return luminance;
+ }
+
+ // module exports
+ cornerstoneTools.getLuminance = getLuminance;
+
+})(cornerstone, cornerstoneTools);
+
+// End Source; src/util/getLuminance.js
+
+// Begin Source: src/util/getMaxSimultaneousRequests.js
+(function(cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ // Maximum concurrent connections to the same server
+ // Information from http://sgdev-blog.blogspot.fr/2014/01/maximum-concurrent-connection-to-same.html
+ var maxSimultaneousRequests = {
+ default: 6,
+ IE: {
+ 9: 6,
+ 10: 8,
+ default: 8
+ },
+ Firefox: {
+ default: 6
+ },
+ Opera: {
+ 10: 8,
+ 11: 6,
+ 12: 6,
+ default: 6
+ },
+ Chrome: {
+ default: 6
+ },
+ Safari: {
+ default: 4
+ }
+ };
+
+ // Browser name / version detection
+ // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
+ function getBrowserInfo() {
+ var ua = navigator.userAgent,
+ M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [],
+ tem;
+
+ if (/trident/i.test(M[1])) {
+ tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
+ return 'IE ' + (tem[1] || '');
+ }
+
+ if (M[1] === 'Chrome') {
+ tem = ua.match(/\b(OPR|Edge)\/(\d+)/);
+ if (tem !== null) {
+ return tem.slice(1).join(' ').replace('OPR', 'Opera');
+ }
+ }
+
+ M = M[2]? [ M[1], M[2] ]: [ navigator.appName, navigator.appVersion, '-?' ];
+ if ((tem = ua.match(/version\/(\d+)/i)) !== null) {
+ M.splice(1, 1, tem[1]);
+ }
+
+ return M.join(' ');
+ }
+
+ function getMaxSimultaneousRequests() {
+ var config = cornerstoneTools.stackPrefetch.getConfiguration();
+
+ // Give preference to user-chosen values
+ if (config.maxSimultaneousRequests) {
+ return config.maxSimultaneousRequests;
+ }
+
+ var infoString = getBrowserInfo();
+ var info = infoString.split(' ');
+ var browserName = info[0];
+ var browserVersion = info[1];
+ var browserData = maxSimultaneousRequests[browserName];
+
+ if (!browserData) {
+ return maxSimultaneousRequests['default'];
+ }
+
+ if (!browserData[browserVersion]) {
+ return browserData['default'];
+ }
+
+ return browserData[browserVersion];
+ }
+
+ // module exports
+ cornerstoneTools.getMaxSimultaneousRequests = getMaxSimultaneousRequests;
+ cornerstoneTools.getBrowserInfo = getBrowserInfo;
+
+})(cornerstone, cornerstoneTools);
+
+// End Source; src/util/getMaxSimultaneousRequests.js
+
+// Begin Source: src/util/getRGBPixels.js
+(function(cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function getRGBPixels(element, x, y, width, height) {
+ if (!element) {
+ throw 'getRGBPixels: parameter element must not be undefined';
+ }
+
+ x = Math.round(x);
+ y = Math.round(y);
+ var enabledElement = cornerstone.getEnabledElement(element);
+ var storedPixelData = [];
+ var index = 0;
+ var pixelData = enabledElement.image.getPixelData();
+ var spIndex,
+ row,
+ column;
+
+ if (enabledElement.image.color) {
+ for (row = 0; row < height; row++) {
+ for (column = 0; column < width; column++) {
+ spIndex = (((row + y) * enabledElement.image.columns) + (column + x)) * 4;
+ var red = pixelData[spIndex];
+ var green = pixelData[spIndex + 1];
+ var blue = pixelData[spIndex + 2];
+ var alpha = pixelData[spIndex + 3];
+ storedPixelData[index++] = red;
+ storedPixelData[index++] = green;
+ storedPixelData[index++] = blue;
+ storedPixelData[index++] = alpha;
+ }
+ }
+ }
+
+ return storedPixelData;
+ }
+
+ // module exports
+ cornerstoneTools.getRGBPixels = getRGBPixels;
+
+})(cornerstone, cornerstoneTools);
+
+// End Source; src/util/getRGBPixels.js
+
+// Begin Source: src/util/isMouseButtonEnabled.js
+(function(cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function isMouseButtonEnabled(which, mouseButtonMask) {
+ /*jshint bitwise: false*/
+ var mouseButton = (1 << (which - 1));
+ return ((mouseButtonMask & mouseButton) !== 0);
+ }
+
+ // module exports
+ cornerstoneTools.isMouseButtonEnabled = isMouseButtonEnabled;
+
+})(cornerstone, cornerstoneTools);
+
+// End Source; src/util/isMouseButtonEnabled.js
+
+// Begin Source: src/util/pauseEvent.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ /**
+ * This function is used to prevent selection from occuring when left click dragging on the image
+ * @param e the event that is provided to your event handler
+ * Based on: http://stackoverflow.com/questions/5429827/how-can-i-prevent-text-element-selection-with-cursor-drag
+ * @returns {boolean}
+ */
+ function pauseEvent(e) {
+ if (e.stopPropagation) {
+ e.stopPropagation();
+ }
+
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
+
+ e.cancelBubble = true;
+ e.returnValue = false;
+ return false;
+ }
+
+ // module exports
+ cornerstoneTools.pauseEvent = pauseEvent;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/util/pauseEvent.js
+
+// Begin Source: src/util/pointProjector.js
+(function($, cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ // projects a patient point to an image point
+ function projectPatientPointToImagePlane(patientPoint, imagePlane) {
+ var point = patientPoint.clone().sub(imagePlane.imagePositionPatient);
+ var x = imagePlane.rowCosines.dot(point) / imagePlane.columnPixelSpacing;
+ var y = imagePlane.columnCosines.dot(point) / imagePlane.rowPixelSpacing;
+ return {
+ x: x, y: y
+ };
+ }
+
+ // projects an image point to a patient point
+ function imagePointToPatientPoint(imagePoint, imagePlane) {
+ var x = imagePlane.rowCosines.clone().multiplyScalar(imagePoint.x);
+ x.multiplyScalar(imagePlane.columnPixelSpacing);
+ var y = imagePlane.columnCosines.clone().multiplyScalar(imagePoint.y);
+ y.multiplyScalar(imagePlane.rowPixelSpacing);
+ var patientPoint = x.add(y);
+ patientPoint.add(imagePlane.imagePositionPatient);
+ return patientPoint;
+ }
+
+ function getRectangleFromImagePlane(imagePlane) {
+ // Get the points
+ var topLeft = imagePointToPatientPoint({
+ x: 0,
+ y: 0
+ }, imagePlane);
+ var topRight = imagePointToPatientPoint({
+ x: imagePlane.columns,
+ y: 0
+ }, imagePlane);
+ var bottomLeft = imagePointToPatientPoint({
+ x: 0,
+ y: imagePlane.rows
+ }, imagePlane);
+ var bottomRight = imagePointToPatientPoint({
+ x: imagePlane.columns,
+ y: imagePlane.rows
+ }, imagePlane);
+
+ // Get each side as a vector
+ var rect = {
+ top: new cornerstoneMath.Line3(topLeft, topRight),
+ left: new cornerstoneMath.Line3(topLeft, bottomLeft),
+ right: new cornerstoneMath.Line3(topRight, bottomRight),
+ bottom: new cornerstoneMath.Line3(bottomLeft, bottomRight),
+ };
+ return rect;
+ }
+
+ function lineRectangleIntersection(line, rect) {
+ var intersections = [];
+ Object.keys(rect).forEach(function(side) {
+ var segment = rect[side];
+ var intersection = line.intersectLine(segment);
+ if (intersection) {
+ intersections.push(intersection);
+ }
+ });
+ return intersections;
+ }
+
+ function planePlaneIntersection(targetImagePlane, referenceImagePlane) {
+ // Gets the line of intersection between two planes in patient space
+
+ // First, get the normals of each image plane
+ var targetNormal = targetImagePlane.rowCosines.clone().cross(targetImagePlane.columnCosines);
+ var targetPlane = new cornerstoneMath.Plane();
+ targetPlane.setFromNormalAndCoplanarPoint(targetNormal, targetImagePlane.imagePositionPatient);
+
+ var referenceNormal = referenceImagePlane.rowCosines.clone().cross(referenceImagePlane.columnCosines);
+ var referencePlane = new cornerstoneMath.Plane();
+ referencePlane.setFromNormalAndCoplanarPoint(referenceNormal, referenceImagePlane.imagePositionPatient);
+
+ var originDirection = referencePlane.clone().intersectPlane(targetPlane);
+ var origin = originDirection.origin;
+ var direction = originDirection.direction;
+
+ // Calculate the longest possible length in the reference image plane (the length of the diagonal)
+ var bottomRight = imagePointToPatientPoint({
+ x: referenceImagePlane.columns,
+ y: referenceImagePlane.rows
+ }, referenceImagePlane);
+ var distance = referenceImagePlane.imagePositionPatient.distanceTo(bottomRight);
+
+ // Use this distance to bound the ray intersecting the two planes
+ var line = new cornerstoneMath.Line3();
+ line.start = origin;
+ line.end = origin.clone().add(direction.multiplyScalar(distance));
+
+ // Find the intersections between this line and the reference image plane's four sides
+ var rect = getRectangleFromImagePlane(referenceImagePlane);
+ var intersections = lineRectangleIntersection(line, rect);
+
+ // Return the intersections between this line and the reference image plane's sides
+ // in order to draw the reference line from the target image.
+ if (intersections.length !== 2) {
+ return;
+ }
+
+ var points = {
+ start: intersections[0],
+ end: intersections[1]
+ };
+ return points;
+
+ }
+
+ // module/private exports
+ cornerstoneTools.projectPatientPointToImagePlane = projectPatientPointToImagePlane;
+ cornerstoneTools.imagePointToPatientPoint = imagePointToPatientPoint;
+ cornerstoneTools.planePlaneIntersection = planePlaneIntersection;
+
+})($, cornerstone, cornerstoneTools);
+
+// End Source; src/util/pointProjector.js
+
+// Begin Source: src/util/scroll.js
+(function(cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function scroll(element, images) {
+ var toolData = cornerstoneTools.getToolState(element, 'stack');
+ if (toolData === undefined || toolData.data === undefined || toolData.data.length === 0) {
+ return;
+ }
+
+ var stackData = toolData.data[0];
+
+ var newImageIdIndex = stackData.currentImageIdIndex + images;
+ newImageIdIndex = Math.min(stackData.imageIds.length - 1, newImageIdIndex);
+ newImageIdIndex = Math.max(0, newImageIdIndex);
+
+ cornerstoneTools.scrollToIndex(element, newImageIdIndex);
+ }
+
+ // module exports
+ cornerstoneTools.scroll = scroll;
+
+})(cornerstone, cornerstoneTools);
+
+// End Source; src/util/scroll.js
+
+// Begin Source: src/util/scrollToIndex.js
+(function(cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ function scrollToIndex(element, newImageIdIndex) {
+ var toolData = cornerstoneTools.getToolState(element, 'stack');
+ if (toolData === undefined || toolData.data === undefined || toolData.data.length === 0) {
+ return;
+ }
+
+ var stackData = toolData.data[0];
+
+ // Allow for negative indexing
+ if (newImageIdIndex < 0) {
+ newImageIdIndex += stackData.imageIds.length;
+ }
+
+ var startLoadingHandler = cornerstoneTools.loadHandlerManager.getStartLoadHandler();
+ var endLoadingHandler = cornerstoneTools.loadHandlerManager.getEndLoadHandler();
+ var errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler();
+
+ function doneCallback(image) {
+ //console.log('interaction done: ' + image.imageId);
+ if (stackData.currentImageIdIndex === newImageIdIndex) {
+ cornerstone.displayImage(element, image, viewport);
+ if (endLoadingHandler) {
+ endLoadingHandler(element);
+ }
+ }
+ }
+
+ function failCallback(error) {
+ var imageId = stackData.imageIds[newImageIdIndex];
+ if (errorLoadingHandler) {
+ errorLoadingHandler(element, imageId, error);
+ }
+ }
+
+ if (newImageIdIndex === stackData.currentImageIdIndex) {
+ return;
+ }
+
+ if (startLoadingHandler) {
+ startLoadingHandler(element);
+ }
+
+ var eventData = {
+ newImageIdIndex: newImageIdIndex,
+ direction: newImageIdIndex - stackData.currentImageIdIndex
+ };
+
+ stackData.currentImageIdIndex = newImageIdIndex;
+ var viewport = cornerstone.getViewport(element);
+ var newImageId = stackData.imageIds[newImageIdIndex];
+
+ // Retry image loading in cases where previous image promise
+ // was rejected, if the option is set
+ var config = cornerstoneTools.stackScroll.getConfiguration();
+ if (config && config.retryLoadOnScroll === true) {
+ var newImagePromise = cornerstone.imageCache.getImagePromise(newImageId);
+ if (newImagePromise && newImagePromise.state() === 'rejected') {
+ cornerstone.imageCache.removeImagePromise(newImageId);
+ }
+ }
+
+ var requestPoolManager = cornerstoneTools.requestPoolManager;
+ var type = 'interaction';
+
+ cornerstoneTools.requestPoolManager.clearRequestStack(type);
+
+ requestPoolManager.addRequest(element, newImageId, type, doneCallback, failCallback);
+ requestPoolManager.startGrabbing();
+
+ $(element).trigger('CornerstoneStackScroll', eventData);
+ }
+
+ // module exports
+ cornerstoneTools.scrollToIndex = scrollToIndex;
+ cornerstoneTools.loadHandlers = {};
+
+})(cornerstone, cornerstoneTools);
+
+// End Source; src/util/scrollToIndex.js
+
+// Begin Source: src/util/setContextToDisplayFontSize.js
+/**
+ * This module sets the transformation matrix for a canvas context so it displays fonts
+ * smoothly even when the image is highly scaled up
+ */
+(function(cornerstone, cornerstoneTools) {
+
+ 'use strict';
+
+ /**
+ * Sets the canvas context transformation matrix so it is scaled to show text
+ * more cleanly even if the image is scaled up. See
+ * https://github.com/chafey/cornerstoneTools/wiki/DrawingText
+ * for more information
+ *
+ * @param ee
+ * @param context
+ * @param fontSize
+ * @returns {{fontSize: number, lineHeight: number, fontScale: number}}
+ */
+ function setContextToDisplayFontSize(ee, context, fontSize) {
+ var fontScale = 0.1;
+ cornerstone.setToPixelCoordinateSystem(ee, context, fontScale);
+ // return the font size to use
+ var scaledFontSize = fontSize / ee.viewport.scale / fontScale;
+ // TODO: actually calculate this?
+ var lineHeight = fontSize / ee.viewport.scale / fontScale;
+ return {
+ fontSize: scaledFontSize, lineHeight: lineHeight, fontScale: fontScale
+ };
+ }
+
+ // Module exports
+ cornerstoneTools.setContextToDisplayFontSize = setContextToDisplayFontSize;
+
+})(cornerstone, cornerstoneTools);
+
+// End Source; src/util/setContextToDisplayFontSize.js
diff --git a/Packages/cornerstone/client/cornerstoneWADOImageLoader.js b/Packages/cornerstone/client/cornerstoneWADOImageLoader.js
new file mode 100644
index 000000000..6ede61230
--- /dev/null
+++ b/Packages/cornerstone/client/cornerstoneWADOImageLoader.js
@@ -0,0 +1,4202 @@
+/*! cornerstone-wado-image-loader - v0.7.2 - 2015-09-18 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneWADOImageLoader */
+//
+// This is a cornerstone image loader for WADO-URI requests. It has limited support for compressed
+// transfer syntaxes, check here to see what is currently supported:
+//
+// https://github.com/chafey/cornerstoneWADOImageLoader/blob/master/docs/TransferSyntaxes.md
+//
+// It will support implicit little endian transfer syntaxes but explicit little endian is strongly preferred
+// to avoid any parsing issues related to SQ elements. To request that the WADO object be returned as explicit little endian, append
+// the following on your WADO url: &transferSyntax=1.2.840.10008.1.2.1
+//
+
+if(typeof cornerstone === 'undefined'){
+ cornerstone = {};
+}
+if(typeof cornerstoneWADOImageLoader === 'undefined'){
+ cornerstoneWADOImageLoader = {
+ internal: {
+ options : {
+ // callback allowing customization of the xhr (e.g. adding custom auth headers, cors, etc)
+ beforeSend: function (xhr) {
+ }
+ },
+ multiFrameCacheHack : {}
+ }
+ };
+}
+
+
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ function convertRGB(dataSet, decodedImageFrame, rgbaBuffer) {
+ var planarConfiguration = dataSet.uint16('x00280006');
+ if(planarConfiguration === 0) {
+ cornerstoneWADOImageLoader.convertRGBColorByPixel(decodedImageFrame, rgbaBuffer);
+ } else {
+ cornerstoneWADOImageLoader.convertRGBColorByPlane(decodedImageFrame, rgbaBuffer);
+ }
+ }
+
+ function convertColorSpace(canvas, dataSet, imageFrame) {
+ // extract the fields we need
+ var height = dataSet.uint16('x00280010');
+ var width = dataSet.uint16('x00280011');
+ var photometricInterpretation = dataSet.string('x00280004');
+
+ // setup the canvas context
+ canvas.height = height;
+ canvas.width = width;
+ var context = canvas.getContext('2d');
+ var imageData = context.createImageData(width, height);
+
+
+ // convert based on the photometric interpretation
+ var deferred = $.Deferred();
+ try {
+ if (photometricInterpretation === "RGB" )
+ {
+ convertRGB(dataSet, imageFrame, imageData.data);
+ }
+ else if (photometricInterpretation === "YBR_RCT")
+ {
+ convertRGB(dataSet, imageFrame, imageData.data);
+ }
+ else if (photometricInterpretation === "YBR_ICT")
+ {
+ convertRGB(dataSet, imageFrame, imageData.data);
+ }
+ else if( photometricInterpretation === "PALETTE COLOR" )
+ {
+ cornerstoneWADOImageLoader.convertPALETTECOLOR(imageFrame, imageData.data, dataSet );
+ }
+ else if( photometricInterpretation === "YBR_FULL_422" )
+ {
+ cornerstoneWADOImageLoader.convertYBRFull(imageFrame, imageData.data);
+ }
+ else if(photometricInterpretation === "YBR_FULL" )
+ {
+ cornerstoneWADOImageLoader.convertYBRFull(imageFrame, imageData.data);
+ }
+ else
+ {
+ throw "no color space conversion for photometric interpretation " + photometricInterpretation;
+ }
+ deferred.resolve(imageData);
+ return deferred;
+ } catch (error) {
+ deferred.reject(error);
+ return deferred;
+ }
+ }
+
+ // module exports
+ cornerstoneWADOImageLoader.convertColorSpace = convertColorSpace;
+
+}(cornerstoneWADOImageLoader));
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ function convertPALETTECOLOR( imageFrame, rgbaBuffer, dataSet ) {
+ var len=dataSet.int16('x00281101',0);
+ var start=dataSet.int16('x00281101',1);
+ var bits=dataSet.int16('x00281101',2);
+ var shift = (bits===8 ? 0 : 8 );
+
+ var buffer = dataSet.byteArray.buffer;
+ var rData=new Uint16Array( buffer, dataSet.elements.x00281201.dataOffset, len );
+ var gData=new Uint16Array( buffer, dataSet.elements.x00281202.dataOffset, len );
+ var bData=new Uint16Array( buffer, dataSet.elements.x00281203.dataOffset, len );
+
+ var numPixels = dataSet.uint16('x00280010') * dataSet.uint16('x00280011');
+ var palIndex=0;
+ var rgbaIndex=0;
+
+ for( var i=0 ; i < numPixels ; ++i ) {
+ var value=imageFrame[palIndex++];
+ if( value < start )
+ value=0;
+ else if( value > start + len -1 )
+ value=len-1;
+ else
+ value=value-start;
+
+ rgbaBuffer[ rgbaIndex++ ] = rData[value] >> shift;
+ rgbaBuffer[ rgbaIndex++ ] = gData[value] >> shift;
+ rgbaBuffer[ rgbaIndex++ ] = bData[value] >> shift;
+ rgbaBuffer[ rgbaIndex++ ] = 255;
+ }
+
+ }
+
+ // module exports
+ cornerstoneWADOImageLoader.convertPALETTECOLOR = convertPALETTECOLOR;
+
+}(cornerstoneWADOImageLoader));
+/**
+ */
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ function convertRGBColorByPixel(imageFrame, rgbaBuffer) {
+ if(imageFrame === undefined) {
+ throw "decodeRGB: rgbBuffer must not be undefined";
+ }
+ if(imageFrame.length % 3 !== 0) {
+ throw "decodeRGB: rgbBuffer length must be divisible by 3";
+ }
+
+ var numPixels = imageFrame.length / 3;
+ var rgbIndex = 0;
+ var rgbaIndex = 0;
+ for(var i= 0; i < numPixels; i++) {
+ rgbaBuffer[rgbaIndex++] = imageFrame[rgbIndex++]; // red
+ rgbaBuffer[rgbaIndex++] = imageFrame[rgbIndex++]; // green
+ rgbaBuffer[rgbaIndex++] = imageFrame[rgbIndex++]; // blue
+ rgbaBuffer[rgbaIndex++] = 255; //alpha
+ }
+ }
+
+ // module exports
+ cornerstoneWADOImageLoader.convertRGBColorByPixel = convertRGBColorByPixel;
+}(cornerstoneWADOImageLoader));
+/**
+ */
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ function convertRGBColorByPlane(imageFrame, rgbaBuffer) {
+ if(imageFrame === undefined) {
+ throw "decodeRGB: rgbBuffer must not be undefined";
+ }
+ if(imageFrame.length % 3 !== 0) {
+ throw "decodeRGB: rgbBuffer length must be divisible by 3";
+ }
+
+ var numPixels = imageFrame.length / 3;
+ var rgbaIndex = 0;
+ for(var i= 0; i < numPixels; i++) {
+ var rIndex = 0;
+ var gIndex = numPixels;
+ var bIndex = numPixels*2;
+ for(var i= 0; i < numPixels; i++) {
+ rgbaBuffer[rgbaIndex++] = imageFrame[rIndex++]; // red
+ rgbaBuffer[rgbaIndex++] = imageFrame[gIndex++]; // green
+ rgbaBuffer[rgbaIndex++] = imageFrame[bIndex++]; // blue
+ rgbaBuffer[rgbaIndex++] = 255; //alpha
+ }
+ }
+ }
+
+ // module exports
+ cornerstoneWADOImageLoader.convertRGBColorByPlane = convertRGBColorByPlane;
+}(cornerstoneWADOImageLoader));
+/**
+ */
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ function convertYBRFull(imageFrame, rgbaBuffer) {
+ if(imageFrame === undefined) {
+ throw "decodeRGB: ybrBuffer must not be undefined";
+ }
+ if(imageFrame.length % 3 !== 0) {
+ throw "decodeRGB: ybrBuffer length must be divisble by 3";
+ }
+
+ var numPixels = imageFrame.length / 3;
+ var ybrIndex = 0;
+ var rgbaIndex = 0;
+ for(var i= 0; i < numPixels; i++) {
+ var y = imageFrame[ybrIndex++];
+ var cb = imageFrame[ybrIndex++];
+ var cr = imageFrame[ybrIndex++];
+ rgbaBuffer[rgbaIndex++] = y + 1.40200 * (cr - 128);// red
+ rgbaBuffer[rgbaIndex++] = y - 0.34414 * (cb -128) - 0.71414 * (cr- 128); // green
+ rgbaBuffer[rgbaIndex++] = y + 1.77200 * (cb - 128); // blue
+ rgbaBuffer[rgbaIndex++] = 255; //alpha
+ }
+ }
+
+ // module exports
+ cornerstoneWADOImageLoader.convertYBRFull = convertYBRFull;
+}(cornerstoneWADOImageLoader));
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ function configure(options) {
+ cornerstoneWADOImageLoader.internal.options = options;
+ }
+
+ // module exports
+ cornerstoneWADOImageLoader.configure = configure;
+
+}(cornerstoneWADOImageLoader));
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ function createImageObject( dataSet, imageId, frame ) {
+ if(frame === undefined) {
+ frame = 0;
+ }
+
+ // make the image based on whether it is color or not
+ var photometricInterpretation = dataSet.string('x00280004');
+ var isColor = cornerstoneWADOImageLoader.isColorImage(photometricInterpretation);
+ if(isColor === false) {
+ return cornerstoneWADOImageLoader.makeGrayscaleImage(imageId, dataSet, frame);
+ } else {
+ return cornerstoneWADOImageLoader.makeColorImage(imageId, dataSet, frame);
+ }
+ }
+
+ // module exports
+ cornerstoneWADOImageLoader.createImageObject = createImageObject;
+
+}(cornerstoneWADOImageLoader));
+(function ($, cornerstone, cornerstoneWADOImageLoader) {
+
+ "use strict";
+ function decodeJPEG2000(dataSet, frame)
+ {
+ var height = dataSet.uint16('x00280010');
+ var width = dataSet.uint16('x00280011');
+ var compressedPixelData = dicomParser.readEncapsulatedPixelData(dataSet, dataSet.elements.x7fe00010, frame);
+ var jpxImage = new JpxImage();
+ jpxImage.parse(compressedPixelData);
+
+ var j2kWidth = jpxImage.width;
+ var j2kHeight = jpxImage.height;
+ if(j2kWidth !== width) {
+ throw 'JPEG2000 decoder returned width of ' + j2kWidth + ', when ' + width + ' is expected';
+ }
+ if(j2kHeight !== height) {
+ throw 'JPEG2000 decoder returned width of ' + j2kHeight + ', when ' + height + ' is expected';
+ }
+ var tileCount = jpxImage.tiles.length;
+ if(tileCount !== 1) {
+ throw 'JPEG2000 decoder returned a tileCount of ' + tileCount + ', when 1 is expected';
+ }
+ var tileComponents = jpxImage.tiles[0];
+ var pixelData = tileComponents.items;
+ return pixelData;
+ }
+
+ cornerstoneWADOImageLoader.decodeJPEG2000 = decodeJPEG2000;
+}($, cornerstone, cornerstoneWADOImageLoader));
+(function ($, cornerstone, cornerstoneWADOImageLoader) {
+
+ "use strict";
+ function decodeJPEGBaseline(dataSet, frame)
+ {
+ var pixelDataElement = dataSet.elements.x7fe00010;
+ var height = dataSet.uint16('x00280010');
+ var width = dataSet.uint16('x00280011');
+ var bitsAllocated = dataSet.uint16('x00280100');
+ var frameData = dicomParser.readEncapsulatedPixelData(dataSet, pixelDataElement, frame);
+ var jpeg = new JpegImage();
+ jpeg.parse( frameData );
+ if(bitsAllocated === 8) {
+ return jpeg.getData(width, height);
+ }
+ else if(bitsAllocated === 16) {
+ return jpeg.getData16(width, height);
+ }
+ }
+
+ cornerstoneWADOImageLoader.decodeJPEGBaseline = decodeJPEGBaseline;
+}($, cornerstone, cornerstoneWADOImageLoader));
+/**
+ * Special decoder for 8 bit jpeg that leverages the browser's built in JPEG decoder for increased performance
+ */
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ function arrayBufferToString(buffer) {
+ return binaryToString(String.fromCharCode.apply(null, Array.prototype.slice.apply(new Uint8Array(buffer))));
+ }
+
+ function binaryToString(binary) {
+ var error;
+
+ try {
+ return decodeURIComponent(escape(binary));
+ } catch (_error) {
+ error = _error;
+ if (error instanceof URIError) {
+ return binary;
+ } else {
+ throw error;
+ }
+ }
+ }
+
+ function decodeJPEGBaseline8Bit(canvas, dataSet, frame) {
+ var deferred = $.Deferred();
+
+ var height = dataSet.uint16('x00280010');
+ var width = dataSet.uint16('x00280011');
+ // resize the canvas
+ canvas.height = height;
+ canvas.width = width;
+
+ var encodedPixelData = dicomParser.readEncapsulatedPixelData(dataSet, dataSet.elements.x7fe00010, frame);
+
+ var imgBlob = new Blob([encodedPixelData], {type: "image/jpeg"});
+
+ var r = new FileReader();
+ if(r.readAsBinaryString === undefined) {
+ r.readAsArrayBuffer(imgBlob);
+ }
+ else {
+ r.readAsBinaryString(imgBlob); // doesn't work on IE11
+ }
+
+ r.onload = function(){
+ var img=new Image();
+ img.onload = function() {
+ var context = canvas.getContext('2d');
+ context.drawImage(this, 0, 0);
+ var imageData = context.getImageData(0, 0, width, height);
+ deferred.resolve(imageData);
+ };
+ img.onerror = function(error) {
+ deferred.reject(error);
+ };
+ if(r.readAsBinaryString === undefined) {
+ img.src = "data:image/jpeg;base64,"+window.btoa(arrayBufferToString(r.result));
+ }
+ else {
+ img.src = "data:image/jpeg;base64,"+window.btoa(r.result); // doesn't work on IE11
+ }
+
+ };
+ return deferred;
+ }
+
+ function isJPEGBaseline8Bit(dataSet) {
+ var transferSyntax = dataSet.string('x00020010');
+ var bitsAllocated = dataSet.uint16('x00280100');
+
+ if((bitsAllocated === 8) &&
+ transferSyntax === "1.2.840.10008.1.2.4.50")
+ {
+ return true;
+ }
+
+ }
+
+ // module exports
+ cornerstoneWADOImageLoader.decodeJPEGBaseline8Bit = decodeJPEGBaseline8Bit;
+ cornerstoneWADOImageLoader.isJPEGBaseline8Bit = isJPEGBaseline8Bit;
+
+}(cornerstoneWADOImageLoader));
+"use strict";
+(function (cornerstoneWADOImageLoader) {
+
+ function decodeJPEGLossless(dataSet, frame) {
+ var pixelDataElement = dataSet.elements.x7fe00010;
+ var bitsAllocated = dataSet.uint16('x00280100');
+ var pixelRepresentation = dataSet.uint16('x00280103');
+ var frameData = dicomParser.readEncapsulatedPixelData(dataSet, pixelDataElement, frame);
+ var byteOutput = bitsAllocated <= 8 ? 1 : 2;
+ //console.time('jpeglossless');
+ var decoder = new jpeg.lossless.Decoder();
+ var decompressedData = decoder.decode(frameData.buffer, frameData.byteOffset, frameData.length, byteOutput);
+ //console.timeEnd('jpeglossless');
+ if (pixelRepresentation === 0) {
+ if (byteOutput === 2) {
+ return new Uint16Array(decompressedData.buffer);
+ } else {
+ // untested!
+ return new Uint8Array(decompressedData.buffer);
+ }
+ } else {
+ return new Int16Array(decompressedData.buffer);
+ }
+ }
+ // module exports
+ cornerstoneWADOImageLoader.decodeJPEGLossless = decodeJPEGLossless;
+
+}(cornerstoneWADOImageLoader));
+/**
+ */
+(function (cornerstoneWADOImageLoader) {
+
+ function decodeRLE(dataSet, frame) {
+ var height = dataSet.uint16('x00280010');
+ var width = dataSet.uint16('x00280011');
+ var samplesPerPixel = dataSet.uint16('x00280002');
+ var pixelDataElement = dataSet.elements.x7fe00010;
+
+ var frameData = dicomParser.readEncapsulatedPixelData(dataSet, pixelDataElement, frame);
+ var pixelFormat = cornerstoneWADOImageLoader.getPixelFormat(dataSet);
+
+
+ var frameSize = width*height;
+ var buffer;
+ if( pixelFormat===1 ) {
+ buffer = new ArrayBuffer(frameSize*samplesPerPixel);
+ decode8( frameData, buffer, frameSize, samplesPerPixel);
+ return new Uint8Array(buffer);
+ } else if( pixelFormat===2 ) {
+ buffer = new ArrayBuffer(frameSize*samplesPerPixel*2);
+ decode16( frameData, buffer, frameSize );
+ return new Uint16Array(buffer);
+ } else if( pixelFormat===3 ) {
+ buffer = new ArrayBuffer(frameSize*samplesPerPixel*2);
+ decode16( frameData, buffer, frameSize );
+ return new Int16Array(buffer);
+ }
+ }
+
+ function decode8( frameData, outFrame, frameSize, samplesSize ) {
+ var header=new DataView(frameData.buffer, frameData.byteOffset);
+ var data=new DataView( frameData.buffer, frameData.byteOffset );
+ var out=new DataView( outFrame );
+
+ var outIndex=0;
+ var numSegments = header.getInt32(0,true);
+ for( var s=0 ; s < numSegments ; ++s ) {
+ outIndex = s;
+
+ var inIndex=header.getInt32( (s+1)*4,true);
+ var maxIndex=header.getInt32( (s+2)*4,true);
+ if( maxIndex===0 )
+ maxIndex = frameData.length;
+
+ var endOfSegment = frameSize * numSegments;
+
+ while( inIndex < maxIndex ) {
+ var n=data.getInt8(inIndex++);
+ if( n >=0 && n <=127 ) {
+ // copy n bytes
+ for( var i=0 ; i < n+1 && outIndex < endOfSegment; ++i ) {
+ out.setInt8(outIndex, data.getInt8(inIndex++));
+ outIndex+=samplesSize;
+ }
+ } else if( n<= -1 && n>=-127 ) {
+ var value=data.getInt8(inIndex++);
+ // run of n bytes
+ for( var j=0 ; j < -n+1 && outIndex < endOfSegment; ++j ) {
+ out.setInt8(outIndex, value );
+ outIndex+=samplesSize;
+ }
+ } else if (n===-128)
+ ; // do nothing
+ }
+ }
+ }
+
+ function decode16( frameData, outFrame, frameSize ) {
+ var header=new DataView(frameData.buffer, frameData.byteOffset);
+ var data=new DataView( frameData.buffer, frameData.byteOffset );
+ var out=new DataView( outFrame );
+
+ var numSegments = header.getInt32(0,true);
+ for( var s=0 ; s < numSegments ; ++s ) {
+ var outIndex=0;
+ var highByte=( s===0 ? 1 : 0);
+
+ var inIndex=header.getInt32( (s+1)*4,true);
+ var maxIndex=header.getInt32( (s+2)*4,true);
+ if( maxIndex===0 )
+ maxIndex = frameData.length;
+
+ while( inIndex < maxIndex ) {
+ var n=data.getInt8(inIndex++);
+ if( n >=0 && n <=127 ) {
+ for( var i=0 ; i < n+1 && outIndex < frameSize ; ++i ) {
+ out.setInt8( (outIndex*2)+highByte, data.getInt8(inIndex++) );
+ outIndex++;
+ }
+ } else if( n<= -1 && n>=-127 ) {
+ var value=data.getInt8(inIndex++);
+ for( var j=0 ; j < -n+1 && outIndex < frameSize ; ++j ) {
+ out.setInt8( (outIndex*2)+highByte, value );
+ outIndex++;
+ }
+ } else if (n===-128)
+ ; // do nothing
+ }
+ }
+ }
+
+ // module exports
+ cornerstoneWADOImageLoader.decodeRLE = decodeRLE;
+
+}(cornerstoneWADOImageLoader));
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ function decodeTransferSyntax(dataSet, frame) {
+ var transferSyntax = dataSet.string('x00020010');
+
+ // Implicit VR Little Endian
+ if( transferSyntax === "1.2.840.10008.1.2") {
+ return cornerstoneWADOImageLoader.extractUncompressedPixels(dataSet, frame);
+ }
+ // Explicit VR Little Endian
+ else if( transferSyntax === "1.2.840.10008.1.2.1") {
+ return cornerstoneWADOImageLoader.extractUncompressedPixels(dataSet, frame);
+ }
+ // JPEG 2000 Lossless
+ else if(transferSyntax === "1.2.840.10008.1.2.4.90")
+ {
+ return cornerstoneWADOImageLoader.decodeJPEG2000(dataSet, frame);
+ }
+ // JPEG 2000 Lossy
+ else if(transferSyntax === "1.2.840.10008.1.2.4.91")
+ {
+ return cornerstoneWADOImageLoader.decodeJPEG2000(dataSet, frame);
+ }
+ /* Don't know if these work...
+ // JPEG 2000 Part 2 Multicomponent Image Compression (Lossless Only)
+ else if(transferSyntax === "1.2.840.10008.1.2.4.92")
+ {
+ return cornerstoneWADOImageLoader.decodeJPEG2000(dataSet, frame);
+ }
+ // JPEG 2000 Part 2 Multicomponent Image Compression
+ else if(transferSyntax === "1.2.840.10008.1.2.4.93")
+ {
+ return cornerstoneWADOImageLoader.decodeJPEG2000(dataSet, frame);
+ }
+ */
+ // RLE Lossless
+ else if ( transferSyntax === "1.2.840.10008.1.2.5" )
+ {
+ return cornerstoneWADOImageLoader.decodeRLE( dataSet, frame);
+ }
+ // JPEG Baseline lossy process 1 (8 bit)
+ else if ( transferSyntax === "1.2.840.10008.1.2.4.50" )
+ {
+ return cornerstoneWADOImageLoader.decodeJPEGBaseline(dataSet, frame);
+ }
+ // JPEG Baseline lossy process 2 & 4 (12 bit)
+ else if ( transferSyntax === "1.2.840.10008.1.2.4.51" )
+ {
+ return cornerstoneWADOImageLoader.decodeJPEGBaseline(dataSet, frame);
+ }
+ // JPEG Lossless, Nonhierarchical (Processes 14)
+ else if ( transferSyntax === "1.2.840.10008.1.2.4.57" )
+ {
+ return cornerstoneWADOImageLoader.decodeJPEGLossless(dataSet, frame);
+ }
+ // JPEG Lossless, Nonhierarchical (Processes 14 [Selection 1])
+ else if ( transferSyntax === "1.2.840.10008.1.2.4.70" )
+ {
+ return cornerstoneWADOImageLoader.decodeJPEGLossless(dataSet, frame);
+ }
+ else
+ {
+ throw "no decoder for transfer syntax " + transferSyntax;
+ }
+ }
+
+ // module exports
+ cornerstoneWADOImageLoader.decodeTransferSyntax = decodeTransferSyntax;
+
+}(cornerstoneWADOImageLoader));
+// jshint ignore: start
+
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/*
+ Copyright 2011 notmasteryet
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+// - The JPEG specification can be found in the ITU CCITT Recommendation T.81
+// (www.w3.org/Graphics/JPEG/itu-t81.pdf)
+// - The JFIF specification can be found in the JPEG File Interchange Format
+// (www.w3.org/Graphics/JPEG/jfif3.pdf)
+// - The Adobe Application-Specific JPEG markers in the Supporting the DCT Filters
+// in PostScript Level 2, Technical Note #5116
+// (partners.adobe.com/public/developer/en/ps/sdk/5116.DCT_Filter.pdf)
+
+var ColorSpace = {Unkown: 0, Grayscale: 1, AdobeRGB: 2, RGB: 3, CYMK: 4};
+var JpegImage = (function jpegImage() {
+ "use strict";
+ var dctZigZag = new Int32Array([
+ 0,
+ 1, 8,
+ 16, 9, 2,
+ 3, 10, 17, 24,
+ 32, 25, 18, 11, 4,
+ 5, 12, 19, 26, 33, 40,
+ 48, 41, 34, 27, 20, 13, 6,
+ 7, 14, 21, 28, 35, 42, 49, 56,
+ 57, 50, 43, 36, 29, 22, 15,
+ 23, 30, 37, 44, 51, 58,
+ 59, 52, 45, 38, 31,
+ 39, 46, 53, 60,
+ 61, 54, 47,
+ 55, 62,
+ 63
+ ]);
+
+ var dctCos1 = 4017; // cos(pi/16)
+ var dctSin1 = 799; // sin(pi/16)
+ var dctCos3 = 3406; // cos(3*pi/16)
+ var dctSin3 = 2276; // sin(3*pi/16)
+ var dctCos6 = 1567; // cos(6*pi/16)
+ var dctSin6 = 3784; // sin(6*pi/16)
+ var dctSqrt2 = 5793; // sqrt(2)
+ var dctSqrt1d2 = 2896; // sqrt(2) / 2
+
+ function constructor() {
+ }
+
+ function buildHuffmanTable(codeLengths, values) {
+ var k = 0, code = [], i, j, length = 16;
+ while (length > 0 && !codeLengths[length - 1])
+ length--;
+ code.push({children: [], index: 0});
+ var p = code[0], q;
+ for (i = 0; i < length; i++) {
+ for (j = 0; j < codeLengths[i]; j++) {
+ p = code.pop();
+ p.children[p.index] = values[k];
+ while (p.index > 0) {
+ p = code.pop();
+ }
+ p.index++;
+ code.push(p);
+ while (code.length <= i) {
+ code.push(q = {children: [], index: 0});
+ p.children[p.index] = q.children;
+ p = q;
+ }
+ k++;
+ }
+ if (i + 1 < length) {
+ // p here points to last code
+ code.push(q = {children: [], index: 0});
+ p.children[p.index] = q.children;
+ p = q;
+ }
+ }
+ return code[0].children;
+ }
+
+ function getBlockBufferOffset(component, row, col) {
+ return 64 * ((component.blocksPerLine + 1) * row + col);
+ }
+
+ function decodeScan(data, offset,
+ frame, components, resetInterval,
+ spectralStart, spectralEnd,
+ successivePrev, successive) {
+ var precision = frame.precision;
+ var samplesPerLine = frame.samplesPerLine;
+ var scanLines = frame.scanLines;
+ var mcusPerLine = frame.mcusPerLine;
+ var progressive = frame.progressive;
+ var maxH = frame.maxH, maxV = frame.maxV;
+
+ var startOffset = offset, bitsData = 0, bitsCount = 0;
+
+ function readBit() {
+ if (bitsCount > 0) {
+ bitsCount--;
+ return (bitsData >> bitsCount) & 1;
+ }
+ bitsData = data[offset++];
+ if (bitsData == 0xFF) {
+ var nextByte = data[offset++];
+ if (nextByte) {
+ throw "unexpected marker: " + ((bitsData << 8) | nextByte).toString(16);
+ }
+ // unstuff 0
+ }
+ bitsCount = 7;
+ return bitsData >>> 7;
+ }
+
+ function decodeHuffman(tree) {
+ var node = tree;
+ var bit;
+ while ((bit = readBit()) !== null) {
+ node = node[bit];
+ if (typeof node === 'number')
+ return node;
+ if (typeof node !== 'object')
+ throw "invalid huffman sequence";
+ }
+ return null;
+ }
+
+ function receive(length) {
+ var n = 0;
+ while (length > 0) {
+ var bit = readBit();
+ if (bit === null)
+ return;
+ n = (n << 1) | bit;
+ length--;
+ }
+ return n;
+ }
+
+ function receiveAndExtend(length) {
+ var n = receive(length);
+ if (n >= 1 << (length - 1))
+ return n;
+ return n + (-1 << length) + 1;
+ }
+
+ function decodeBaseline(component, offset) {
+ var t = decodeHuffman(component.huffmanTableDC);
+ var diff = t === 0 ? 0 : receiveAndExtend(t);
+ component.blockData[offset] = (component.pred += diff);
+ var k = 1;
+ while (k < 64) {
+ var rs = decodeHuffman(component.huffmanTableAC);
+ var s = rs & 15, r = rs >> 4;
+ if (s === 0) {
+ if (r < 15)
+ break;
+ k += 16;
+ continue;
+ }
+ k += r;
+ var z = dctZigZag[k];
+ component.blockData[offset + z] = receiveAndExtend(s);
+ k++;
+ }
+ }
+
+ function decodeDCFirst(component, offset) {
+ var t = decodeHuffman(component.huffmanTableDC);
+ var diff = t === 0 ? 0 : (receiveAndExtend(t) << successive);
+ component.blockData[offset] = (component.pred += diff);
+ }
+
+ function decodeDCSuccessive(component, offset) {
+ component.blockData[offset] |= readBit() << successive;
+ }
+
+ var eobrun = 0;
+ function decodeACFirst(component, offset) {
+ if (eobrun > 0) {
+ eobrun--;
+ return;
+ }
+ var k = spectralStart, e = spectralEnd;
+ while (k <= e) {
+ var rs = decodeHuffman(component.huffmanTableAC);
+ var s = rs & 15, r = rs >> 4;
+ if (s === 0) {
+ if (r < 15) {
+ eobrun = receive(r) + (1 << r) - 1;
+ break;
+ }
+ k += 16;
+ continue;
+ }
+ k += r;
+ var z = dctZigZag[k];
+ component.blockData[offset + z] = receiveAndExtend(s) * (1 << successive);
+ k++;
+ }
+ }
+
+ var successiveACState = 0, successiveACNextValue;
+ function decodeACSuccessive(component, offset) {
+ var k = spectralStart, e = spectralEnd, r = 0;
+ while (k <= e) {
+ var z = dctZigZag[k];
+ switch (successiveACState) {
+ case 0: // initial state
+ var rs = decodeHuffman(component.huffmanTableAC);
+ var s = rs & 15;
+ r = rs >> 4;
+ if (s === 0) {
+ if (r < 15) {
+ eobrun = receive(r) + (1 << r);
+ successiveACState = 4;
+ } else {
+ r = 16;
+ successiveACState = 1;
+ }
+ } else {
+ if (s !== 1)
+ throw "invalid ACn encoding";
+ successiveACNextValue = receiveAndExtend(s);
+ successiveACState = r ? 2 : 3;
+ }
+ continue;
+ case 1: // skipping r zero items
+ case 2:
+ if (component.blockData[offset + z]) {
+ component.blockData[offset + z] += (readBit() << successive);
+ } else {
+ r--;
+ if (r === 0)
+ successiveACState = successiveACState == 2 ? 3 : 0;
+ }
+ break;
+ case 3: // set value for a zero item
+ if (component.blockData[offset + z]) {
+ component.blockData[offset + z] += (readBit() << successive);
+ } else {
+ component.blockData[offset + z] = successiveACNextValue << successive;
+ successiveACState = 0;
+ }
+ break;
+ case 4: // eob
+ if (component.blockData[offset + z]) {
+ component.blockData[offset + z] += (readBit() << successive);
+ }
+ break;
+ }
+ k++;
+ }
+ if (successiveACState === 4) {
+ eobrun--;
+ if (eobrun === 0)
+ successiveACState = 0;
+ }
+ }
+
+ function decodeMcu(component, decode, mcu, row, col) {
+ var mcuRow = (mcu / mcusPerLine) | 0;
+ var mcuCol = mcu % mcusPerLine;
+ var blockRow = mcuRow * component.v + row;
+ var blockCol = mcuCol * component.h + col;
+ var offset = getBlockBufferOffset(component, blockRow, blockCol);
+ decode(component, offset);
+ }
+
+ function decodeBlock(component, decode, mcu) {
+ var blockRow = (mcu / component.blocksPerLine) | 0;
+ var blockCol = mcu % component.blocksPerLine;
+ var offset = getBlockBufferOffset(component, blockRow, blockCol);
+ decode(component, offset);
+ }
+
+ var componentsLength = components.length;
+ var component, i, j, k, n;
+ var decodeFn;
+ if (progressive) {
+ if (spectralStart === 0)
+ decodeFn = successivePrev === 0 ? decodeDCFirst : decodeDCSuccessive;
+ else
+ decodeFn = successivePrev === 0 ? decodeACFirst : decodeACSuccessive;
+ } else {
+ decodeFn = decodeBaseline;
+ }
+
+ var mcu = 0, marker;
+ var mcuExpected;
+ if (componentsLength == 1) {
+ mcuExpected = components[0].blocksPerLine * components[0].blocksPerColumn;
+ } else {
+ mcuExpected = mcusPerLine * frame.mcusPerColumn;
+ }
+ if (!resetInterval) {
+ resetInterval = mcuExpected;
+ }
+
+ var h, v;
+ while (mcu < mcuExpected) {
+ // reset interval stuff
+ for (i = 0; i < componentsLength; i++) {
+ components[i].pred = 0;
+ }
+ eobrun = 0;
+
+ if (componentsLength == 1) {
+ component = components[0];
+ for (n = 0; n < resetInterval; n++) {
+ decodeBlock(component, decodeFn, mcu);
+ mcu++;
+ }
+ } else {
+ for (n = 0; n < resetInterval; n++) {
+ for (i = 0; i < componentsLength; i++) {
+ component = components[i];
+ h = component.h;
+ v = component.v;
+ for (j = 0; j < v; j++) {
+ for (k = 0; k < h; k++) {
+ decodeMcu(component, decodeFn, mcu, j, k);
+ }
+ }
+ }
+ mcu++;
+ }
+ }
+
+ // find marker
+ bitsCount = 0;
+ marker = (data[offset] << 8) | data[offset + 1];
+ if (marker <= 0xFF00) {
+ throw "marker was not found";
+ }
+
+ if (marker >= 0xFFD0 && marker <= 0xFFD7) { // RSTx
+ offset += 2;
+ } else {
+ break;
+ }
+ }
+
+ return offset - startOffset;
+ }
+
+ // A port of poppler's IDCT method which in turn is taken from:
+ // Christoph Loeffler, Adriaan Ligtenberg, George S. Moschytz,
+ // "Practical Fast 1-D DCT Algorithms with 11 Multiplications",
+ // IEEE Intl. Conf. on Acoustics, Speech & Signal Processing, 1989,
+ // 988-991.
+ function quantizeAndInverse(component, blockBufferOffset, p) {
+ var qt = component.quantizationTable;
+ var v0, v1, v2, v3, v4, v5, v6, v7, t;
+ var i;
+
+ // dequant
+ for (i = 0; i < 64; i++) {
+ p[i] = component.blockData[blockBufferOffset + i] * qt[i];
+ }
+
+ // inverse DCT on rows
+ for (i = 0; i < 8; ++i) {
+ var row = 8 * i;
+
+ // check for all-zero AC coefficients
+ if (p[1 + row] === 0 && p[2 + row] === 0 && p[3 + row] === 0 &&
+ p[4 + row] === 0 && p[5 + row] === 0 && p[6 + row] === 0 &&
+ p[7 + row] === 0) {
+ t = (dctSqrt2 * p[0 + row] + 512) >> 10;
+ p[0 + row] = t;
+ p[1 + row] = t;
+ p[2 + row] = t;
+ p[3 + row] = t;
+ p[4 + row] = t;
+ p[5 + row] = t;
+ p[6 + row] = t;
+ p[7 + row] = t;
+ continue;
+ }
+
+ // stage 4
+ v0 = (dctSqrt2 * p[0 + row] + 128) >> 8;
+ v1 = (dctSqrt2 * p[4 + row] + 128) >> 8;
+ v2 = p[2 + row];
+ v3 = p[6 + row];
+ v4 = (dctSqrt1d2 * (p[1 + row] - p[7 + row]) + 128) >> 8;
+ v7 = (dctSqrt1d2 * (p[1 + row] + p[7 + row]) + 128) >> 8;
+ v5 = p[3 + row] << 4;
+ v6 = p[5 + row] << 4;
+
+ // stage 3
+ t = (v0 - v1 + 1) >> 1;
+ v0 = (v0 + v1 + 1) >> 1;
+ v1 = t;
+ t = (v2 * dctSin6 + v3 * dctCos6 + 128) >> 8;
+ v2 = (v2 * dctCos6 - v3 * dctSin6 + 128) >> 8;
+ v3 = t;
+ t = (v4 - v6 + 1) >> 1;
+ v4 = (v4 + v6 + 1) >> 1;
+ v6 = t;
+ t = (v7 + v5 + 1) >> 1;
+ v5 = (v7 - v5 + 1) >> 1;
+ v7 = t;
+
+ // stage 2
+ t = (v0 - v3 + 1) >> 1;
+ v0 = (v0 + v3 + 1) >> 1;
+ v3 = t;
+ t = (v1 - v2 + 1) >> 1;
+ v1 = (v1 + v2 + 1) >> 1;
+ v2 = t;
+ t = (v4 * dctSin3 + v7 * dctCos3 + 2048) >> 12;
+ v4 = (v4 * dctCos3 - v7 * dctSin3 + 2048) >> 12;
+ v7 = t;
+ t = (v5 * dctSin1 + v6 * dctCos1 + 2048) >> 12;
+ v5 = (v5 * dctCos1 - v6 * dctSin1 + 2048) >> 12;
+ v6 = t;
+
+ // stage 1
+ p[0 + row] = v0 + v7;
+ p[7 + row] = v0 - v7;
+ p[1 + row] = v1 + v6;
+ p[6 + row] = v1 - v6;
+ p[2 + row] = v2 + v5;
+ p[5 + row] = v2 - v5;
+ p[3 + row] = v3 + v4;
+ p[4 + row] = v3 - v4;
+ }
+
+ // inverse DCT on columns
+ for (i = 0; i < 8; ++i) {
+ var col = i;
+
+ // check for all-zero AC coefficients
+ if (p[1 * 8 + col] === 0 && p[2 * 8 + col] === 0 && p[3 * 8 + col] === 0 &&
+ p[4 * 8 + col] === 0 && p[5 * 8 + col] === 0 && p[6 * 8 + col] === 0 &&
+ p[7 * 8 + col] === 0) {
+ t = (dctSqrt2 * p[i + 0] + 8192) >> 14;
+ p[0 * 8 + col] = t;
+ p[1 * 8 + col] = t;
+ p[2 * 8 + col] = t;
+ p[3 * 8 + col] = t;
+ p[4 * 8 + col] = t;
+ p[5 * 8 + col] = t;
+ p[6 * 8 + col] = t;
+ p[7 * 8 + col] = t;
+ continue;
+ }
+
+ // stage 4
+ v0 = (dctSqrt2 * p[0 * 8 + col] + 2048) >> 12;
+ v1 = (dctSqrt2 * p[4 * 8 + col] + 2048) >> 12;
+ v2 = p[2 * 8 + col];
+ v3 = p[6 * 8 + col];
+ v4 = (dctSqrt1d2 * (p[1 * 8 + col] - p[7 * 8 + col]) + 2048) >> 12;
+ v7 = (dctSqrt1d2 * (p[1 * 8 + col] + p[7 * 8 + col]) + 2048) >> 12;
+ v5 = p[3 * 8 + col];
+ v6 = p[5 * 8 + col];
+
+ // stage 3
+ t = (v0 - v1 + 1) >> 1;
+ v0 = (v0 + v1 + 1) >> 1;
+ v1 = t;
+ t = (v2 * dctSin6 + v3 * dctCos6 + 2048) >> 12;
+ v2 = (v2 * dctCos6 - v3 * dctSin6 + 2048) >> 12;
+ v3 = t;
+ t = (v4 - v6 + 1) >> 1;
+ v4 = (v4 + v6 + 1) >> 1;
+ v6 = t;
+ t = (v7 + v5 + 1) >> 1;
+ v5 = (v7 - v5 + 1) >> 1;
+ v7 = t;
+
+ // stage 2
+ t = (v0 - v3 + 1) >> 1;
+ v0 = (v0 + v3 + 1) >> 1;
+ v3 = t;
+ t = (v1 - v2 + 1) >> 1;
+ v1 = (v1 + v2 + 1) >> 1;
+ v2 = t;
+ t = (v4 * dctSin3 + v7 * dctCos3 + 2048) >> 12;
+ v4 = (v4 * dctCos3 - v7 * dctSin3 + 2048) >> 12;
+ v7 = t;
+ t = (v5 * dctSin1 + v6 * dctCos1 + 2048) >> 12;
+ v5 = (v5 * dctCos1 - v6 * dctSin1 + 2048) >> 12;
+ v6 = t;
+
+ // stage 1
+ p[0 * 8 + col] = v0 + v7;
+ p[7 * 8 + col] = v0 - v7;
+ p[1 * 8 + col] = v1 + v6;
+ p[6 * 8 + col] = v1 - v6;
+ p[2 * 8 + col] = v2 + v5;
+ p[5 * 8 + col] = v2 - v5;
+ p[3 * 8 + col] = v3 + v4;
+ p[4 * 8 + col] = v3 - v4;
+ }
+
+ // convert to 8-bit integers
+ for (i = 0; i < 64; ++i) {
+ var index = blockBufferOffset + i;
+ var q = p[i];
+ q = (q <= -2056 / component.bitConversion) ? 0 :
+ (q >= 2024 / component.bitConversion) ? 255 / component.bitConversion :
+ (q + 2056 / component.bitConversion) >> 4;
+ component.blockData[index] = q;
+ }
+ }
+
+ function buildComponentData(frame, component) {
+ var lines = [];
+ var blocksPerLine = component.blocksPerLine;
+ var blocksPerColumn = component.blocksPerColumn;
+ var samplesPerLine = blocksPerLine << 3;
+ var computationBuffer = new Int32Array(64);
+
+ var i, j, ll = 0;
+ for (var blockRow = 0; blockRow < blocksPerColumn; blockRow++) {
+ for (var blockCol = 0; blockCol < blocksPerLine; blockCol++) {
+ var offset = getBlockBufferOffset(component, blockRow, blockCol);
+ quantizeAndInverse(component, offset, computationBuffer);
+ }
+ }
+ return component.blockData;
+ }
+
+ function clampToUint8(a) {
+ return a <= 0 ? 0 : a >= 255 ? 255 : a | 0;
+ }
+
+ constructor.prototype = {
+ load: function load(path) {
+ var handleData = (function (data) {
+ this.parse(data);
+ if (this.onload)
+ this.onload();
+ }).bind(this);
+
+ if (path.indexOf("data:") > -1) {
+ var offset = path.indexOf("base64,") + 7;
+ var data = atob(path.substring(offset));
+ var arr = new Uint8Array(data.length);
+ for (var i = data.length - 1; i >= 0; i--) {
+ arr[i] = data.charCodeAt(i);
+ }
+ handleData(data);
+ } else {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", path, true);
+ xhr.responseType = "arraybuffer";
+ xhr.onload = (function () {
+ // TODO catch parse error
+ var data = new Uint8Array(xhr.response);
+ handleData(data);
+ }).bind(this);
+ xhr.send(null);
+ }
+ },
+ parse: function parse(data) {
+
+ function readUint16() {
+ var value = (data[offset] << 8) | data[offset + 1];
+ offset += 2;
+ return value;
+ }
+
+ function readDataBlock() {
+ var length = readUint16();
+ var array = data.subarray(offset, offset + length - 2);
+ offset += array.length;
+ return array;
+ }
+
+ function prepareComponents(frame) {
+ var mcusPerLine = Math.ceil(frame.samplesPerLine / 8 / frame.maxH);
+ var mcusPerColumn = Math.ceil(frame.scanLines / 8 / frame.maxV);
+ for (var i = 0; i < frame.components.length; i++) {
+ component = frame.components[i];
+ var blocksPerLine = Math.ceil(Math.ceil(frame.samplesPerLine / 8) * component.h / frame.maxH);
+ var blocksPerColumn = Math.ceil(Math.ceil(frame.scanLines / 8) * component.v / frame.maxV);
+ var blocksPerLineForMcu = mcusPerLine * component.h;
+ var blocksPerColumnForMcu = mcusPerColumn * component.v;
+
+ var blocksBufferSize = 64 * blocksPerColumnForMcu * (blocksPerLineForMcu + 1);
+ component.blockData = new Int16Array(blocksBufferSize);
+ component.blocksPerLine = blocksPerLine;
+ component.blocksPerColumn = blocksPerColumn;
+ }
+ frame.mcusPerLine = mcusPerLine;
+ frame.mcusPerColumn = mcusPerColumn;
+ }
+
+ var offset = 0, length = data.length;
+ var jfif = null;
+ var adobe = null;
+ var pixels = null;
+ var frame, resetInterval;
+ var quantizationTables = [];
+ var huffmanTablesAC = [], huffmanTablesDC = [];
+ var fileMarker = readUint16();
+ if (fileMarker != 0xFFD8) { // SOI (Start of Image)
+ throw "SOI not found";
+ }
+
+ fileMarker = readUint16();
+ while (fileMarker != 0xFFD9) { // EOI (End of image)
+ var i, j, l;
+ switch (fileMarker) {
+ case 0xFFE0: // APP0 (Application Specific)
+ case 0xFFE1: // APP1
+ case 0xFFE2: // APP2
+ case 0xFFE3: // APP3
+ case 0xFFE4: // APP4
+ case 0xFFE5: // APP5
+ case 0xFFE6: // APP6
+ case 0xFFE7: // APP7
+ case 0xFFE8: // APP8
+ case 0xFFE9: // APP9
+ case 0xFFEA: // APP10
+ case 0xFFEB: // APP11
+ case 0xFFEC: // APP12
+ case 0xFFED: // APP13
+ case 0xFFEE: // APP14
+ case 0xFFEF: // APP15
+ case 0xFFFE: // COM (Comment)
+ var appData = readDataBlock();
+
+ if (fileMarker === 0xFFE0) {
+ if (appData[0] === 0x4A && appData[1] === 0x46 && appData[2] === 0x49 &&
+ appData[3] === 0x46 && appData[4] === 0) { // 'JFIF\x00'
+ jfif = {
+ version: {major: appData[5], minor: appData[6]},
+ densityUnits: appData[7],
+ xDensity: (appData[8] << 8) | appData[9],
+ yDensity: (appData[10] << 8) | appData[11],
+ thumbWidth: appData[12],
+ thumbHeight: appData[13],
+ thumbData: appData.subarray(14, 14 + 3 * appData[12] * appData[13])
+ };
+ }
+ }
+ // TODO APP1 - Exif
+ if (fileMarker === 0xFFEE) {
+ if (appData[0] === 0x41 && appData[1] === 0x64 && appData[2] === 0x6F &&
+ appData[3] === 0x62 && appData[4] === 0x65 && appData[5] === 0) { // 'Adobe\x00'
+ adobe = {
+ version: appData[6],
+ flags0: (appData[7] << 8) | appData[8],
+ flags1: (appData[9] << 8) | appData[10],
+ transformCode: appData[11]
+ };
+ }
+ }
+ break;
+
+ case 0xFFDB: // DQT (Define Quantization Tables)
+ var quantizationTablesLength = readUint16();
+ var quantizationTablesEnd = quantizationTablesLength + offset - 2;
+ while (offset < quantizationTablesEnd) {
+ var quantizationTableSpec = data[offset++];
+ var tableData = new Int32Array(64);
+ if ((quantizationTableSpec >> 4) === 0) { // 8 bit values
+ for (j = 0; j < 64; j++) {
+ var z = dctZigZag[j];
+ tableData[z] = data[offset++];
+ }
+ } else if ((quantizationTableSpec >> 4) === 1) { //16 bit
+ for (j = 0; j < 64; j++) {
+ var zz = dctZigZag[j];
+ tableData[zz] = readUint16();
+ }
+ } else
+ throw "DQT: invalid table spec";
+ quantizationTables[quantizationTableSpec & 15] = tableData;
+ }
+ break;
+
+ case 0xFFC0: // SOF0 (Start of Frame, Baseline DCT)
+ case 0xFFC1: // SOF1 (Start of Frame, Extended DCT)
+ case 0xFFC2: // SOF2 (Start of Frame, Progressive DCT)
+ if (frame) {
+ throw "Only single frame JPEGs supported";
+ }
+ readUint16(); // skip data length
+ frame = {};
+ frame.extended = (fileMarker === 0xFFC1);
+ frame.progressive = (fileMarker === 0xFFC2);
+ frame.precision = data[offset++];
+ frame.scanLines = readUint16();
+ frame.samplesPerLine = readUint16();
+ frame.components = [];
+ frame.componentIds = {};
+ var componentsCount = data[offset++], componentId;
+ var maxH = 0, maxV = 0;
+ for (i = 0; i < componentsCount; i++) {
+ componentId = data[offset];
+ var h = data[offset + 1] >> 4;
+ var v = data[offset + 1] & 15;
+ if (maxH < h)
+ maxH = h;
+ if (maxV < v)
+ maxV = v;
+ var qId = data[offset + 2];
+ l = frame.components.push({
+ h: h,
+ v: v,
+ quantizationTable: quantizationTables[qId],
+ quantizationTableId: qId,
+ bitConversion: 255 / ((1 << frame.precision) - 1)
+ });
+ frame.componentIds[componentId] = l - 1;
+ offset += 3;
+ }
+ frame.maxH = maxH;
+ frame.maxV = maxV;
+ prepareComponents(frame);
+ break;
+
+ case 0xFFC4: // DHT (Define Huffman Tables)
+ var huffmanLength = readUint16();
+ for (i = 2; i < huffmanLength; ) {
+ var huffmanTableSpec = data[offset++];
+ var codeLengths = new Uint8Array(16);
+ var codeLengthSum = 0;
+ for (j = 0; j < 16; j++, offset++)
+ codeLengthSum += (codeLengths[j] = data[offset]);
+ var huffmanValues = new Uint8Array(codeLengthSum);
+ for (j = 0; j < codeLengthSum; j++, offset++)
+ huffmanValues[j] = data[offset];
+ i += 17 + codeLengthSum;
+
+ ((huffmanTableSpec >> 4) === 0 ?
+ huffmanTablesDC : huffmanTablesAC)[huffmanTableSpec & 15] =
+ buildHuffmanTable(codeLengths, huffmanValues);
+ }
+ break;
+
+ case 0xFFDD: // DRI (Define Restart Interval)
+ readUint16(); // skip data length
+ resetInterval = readUint16();
+ break;
+
+ case 0xFFDA: // SOS (Start of Scan)
+ var scanLength = readUint16();
+ var selectorsCount = data[offset++];
+ var components = [], component;
+ for (i = 0; i < selectorsCount; i++) {
+ var componentIndex = frame.componentIds[data[offset++]];
+ component = frame.components[componentIndex];
+ var tableSpec = data[offset++];
+ component.huffmanTableDC = huffmanTablesDC[tableSpec >> 4];
+ component.huffmanTableAC = huffmanTablesAC[tableSpec & 15];
+ components.push(component);
+ }
+ var spectralStart = data[offset++];
+ var spectralEnd = data[offset++];
+ var successiveApproximation = data[offset++];
+ var processed = decodeScan(data, offset,
+ frame, components, resetInterval,
+ spectralStart, spectralEnd,
+ successiveApproximation >> 4, successiveApproximation & 15);
+ offset += processed;
+ break;
+ default:
+ if (data[offset - 3] == 0xFF &&
+ data[offset - 2] >= 0xC0 && data[offset - 2] <= 0xFE) {
+ // could be incorrect encoding -- last 0xFF byte of the previous
+ // block was eaten by the encoder
+ offset -= 3;
+ break;
+ }
+ throw "unknown JPEG marker " + fileMarker.toString(16);
+ }
+ fileMarker = readUint16();
+ }
+
+ this.width = frame.samplesPerLine;
+ this.height = frame.scanLines;
+ this.jfif = jfif;
+ this.adobe = adobe;
+ this.components = [];
+ switch (frame.components.length)
+ {
+ case 1:
+ this.colorspace = ColorSpace.Grayscale;
+ break;
+ case 3:
+ if (this.adobe)
+ this.colorspace = ColorSpace.AdobeRGB;
+ else
+ this.colorspace = ColorSpace.RGB;
+ break;
+ case 4:
+ this.colorspace = ColorSpace.CYMK;
+ break;
+ default:
+ this.colorspace = ColorSpace.Unknown;
+ }
+ for (var i = 0; i < frame.components.length; i++) {
+ var component = frame.components[i];
+ if (!component.quantizationTable && component.quantizationTableId !== null)
+ component.quantizationTable = quantizationTables[component.quantizationTableId];
+ this.components.push({
+ output: buildComponentData(frame, component),
+ scaleX: component.h / frame.maxH,
+ scaleY: component.v / frame.maxV,
+ blocksPerLine: component.blocksPerLine,
+ blocksPerColumn: component.blocksPerColumn,
+ bitConversion: component.bitConversion
+ });
+ }
+ },
+ getData16: function getData16(width, height) {
+ if (this.components.length !== 1)
+ throw 'Unsupported color mode';
+ var scaleX = this.width / width, scaleY = this.height / height;
+
+ var component, componentScaleX, componentScaleY;
+ var x, y, i;
+ var offset = 0;
+ var numComponents = this.components.length;
+ var dataLength = width * height * numComponents;
+ var data = new Uint16Array(dataLength);
+ var componentLine;
+
+ // lineData is reused for all components. Assume first component is
+ // the biggest
+ var lineData = new Uint16Array((this.components[0].blocksPerLine << 3) *
+ this.components[0].blocksPerColumn * 8);
+
+ // First construct image data ...
+ for (i = 0; i < numComponents; i++) {
+ component = this.components[i];
+ var blocksPerLine = component.blocksPerLine;
+ var blocksPerColumn = component.blocksPerColumn;
+ var samplesPerLine = blocksPerLine << 3;
+
+ var j, k, ll = 0;
+ var lineOffset = 0;
+ for (var blockRow = 0; blockRow < blocksPerColumn; blockRow++) {
+ var scanLine = blockRow << 3;
+ for (var blockCol = 0; blockCol < blocksPerLine; blockCol++) {
+ var bufferOffset = getBlockBufferOffset(component, blockRow, blockCol);
+ var offset = 0, sample = blockCol << 3;
+ for (j = 0; j < 8; j++) {
+ var lineOffset = (scanLine + j) * samplesPerLine;
+ for (k = 0; k < 8; k++) {
+ lineData[lineOffset + sample + k] =
+ component.output[bufferOffset + offset++];
+ }
+ }
+ }
+ }
+
+ componentScaleX = component.scaleX * scaleX;
+ componentScaleY = component.scaleY * scaleY;
+ offset = i;
+
+ var cx, cy;
+ var index;
+ for (y = 0; y < height; y++) {
+ for (x = 0; x < width; x++) {
+ cy = 0 | (y * componentScaleY);
+ cx = 0 | (x * componentScaleX);
+ index = cy * samplesPerLine + cx;
+ data[offset] = lineData[index];
+ offset += numComponents;
+ }
+ }
+ }
+ return data;
+ },
+ getData: function getData(width, height) {
+ var scaleX = this.width / width, scaleY = this.height / height;
+
+ var component, componentScaleX, componentScaleY;
+ var x, y, i;
+ var offset = 0;
+ var Y, Cb, Cr, K, C, M, Ye, R, G, B;
+ var colorTransform;
+ var numComponents = this.components.length;
+ var dataLength = width * height * numComponents;
+ var data = new Uint8Array(dataLength);
+ var componentLine;
+
+ // lineData is reused for all components. Assume first component is
+ // the biggest
+ var lineData = new Uint8Array((this.components[0].blocksPerLine << 3) *
+ this.components[0].blocksPerColumn * 8);
+
+ // First construct image data ...
+ for (i = 0; i < numComponents; i++) {
+ component = this.components[i];
+ var blocksPerLine = component.blocksPerLine;
+ var blocksPerColumn = component.blocksPerColumn;
+ var samplesPerLine = blocksPerLine << 3;
+
+ var j, k, ll = 0;
+ var lineOffset = 0;
+ for (var blockRow = 0; blockRow < blocksPerColumn; blockRow++) {
+ var scanLine = blockRow << 3;
+ for (var blockCol = 0; blockCol < blocksPerLine; blockCol++) {
+ var bufferOffset = getBlockBufferOffset(component, blockRow, blockCol);
+ var offset = 0, sample = blockCol << 3;
+ for (j = 0; j < 8; j++) {
+ var lineOffset = (scanLine + j) * samplesPerLine;
+ for (k = 0; k < 8; k++) {
+ lineData[lineOffset + sample + k] =
+ component.output[bufferOffset + offset++] * component.bitConversion;
+ }
+ }
+ }
+ }
+
+ componentScaleX = component.scaleX * scaleX;
+ componentScaleY = component.scaleY * scaleY;
+ offset = i;
+
+ var cx, cy;
+ var index;
+ for (y = 0; y < height; y++) {
+ for (x = 0; x < width; x++) {
+ cy = 0 | (y * componentScaleY);
+ cx = 0 | (x * componentScaleX);
+ index = cy * samplesPerLine + cx;
+ data[offset] = lineData[index];
+ offset += numComponents;
+ }
+ }
+ }
+
+ // ... then transform colors, if necessary
+ switch (numComponents) {
+ case 1:
+ case 2:
+ break;
+ // no color conversion for one or two compoenents
+
+ case 3:
+ // The default transform for three components is true
+ colorTransform = true;
+ // The adobe transform marker overrides any previous setting
+ if (this.adobe && this.adobe.transformCode)
+ colorTransform = true;
+ else if (typeof this.colorTransform !== 'undefined')
+ colorTransform = !!this.colorTransform;
+
+ if (colorTransform) {
+ for (i = 0; i < dataLength; i += numComponents) {
+ Y = data[i ];
+ Cb = data[i + 1];
+ Cr = data[i + 2];
+
+ R = clampToUint8(Y - 179.456 + 1.402 * Cr);
+ G = clampToUint8(Y + 135.459 - 0.344 * Cb - 0.714 * Cr);
+ B = clampToUint8(Y - 226.816 + 1.772 * Cb);
+
+ data[i ] = R;
+ data[i + 1] = G;
+ data[i + 2] = B;
+ }
+ }
+ break;
+ case 4:
+ if (!this.adobe)
+ throw 'Unsupported color mode (4 components)';
+ // The default transform for four components is false
+ colorTransform = false;
+ // The adobe transform marker overrides any previous setting
+ if (this.adobe && this.adobe.transformCode)
+ colorTransform = true;
+ else if (typeof this.colorTransform !== 'undefined')
+ colorTransform = !!this.colorTransform;
+
+ if (colorTransform) {
+ for (i = 0; i < dataLength; i += numComponents) {
+ Y = data[i];
+ Cb = data[i + 1];
+ Cr = data[i + 2];
+
+ C = clampToUint8(434.456 - Y - 1.402 * Cr);
+ M = clampToUint8(119.541 - Y + 0.344 * Cb + 0.714 * Cr);
+ Y = clampToUint8(481.816 - Y - 1.772 * Cb);
+
+ data[i ] = C;
+ data[i + 1] = M;
+ data[i + 2] = Y;
+ // K is unchanged
+ }
+ }
+ break;
+ default:
+ throw 'Unsupported color mode';
+ }
+ return data;
+ }
+ };
+
+ return constructor;
+})();
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.jpeg = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o> 4) !== 0x0FFC) || (current === 0xFFC4))) { // SOF 0~15
+ switch (current) {
+ case 0xFFC4: // DHT
+ this.huffTable.read(this.stream, this.HuffTab);
+ break;
+ case 0xFFCC: // DAC
+ throw new Error("Program doesn't support arithmetic coding. (format throw new IOException)");
+ case 0xFFDB:
+ this.quantTable.read(this.stream, jpeg.lossless.Decoder.TABLE);
+ break;
+ case 0xFFDD:
+ this.restartInterval = this.readNumber();
+ break;
+ case 0xFFE0:
+ case 0xFFE1:
+ case 0xFFE2:
+ case 0xFFE3:
+ case 0xFFE4:
+ case 0xFFE5:
+ case 0xFFE6:
+ case 0xFFE7:
+ case 0xFFE8:
+ case 0xFFE9:
+ case 0xFFEA:
+ case 0xFFEB:
+ case 0xFFEC:
+ case 0xFFED:
+ case 0xFFEE:
+ case 0xFFEF:
+ this.readApp();
+ break;
+ case 0xFFFE:
+ this.readComment();
+ break;
+ default:
+ if ((current >> 8) !== 0xFF) {
+ throw new Error("ERROR: format throw new IOException! (decode)");
+ }
+ }
+
+ current = this.stream.get16();
+ }
+
+ if ((current < 0xFFC0) || (current > 0xFFC7)) {
+ throw new Error("ERROR: could not handle arithmetic code!");
+ }
+
+ this.frame.read(this.stream);
+ current = this.stream.get16();
+
+ do {
+ while (current !== 0x0FFDA) { // SOS
+ switch (current) {
+ case 0xFFC4: // DHT
+ this.huffTable.read(this.stream, this.HuffTab);
+ break;
+ case 0xFFCC: // DAC
+ throw new Error("Program doesn't support arithmetic coding. (format throw new IOException)");
+ case 0xFFDB:
+ this.quantTable.read(this.stream, jpeg.lossless.Decoder.TABLE);
+ break;
+ case 0xFFDD:
+ this.restartInterval = this.readNumber();
+ break;
+ case 0xFFE0:
+ case 0xFFE1:
+ case 0xFFE2:
+ case 0xFFE3:
+ case 0xFFE4:
+ case 0xFFE5:
+ case 0xFFE6:
+ case 0xFFE7:
+ case 0xFFE8:
+ case 0xFFE9:
+ case 0xFFEA:
+ case 0xFFEB:
+ case 0xFFEC:
+ case 0xFFED:
+ case 0xFFEE:
+ case 0xFFEF:
+ this.readApp();
+ break;
+ case 0xFFFE:
+ this.readComment();
+ break;
+ default:
+ if ((current >> 8) !== 0xFF) {
+ throw new Error("ERROR: format throw new IOException! (Parser.decode)");
+ }
+ }
+
+ current = this.stream.get16();
+ }
+
+ this.precision = this.frame.precision;
+ this.components = this.frame.components;
+
+ if (!this.numBytes) {
+ this.numBytes = parseInt(this.precision / 8);
+ }
+
+ this.scan.read(this.stream);
+ this.numComp = this.scan.numComp;
+ this.selection = this.scan.selection;
+
+ if (this.numBytes === 1) {
+ if (this.numComp === 3) {
+ this.getter = this.getValueRGB;
+ this.setter = this.setValueRGB;
+ this.output = this.outputRGB;
+ } else {
+ this.getter = this.getValue8;
+ this.setter = this.setValue8;
+ this.output = this.outputSingle;
+ }
+ } else {
+ this.getter = this.getValue16;
+ this.setter = this.setValue16;
+ this.output = this.outputSingle;
+ }
+
+ switch (this.selection) {
+ case 2:
+ this.selector = this.select2;
+ break;
+ case 3:
+ this.selector = this.select3;
+ break;
+ case 4:
+ this.selector = this.select4;
+ break;
+ case 5:
+ this.selector = this.select5;
+ break;
+ case 6:
+ this.selector = this.select6;
+ break;
+ case 7:
+ this.selector = this.select7;
+ break;
+ default:
+ this.selector = this.select1;
+ break;
+ }
+
+ this.scanComps = this.scan.components;
+ this.quantTables = this.quantTable.quantTables;
+
+ for (i = 0; i < this.numComp; i+=1) {
+ compN = this.scanComps[i].scanCompSel;
+ this.qTab[i] = this.quantTables[this.components[compN].quantTableSel];
+ this.nBlock[i] = this.components[compN].vSamp * this.components[compN].hSamp;
+ this.dcTab[i] = this.HuffTab[this.scanComps[i].dcTabSel][0];
+ this.acTab[i] = this.HuffTab[this.scanComps[i].acTabSel][1];
+ }
+
+ this.xDim = this.frame.dimX;
+ this.yDim = this.frame.dimY;
+ this.outputData = new DataView(new ArrayBuffer(this.xDim * this.yDim * this.numBytes * this.numComp));
+
+ scanNum+=1;
+
+ while (true) { // Decode one scan
+ temp[0] = 0;
+ index[0] = 0;
+
+ for (i = 0; i < 10; i+=1) {
+ pred[i] = (1 << (this.precision - 1));
+ }
+
+ if (this.restartInterval === 0) {
+ current = this.decodeUnit(pred, temp, index);
+
+ while ((current === 0) && ((this.xLoc < this.xDim) && (this.yLoc < this.yDim))) {
+ this.output(pred);
+ current = this.decodeUnit(pred, temp, index);
+ }
+
+ break; //current=MARKER
+ }
+
+ for (mcuNum = 0; mcuNum < this.restartInterval; mcuNum+=1) {
+ current = this.decodeUnit(pred, temp, index);
+ this.output(pred);
+
+ if (current !== 0) {
+ break;
+ }
+ }
+
+ if (current === 0) {
+ if (this.markerIndex !== 0) {
+ current = (0xFF00 | this.marker);
+ this.markerIndex = 0;
+ } else {
+ current = this.stream.get16();
+ }
+ }
+
+ if (!((current >= 0xFFD0) && (current <= 0xFFD7))) {
+ break; //current=MARKER
+ }
+ }
+
+ if ((current === 0xFFDC) && (scanNum === 1)) { //DNL
+ this.readNumber();
+ current = this.stream.get16();
+ }
+ } while ((current !== 0xFFD9) && ((this.xLoc < this.xDim) && (this.yLoc < this.yDim)) && (scanNum === 0));
+
+ return this.outputData;
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.decodeUnit = function (prev, temp, index) {
+ if (this.numComp == 1) {
+ return this.decodeSingle(prev, temp, index);
+ } else if (this.numComp == 3) {
+ return this.decodeRGB(prev, temp, index);
+ } else {
+ return -1;
+ }
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.select1 = function (compOffset) {
+ return this.getPreviousX(compOffset);
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.select2 = function (compOffset) {
+ return this.getPreviousY(compOffset);
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.select3 = function (compOffset) {
+ return this.getPreviousXY(compOffset);
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.select4 = function (compOffset) {
+ return (this.getPreviousX(compOffset) + this.getPreviousY(compOffset)) - this.getPreviousXY(compOffset);
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.select5 = function (compOffset) {
+ return this.getPreviousX(compOffset) + ((this.getPreviousY(compOffset) - this.getPreviousXY(compOffset)) >> 1);
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.select6 = function (compOffset) {
+ return this.getPreviousY(compOffset) + ((this.getPreviousX(compOffset) - this.getPreviousXY(compOffset)) >> 1);
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.select7 = function (compOffset) {
+ return ((this.getPreviousX(compOffset) + this.getPreviousY(compOffset)) / 2);
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.decodeRGB = function (prev, temp, index) {
+ /*jslint bitwise: true */
+
+ var value, actab, dctab, qtab, ctrC, i, k, j;
+
+ prev[0] = this.selector(0);
+ prev[1] = this.selector(1);
+ prev[2] = this.selector(2);
+
+ for (ctrC = 0; ctrC < this.numComp; ctrC+=1) {
+ qtab = this.qTab[ctrC];
+ actab = this.acTab[ctrC];
+ dctab = this.dcTab[ctrC];
+ for (i = 0; i < this.nBlock[ctrC]; i+=1) {
+ for (k = 0; k < this.IDCT_Source.length; k+=1) {
+ this.IDCT_Source[k] = 0;
+ }
+
+ value = this.getHuffmanValue(dctab, temp, index);
+
+ if (value >= 0xFF00) {
+ return value;
+ }
+
+ prev[ctrC] = this.IDCT_Source[0] = prev[ctrC] + this.getn(index, value, temp, index);
+ this.IDCT_Source[0] *= qtab[0];
+
+ for (j = 1; j < 64; j+=1) {
+ value = this.getHuffmanValue(actab, temp, index);
+
+ if (value >= 0xFF00) {
+ return value;
+ }
+
+ j += (value >> 4);
+
+ if ((value & 0x0F) === 0) {
+ if ((value >> 4) === 0) {
+ break;
+ }
+ } else {
+ this.IDCT_Source[jpeg.lossless.Decoder.IDCT_P[j]] = this.getn(index, value & 0x0F, temp, index) * qtab[j];
+ }
+ }
+ }
+ }
+
+ return 0;
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.decodeSingle = function (prev, temp, index) {
+ /*jslint bitwise: true */
+
+ var value, i;
+
+ prev[0] = this.selector();
+
+ for (i = 0; i < this.nBlock[0]; i+=1) {
+ value = this.getHuffmanValue(this.dcTab[0], temp, index);
+ if (value >= 0xFF00) {
+ return value;
+ }
+
+ prev[0] += this.getn(prev, value, temp, index);
+ }
+
+ return 0;
+ };
+
+
+
+// Huffman table for fast search: (HuffTab) 8-bit Look up table 2-layer search architecture, 1st-layer represent 256 node (8 bits) if codeword-length > 8
+// bits, then the entry of 1st-layer = (# of 2nd-layer table) | MSB and it is stored in the 2nd-layer Size of tables in each layer are 256.
+// HuffTab[*][*][0-256] is always the only 1st-layer table.
+//
+// An entry can be: (1) (# of 2nd-layer table) | MSB , for code length > 8 in 1st-layer (2) (Code length) << 8 | HuffVal
+//
+// HuffmanValue(table HuffTab[x][y] (ex) HuffmanValue(HuffTab[1][0],...)
+// ):
+// return: Huffman Value of table
+// 0xFF?? if it receives a MARKER
+// Parameter: table HuffTab[x][y] (ex) HuffmanValue(HuffTab[1][0],...)
+// temp temp storage for remainded bits
+// index index to bit of temp
+// in FILE pointer
+// Effect:
+// temp store new remainded bits
+// index change to new index
+// in change to new position
+// NOTE:
+// Initial by temp=0; index=0;
+// NOTE: (explain temp and index)
+// temp: is always in the form at calling time or returning time
+// | byte 4 | byte 3 | byte 2 | byte 1 |
+// | 0 | 0 | 00000000 | 00000??? | if not a MARKER
+// ^index=3 (from 0 to 15)
+// 321
+// NOTE (marker and marker_index):
+// If get a MARKER from 'in', marker=the low-byte of the MARKER
+// and marker_index=9
+// If marker_index=9 then index is always > 8, or HuffmanValue()
+// will not be called
+ jpeg.lossless.Decoder.prototype.getHuffmanValue = function (table, temp, index) {
+ /*jslint bitwise: true */
+
+ var code, input, mask;
+ mask = 0xFFFF;
+
+ if (index[0] < 8) {
+ temp[0] <<= 8;
+ input = this.stream.get8();
+ if (input === 0xFF) {
+ this.marker = this.stream.get8();
+ if (this.marker !== 0) {
+ this.markerIndex = 9;
+ }
+ }
+ temp[0] |= input;
+ } else {
+ index[0] -= 8;
+ }
+
+ code = table[temp[0] >> index[0]];
+
+ if ((code & jpeg.lossless.Decoder.MSB) !== 0) {
+ if (this.markerIndex !== 0) {
+ this.markerIndex = 0;
+ return 0xFF00 | this.marker;
+ }
+
+ temp[0] &= (mask >> (16 - index[0]));
+ temp[0] <<= 8;
+ input = this.stream.get8();
+
+ if (input === 0xFF) {
+ this.marker = this.stream.get8();
+ if (this.marker !== 0) {
+ this.markerIndex = 9;
+ }
+ }
+
+ temp[0] |= input;
+ code = table[((code & 0xFF) * 256) + (temp[0] >> index[0])];
+ index[0] += 8;
+ }
+
+ index[0] += 8 - (code >> 8);
+
+ if (index[0] < 0) {
+ throw new Error("index=" + index[0] + " temp=" + temp[0] + " code=" + code + " in HuffmanValue()");
+ }
+
+ if (index[0] < this.markerIndex) {
+ this.markerIndex = 0;
+ return 0xFF00 | this.marker;
+ }
+
+ temp[0] &= (mask >> (16 - index[0]));
+ return code & 0xFF;
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.getn = function (PRED, n, temp, index) {
+ /*jslint bitwise: true */
+
+ var result, one, n_one, mask, input;
+ one = 1;
+ n_one = -1;
+ mask = 0xFFFF;
+
+ if (n === 0) {
+ return 0;
+ }
+
+ if (n === 16) {
+ if (PRED[0] >= 0) {
+ return -32768;
+ } else {
+ return 32768;
+ }
+ }
+
+ index[0] -= n;
+
+ if (index[0] >= 0) {
+ if ((index[0] < this.markerIndex) && !this.isLastPixel()) { // this was corrupting the last pixel in some cases
+ this.markerIndex = 0;
+ return (0xFF00 | this.marker) << 8;
+ }
+
+ result = temp[0] >> index[0];
+ temp[0] &= (mask >> (16 - index[0]));
+ } else {
+ temp[0] <<= 8;
+ input = this.stream.get8();
+
+ if (input === 0xFF) {
+ this.marker = this.stream.get8();
+ if (this.marker !== 0) {
+ this.markerIndex = 9;
+ }
+ }
+
+ temp[0] |= input;
+ index[0] += 8;
+
+ if (index[0] < 0) {
+ if (this.markerIndex !== 0) {
+ this.markerIndex = 0;
+ return (0xFF00 | this.marker) << 8;
+ }
+
+ temp[0] <<= 8;
+ input = this.stream.get8();
+
+ if (input === 0xFF) {
+ this.marker = this.stream.get8();
+ if (this.marker !== 0) {
+ this.markerIndex = 9;
+ }
+ }
+
+ temp[0] |= input;
+ index[0] += 8;
+ }
+
+ if (index[0] < 0) {
+ throw new Error("index=" + index[0] + " in getn()");
+ }
+
+ if (index[0] < this.markerIndex) {
+ this.markerIndex = 0;
+ return (0xFF00 | this.marker) << 8;
+ }
+
+ result = temp[0] >> index[0];
+ temp[0] &= (mask >> (16 - index[0]));
+ }
+
+ if (result < (one << (n - 1))) {
+ result += (n_one << n) + 1;
+ }
+
+ return result;
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.getPreviousX = function (compOffset) {
+ /*jslint bitwise: true */
+
+ if (this.xLoc > 0) {
+ return this.getter((((this.yLoc * this.xDim) + this.xLoc) - 1), compOffset);
+ } else if (this.yLoc > 0) {
+ return this.getPreviousY(compOffset);
+ } else {
+ return (1 << (this.frame.precision - 1));
+ }
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.getPreviousXY = function (compOffset) {
+ /*jslint bitwise: true */
+
+ if ((this.xLoc > 0) && (this.yLoc > 0)) {
+ return this.getter(((((this.yLoc - 1) * this.xDim) + this.xLoc) - 1), compOffset);
+ } else {
+ return this.getPreviousY(compOffset);
+ }
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.getPreviousY = function (compOffset) {
+ /*jslint bitwise: true */
+
+ if (this.yLoc > 0) {
+ return this.getter((((this.yLoc - 1) * this.xDim) + this.xLoc), compOffset);
+ } else {
+ return this.getPreviousX(compOffset);
+ }
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.isLastPixel = function () {
+ return (this.xLoc === (this.xDim - 1)) && (this.yLoc === (this.yDim - 1));
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.outputSingle = function (PRED) {
+ if ((this.xLoc < this.xDim) && (this.yLoc < this.yDim)) {
+ this.setter((((this.yLoc * this.xDim) + this.xLoc)), PRED[0]);
+
+ this.xLoc+=1;
+
+ if (this.xLoc >= this.xDim) {
+ this.yLoc+=1;
+ this.xLoc = 0;
+ }
+ }
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.outputRGB = function (PRED) {
+ var offset = ((this.yLoc * this.xDim) + this.xLoc);
+
+ if ((this.xLoc < this.xDim) && (this.yLoc < this.yDim)) {
+ this.setter(offset, PRED[0], 0);
+ this.setter(offset, PRED[1], 1);
+ this.setter(offset, PRED[2], 2);
+
+ this.xLoc+=1;
+
+ if (this.xLoc >= this.xDim) {
+ this.yLoc+=1;
+ this.xLoc = 0;
+ }
+ }
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.setValue16 = function (index, val) {
+ this.outputData.setInt16(index * 2, val, true);
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.getValue16 = function (index) {
+ return this.outputData.getInt16(index * 2, true);
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.setValue8 = function (index, val) {
+ this.outputData.setInt8(index, val);
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.getValue8 = function (index) {
+ return this.outputData.getInt8(index);
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.setValueRGB = function (index, val, compOffset) {
+ this.outputData.setUint8(index * 3 + compOffset, val);
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.getValueRGB = function (index, compOffset) {
+ return this.outputData.getUint8(index * 3 + compOffset);
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.readApp = function() {
+ var count = 0, length = this.stream.get16();
+ count += 2;
+
+ while (count < length) {
+ this.stream.get8();
+ count+=1;
+ }
+
+ return length;
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.readComment = function () {
+ var sb = "", count = 0, length;
+
+ length = this.stream.get16();
+ count += 2;
+
+ while (count < length) {
+ sb += this.stream.get8();
+ count+=1;
+ }
+
+ return sb;
+ };
+
+
+
+ jpeg.lossless.Decoder.prototype.readNumber = function() {
+ var Ld = this.stream.get16();
+
+ if (Ld !== 4) {
+ throw new Error("ERROR: Define number format throw new IOException [Ld!=4]");
+ }
+
+ return this.stream.get16();
+ };
+
+
+
+ /*** Exports ***/
+
+ var moduleType = typeof module;
+ if ((moduleType !== 'undefined') && module.exports) {
+ module.exports = jpeg.lossless.Decoder;
+ }
+
+},{"./data-stream.js":2,"./frame-header.js":4,"./huffman-table.js":5,"./quantization-table.js":7,"./scan-header.js":9,"./utils.js":10}],4:[function(require,module,exports){
+ /*
+ * Copyright (C) 2015 Michael Martinez
+ * Changes: Added support for selection values 2-7, fixed minor bugs &
+ * warnings, split into multiple class files, and general clean up.
+ *
+ * 08-25-2015: Helmut Dersch agreed to a license change from LGPL to MIT.
+ */
+
+ /*
+ * Copyright (C) Helmut Dersch
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+ /*jslint browser: true, node: true */
+ /*global require, module */
+
+ "use strict";
+
+ /*** Imports ***/
+ var jpeg = jpeg || {};
+ jpeg.lossless = jpeg.lossless || {};
+ jpeg.lossless.ComponentSpec = jpeg.lossless.ComponentSpec || ((typeof require !== 'undefined') ? require('./component-spec.js') : null);
+ jpeg.lossless.DataStream = jpeg.lossless.DataStream || ((typeof require !== 'undefined') ? require('./data-stream.js') : null);
+
+
+ /*** Constructor ***/
+ jpeg.lossless.FrameHeader = jpeg.lossless.FrameHeader || function () {
+ this.components = []; // Components
+ this.dimX = 0; // Number of samples per line
+ this.dimY = 0; // Number of lines
+ this.numComp = 0; // Number of component in the frame
+ this.precision = 0; // Sample Precision (from the original image)
+ };
+
+
+
+ /*** Prototype Methods ***/
+
+ jpeg.lossless.FrameHeader.prototype.read = function (data) {
+ /*jslint bitwise: true */
+
+ var count = 0, length, i, c, temp;
+
+ length = data.get16();
+ count += 2;
+
+ this.precision = data.get8();
+ count+=1;
+
+ this.dimY = data.get16();
+ count += 2;
+
+ this.dimX = data.get16();
+ count += 2;
+
+ this.numComp = data.get8();
+ count+=1;
+ for (i = 1; i <= this.numComp; i+=1) {
+ if (count > length) {
+ throw new Error("ERROR: frame format error");
+ }
+
+ c = data.get8();
+ count+=1;
+
+ if (count >= length) {
+ throw new Error("ERROR: frame format error [c>=Lf]");
+ }
+
+ temp = data.get8();
+ count+=1;
+
+ if (!this.components[c]) {
+ this.components[c] = new jpeg.lossless.ComponentSpec();
+ }
+
+ this.components[c].hSamp = temp >> 4;
+ this.components[c].vSamp = temp & 0x0F;
+ this.components[c].quantTableSel = data.get8();
+ count+=1;
+ }
+
+ if (count !== length) {
+ throw new Error("ERROR: frame format error [Lf!=count]");
+ }
+
+ return 1;
+ };
+
+
+ /*** Exports ***/
+
+ var moduleType = typeof module;
+ if ((moduleType !== 'undefined') && module.exports) {
+ module.exports = jpeg.lossless.FrameHeader;
+ }
+
+},{"./component-spec.js":1,"./data-stream.js":2}],5:[function(require,module,exports){
+ /*
+ * Copyright (C) 2015 Michael Martinez
+ * Changes: Added support for selection values 2-7, fixed minor bugs &
+ * warnings, split into multiple class files, and general clean up.
+ *
+ * 08-25-2015: Helmut Dersch agreed to a license change from LGPL to MIT.
+ */
+
+ /*
+ * Copyright (C) Helmut Dersch
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+ /*jslint browser: true, node: true */
+ /*global require, module */
+
+ "use strict";
+
+ /*** Imports ***/
+ var jpeg = jpeg || {};
+ jpeg.lossless = jpeg.lossless || {};
+ jpeg.lossless.DataStream = jpeg.lossless.DataStream || ((typeof require !== 'undefined') ? require('./data-stream.js') : null);
+ jpeg.lossless.Utils = jpeg.lossless.Utils || ((typeof require !== 'undefined') ? require('./utils.js') : null);
+
+
+ /*** Constructor ***/
+ jpeg.lossless.HuffmanTable = jpeg.lossless.HuffmanTable || function () {
+ this.l = jpeg.lossless.Utils.createArray(4, 2, 16);
+ this.th = [];
+ this.v = jpeg.lossless.Utils.createArray(4, 2, 16, 200);
+ this.tc = jpeg.lossless.Utils.createArray(4, 2);
+
+ this.tc[0][0] = 0;
+ this.tc[1][0] = 0;
+ this.tc[2][0] = 0;
+ this.tc[3][0] = 0;
+ this.tc[0][1] = 0;
+ this.tc[1][1] = 0;
+ this.tc[2][1] = 0;
+ this.tc[3][1] = 0;
+ this.th[0] = 0;
+ this.th[1] = 0;
+ this.th[2] = 0;
+ this.th[3] = 0;
+ };
+
+
+
+ /*** Static Pseudo-constants ***/
+
+ jpeg.lossless.HuffmanTable.MSB = 0x80000000;
+
+
+ /*** Prototype Methods ***/
+
+ jpeg.lossless.HuffmanTable.prototype.read = function(data, HuffTab) {
+ /*jslint bitwise: true */
+
+ var count = 0, length, temp, t, c, i, j;
+
+ length = data.get16();
+ count += 2;
+
+ while (count < length) {
+ temp = data.get8();
+ count+=1;
+ t = temp & 0x0F;
+ if (t > 3) {
+ throw new Error("ERROR: Huffman table ID > 3");
+ }
+
+ c = temp >> 4;
+ if (c > 2) {
+ throw new Error("ERROR: Huffman table [Table class > 2 ]");
+ }
+
+ this.th[t] = 1;
+ this.tc[t][c] = 1;
+
+ for (i = 0; i < 16; i+=1) {
+ this.l[t][c][i] = data.get8();
+ count+=1;
+ }
+
+ for (i = 0; i < 16; i+=1) {
+ for (j = 0; j < this.l[t][c][i]; j+=1) {
+ if (count > length) {
+ throw new Error("ERROR: Huffman table format error [count>Lh]");
+ }
+
+ this.v[t][c][i][j] = data.get8();
+ count+=1;
+ }
+ }
+ }
+
+ if (count !== length) {
+ throw new Error("ERROR: Huffman table format error [count!=Lf]");
+ }
+
+ for (i = 0; i < 4; i+=1) {
+ for (j = 0; j < 2; j+=1) {
+ if (this.tc[i][j] !== 0) {
+ this.buildHuffTable(HuffTab[i][j], this.l[i][j], this.v[i][j]);
+ }
+ }
+ }
+
+ return 1;
+ };
+
+
+
+// Build_HuffTab()
+// Parameter: t table ID
+// c table class ( 0 for DC, 1 for AC )
+// L[i] # of codewords which length is i
+// V[i][j] Huffman Value (length=i)
+// Effect:
+// build up HuffTab[t][c] using L and V.
+ jpeg.lossless.HuffmanTable.prototype.buildHuffTable = function(tab, L, V) {
+ /*jslint bitwise: true */
+
+ var currentTable, temp, k, i, j, n;
+ temp = 256;
+ k = 0;
+
+ for (i = 0; i < 8; i+=1) { // i+1 is Code length
+ for (j = 0; j < L[i]; j+=1) {
+ for (n = 0; n < (temp >> (i + 1)); n+=1) {
+ tab[k] = V[i][j] | ((i + 1) << 8);
+ k+=1;
+ }
+ }
+ }
+
+ for (i = 1; k < 256; i+=1, k+=1) {
+ tab[k] = i | jpeg.lossless.HuffmanTable.MSB;
+ }
+
+ currentTable = 1;
+ k = 0;
+
+ for (i = 8; i < 16; i+=1) { // i+1 is Code length
+ for (j = 0; j < L[i]; j+=1) {
+ for (n = 0; n < (temp >> (i - 7)); n+=1) {
+ tab[(currentTable * 256) + k] = V[i][j] | ((i + 1) << 8);
+ k+=1;
+ }
+
+ if (k >= 256) {
+ if (k > 256) {
+ throw new Error("ERROR: Huffman table error(1)!");
+ }
+
+ k = 0;
+ currentTable+=1;
+ }
+ }
+ }
+ };
+
+
+ /*** Exports ***/
+
+ var moduleType = typeof module;
+ if ((moduleType !== 'undefined') && module.exports) {
+ module.exports = jpeg.lossless.HuffmanTable;
+ }
+
+},{"./data-stream.js":2,"./utils.js":10}],6:[function(require,module,exports){
+ /*jslint browser: true, node: true */
+ /*global require, module */
+
+ "use strict";
+
+ /*** Imports ***/
+ var jpeg = jpeg || {};
+ jpeg.lossless = jpeg.lossless || {};
+ jpeg.lossless.ComponentSpec = jpeg.lossless.ComponentSpec || ((typeof require !== 'undefined') ? require('./component-spec.js') : null);
+ jpeg.lossless.DataStream = jpeg.lossless.DataStream || ((typeof require !== 'undefined') ? require('./data-stream.js') : null);
+ jpeg.lossless.Decoder = jpeg.lossless.Decoder || ((typeof require !== 'undefined') ? require('./decoder.js') : null);
+ jpeg.lossless.FrameHeader = jpeg.lossless.FrameHeader || ((typeof require !== 'undefined') ? require('./frame-header.js') : null);
+ jpeg.lossless.HuffmanTable = jpeg.lossless.HuffmanTable || ((typeof require !== 'undefined') ? require('./huffman-table.js') : null);
+ jpeg.lossless.QuantizationTable = jpeg.lossless.QuantizationTable || ((typeof require !== 'undefined') ? require('./quantization-table.js') : null);
+ jpeg.lossless.ScanComponent = jpeg.lossless.ScanComponent || ((typeof require !== 'undefined') ? require('./scan-component.js') : null);
+ jpeg.lossless.ScanHeader = jpeg.lossless.ScanHeader || ((typeof require !== 'undefined') ? require('./scan-header.js') : null);
+ jpeg.lossless.Utils = jpeg.lossless.Utils || ((typeof require !== 'undefined') ? require('./utils.js') : null);
+
+
+ /*** Exports ***/
+ var moduleType = typeof module;
+ if ((moduleType !== 'undefined') && module.exports) {
+ module.exports = jpeg;
+ }
+
+},{"./component-spec.js":1,"./data-stream.js":2,"./decoder.js":3,"./frame-header.js":4,"./huffman-table.js":5,"./quantization-table.js":7,"./scan-component.js":8,"./scan-header.js":9,"./utils.js":10}],7:[function(require,module,exports){
+ /*
+ * Copyright (C) 2015 Michael Martinez
+ * Changes: Added support for selection values 2-7, fixed minor bugs &
+ * warnings, split into multiple class files, and general clean up.
+ *
+ * 08-25-2015: Helmut Dersch agreed to a license change from LGPL to MIT.
+ */
+
+ /*
+ * Copyright (C) Helmut Dersch
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+ /*jslint browser: true, node: true */
+ /*global require, module */
+
+ "use strict";
+
+ /*** Imports ***/
+ var jpeg = jpeg || {};
+ jpeg.lossless = jpeg.lossless || {};
+ jpeg.lossless.DataStream = jpeg.lossless.DataStream || ((typeof require !== 'undefined') ? require('./data-stream.js') : null);
+ jpeg.lossless.Utils = jpeg.lossless.Utils || ((typeof require !== 'undefined') ? require('./utils.js') : null);
+
+
+ /*** Constructor ***/
+ jpeg.lossless.QuantizationTable = jpeg.lossless.QuantizationTable || function () {
+ this.precision = []; // Quantization precision 8 or 16
+ this.tq = []; // 1: this table is presented
+ this.quantTables = jpeg.lossless.Utils.createArray(4, 64); // Tables
+
+ this.tq[0] = 0;
+ this.tq[1] = 0;
+ this.tq[2] = 0;
+ this.tq[3] = 0;
+ };
+
+
+
+ /*** Static Methods ***/
+
+ jpeg.lossless.QuantizationTable.enhanceQuantizationTable = function(qtab, table) {
+ /*jslint bitwise: true */
+
+ var i;
+
+ for (i = 0; i < 8; i+=1) {
+ qtab[table[(0 * 8) + i]] *= 90;
+ qtab[table[(4 * 8) + i]] *= 90;
+ qtab[table[(2 * 8) + i]] *= 118;
+ qtab[table[(6 * 8) + i]] *= 49;
+ qtab[table[(5 * 8) + i]] *= 71;
+ qtab[table[(1 * 8) + i]] *= 126;
+ qtab[table[(7 * 8) + i]] *= 25;
+ qtab[table[(3 * 8) + i]] *= 106;
+ }
+
+ for (i = 0; i < 8; i+=1) {
+ qtab[table[0 + (8 * i)]] *= 90;
+ qtab[table[4 + (8 * i)]] *= 90;
+ qtab[table[2 + (8 * i)]] *= 118;
+ qtab[table[6 + (8 * i)]] *= 49;
+ qtab[table[5 + (8 * i)]] *= 71;
+ qtab[table[1 + (8 * i)]] *= 126;
+ qtab[table[7 + (8 * i)]] *= 25;
+ qtab[table[3 + (8 * i)]] *= 106;
+ }
+
+ for (i = 0; i < 64; i+=1) {
+ qtab[i] >>= 6;
+ }
+ };
+
+
+ /*** Prototype Methods ***/
+
+ jpeg.lossless.QuantizationTable.prototype.read = function (data, table) {
+ /*jslint bitwise: true */
+
+ var count = 0, length, temp, t, i;
+
+ length = data.get16();
+ count += 2;
+
+ while (count < length) {
+ temp = data.get8();
+ count+=1;
+ t = temp & 0x0F;
+
+ if (t > 3) {
+ throw new Error("ERROR: Quantization table ID > 3");
+ }
+
+ this.precision[t] = temp >> 4;
+
+ if (this.precision[t] === 0) {
+ this.precision[t] = 8;
+ } else if (this.precision[t] === 1) {
+ this.precision[t] = 16;
+ } else {
+ throw new Error("ERROR: Quantization table precision error");
+ }
+
+ this.tq[t] = 1;
+
+ if (this.precision[t] === 8) {
+ for (i = 0; i < 64; i+=1) {
+ if (count > length) {
+ throw new Error("ERROR: Quantization table format error");
+ }
+
+ this.quantTables[t][i] = data.get8();
+ count+=1;
+ }
+
+ jpeg.lossless.QuantizationTable.enhanceQuantizationTable(this.quantTables[t], table);
+ } else {
+ for (i = 0; i < 64; i+=1) {
+ if (count > length) {
+ throw new Error("ERROR: Quantization table format error");
+ }
+
+ this.quantTables[t][i] = data.get16();
+ count += 2;
+ }
+
+ jpeg.lossless.QuantizationTable.enhanceQuantizationTable(this.quantTables[t], table);
+ }
+ }
+
+ if (count !== length) {
+ throw new Error("ERROR: Quantization table error [count!=Lq]");
+ }
+
+ return 1;
+ };
+
+
+
+ /*** Exports ***/
+
+ var moduleType = typeof module;
+ if ((moduleType !== 'undefined') && module.exports) {
+ module.exports = jpeg.lossless.QuantizationTable;
+ }
+
+},{"./data-stream.js":2,"./utils.js":10}],8:[function(require,module,exports){
+ /*
+ * Copyright (C) 2015 Michael Martinez
+ * Changes: Added support for selection values 2-7, fixed minor bugs &
+ * warnings, split into multiple class files, and general clean up.
+ *
+ * 08-25-2015: Helmut Dersch agreed to a license change from LGPL to MIT.
+ */
+
+ /*
+ * Copyright (C) Helmut Dersch
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+ /*jslint browser: true, node: true */
+ /*global require, module */
+
+ "use strict";
+
+ /*** Imports ***/
+ var jpeg = jpeg || {};
+ jpeg.lossless = jpeg.lossless || {};
+
+
+ /*** Constructor ***/
+ jpeg.lossless.ScanComponent = jpeg.lossless.ScanComponent || function () {
+ this.acTabSel = 0; // AC table selector
+ this.dcTabSel = 0; // DC table selector
+ this.scanCompSel = 0; // Scan component selector
+ };
+
+
+
+ /*** Exports ***/
+
+ var moduleType = typeof module;
+ if ((moduleType !== 'undefined') && module.exports) {
+ module.exports = jpeg.lossless.ScanComponent;
+ }
+
+},{}],9:[function(require,module,exports){
+ /*
+ * Copyright (C) 2015 Michael Martinez
+ * Changes: Added support for selection values 2-7, fixed minor bugs &
+ * warnings, split into multiple class files, and general clean up.
+ *
+ * 08-25-2015: Helmut Dersch agreed to a license change from LGPL to MIT.
+ */
+
+ /*
+ * Copyright (C) Helmut Dersch
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+ /*jslint browser: true, node: true */
+ /*global require, module */
+
+ "use strict";
+
+ /*** Imports ***/
+ var jpeg = jpeg || {};
+ jpeg.lossless = jpeg.lossless || {};
+ jpeg.lossless.DataStream = jpeg.lossless.DataStream || ((typeof require !== 'undefined') ? require('./data-stream.js') : null);
+ jpeg.lossless.ScanComponent = jpeg.lossless.ScanComponent || ((typeof require !== 'undefined') ? require('./scan-component.js') : null);
+
+
+ /*** Constructor ***/
+ jpeg.lossless.ScanHeader = jpeg.lossless.ScanHeader || function () {
+ this.ah = 0;
+ this.al = 0;
+ this.numComp = 0; // Number of components in the scan
+ this.selection = 0; // Start of spectral or predictor selection
+ this.spectralEnd = 0; // End of spectral selection
+ this.components = [];
+ };
+
+
+ /*** Prototype Methods ***/
+
+ jpeg.lossless.ScanHeader.prototype.read = function(data) {
+ /*jslint bitwise: true */
+
+ var count = 0, length, i, temp;
+
+ length = data.get16();
+ count += 2;
+
+ this.numComp = data.get8();
+ count+=1;
+
+ for (i = 0; i < this.numComp; i+=1) {
+ this.components[i] = new jpeg.lossless.ScanComponent();
+
+ if (count > length) {
+ throw new Error("ERROR: scan header format error");
+ }
+
+ this.components[i].scanCompSel = data.get8();
+ count+=1;
+
+ temp = data.get8();
+ count+=1;
+
+ this.components[i].dcTabSel = (temp >> 4);
+ this.components[i].acTabSel = (temp & 0x0F);
+ }
+
+ this.selection = data.get8();
+ count+=1;
+
+ this.spectralEnd = data.get8();
+ count+=1;
+
+ temp = data.get8();
+ this.ah = (temp >> 4);
+ this.al = (temp & 0x0F);
+ count+=1;
+
+ if (count !== length) {
+ throw new Error("ERROR: scan header format error [count!=Ns]");
+ }
+
+ return 1;
+ };
+
+
+
+ /*** Exports ***/
+
+ var moduleType = typeof module;
+ if ((moduleType !== 'undefined') && module.exports) {
+ module.exports = jpeg.lossless.ScanHeader;
+ }
+
+},{"./data-stream.js":2,"./scan-component.js":8}],10:[function(require,module,exports){
+ /*
+ * Copyright (C) 2015 Michael Martinez
+ * Changes: Added support for selection values 2-7, fixed minor bugs &
+ * warnings, split into multiple class files, and general clean up.
+ *
+ * 08-25-2015: Helmut Dersch agreed to a license change from LGPL to MIT.
+ */
+
+ /*
+ * Copyright (C) Helmut Dersch
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+ /*jslint browser: true, node: true */
+ /*global require, module */
+
+ "use strict";
+
+ /*** Imports ***/
+ var jpeg = jpeg || {};
+ jpeg.lossless = jpeg.lossless || {};
+
+
+ /*** Constructor ***/
+ jpeg.lossless.Utils = jpeg.lossless.Utils || {};
+
+
+ /*** Static methods ***/
+
+// http://stackoverflow.com/questions/966225/how-can-i-create-a-two-dimensional-array-in-javascript
+ jpeg.lossless.Utils.createArray = function (length) {
+ var arr = new Array(length || 0),
+ i = length;
+
+ if (arguments.length > 1) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ while(i--) arr[length-1 - i] = jpeg.lossless.Utils.createArray.apply(this, args);
+ }
+
+ return arr;
+ };
+
+
+ /*** Exports ***/
+
+ var moduleType = typeof module;
+ if ((moduleType !== 'undefined') && module.exports) {
+ module.exports = jpeg.lossless.Utils;
+ }
+
+},{}]},{},[6])(6)
+});
+(function ($, cornerstone, cornerstoneWADOImageLoader) {
+
+ "use strict";
+ function extractUncompressedPixels(dataSet, frame)
+ {
+ var pixelFormat = cornerstoneWADOImageLoader.getPixelFormat(dataSet);
+ var pixelDataElement = dataSet.elements.x7fe00010;
+ var height = dataSet.uint16('x00280010');
+ var width = dataSet.uint16('x00280011');
+ var samplesPerPixel = dataSet.uint16('x00280002');
+ var pixelDataOffset = pixelDataElement.dataOffset;
+ var numPixels = width * height * samplesPerPixel;
+ // Note - we may want to sanity check the rows * columns * bitsAllocated * samplesPerPixel against the buffer size
+
+ var frameOffset = 0;
+ if(pixelFormat === 1) {
+ frameOffset = pixelDataOffset + frame * numPixels;
+ return new Uint8Array(dataSet.byteArray.buffer, frameOffset, numPixels);
+ }
+ else if(pixelFormat === 2) {
+ frameOffset = pixelDataOffset + frame * numPixels * 2;
+ return new Uint16Array(dataSet.byteArray.buffer, frameOffset, numPixels);
+ }
+ else if(pixelFormat === 3) {
+ frameOffset = pixelDataOffset + frame * numPixels * 2;
+ return new Int16Array(dataSet.byteArray.buffer, frameOffset, numPixels);
+ }
+ }
+
+ cornerstoneWADOImageLoader.extractUncompressedPixels = extractUncompressedPixels;
+}($, cornerstone, cornerstoneWADOImageLoader));
+
+(function ($, cornerstone, cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ function loadImage(imageId) {
+ // create a deferred object
+ var deferred = $.Deferred();
+
+ // build a url by parsing out the url scheme and frame index from the imageId
+ var firstColonIndex = imageId.indexOf(':');
+ var url = imageId.substring(firstColonIndex + 1);
+ var frameIndex = url.indexOf('frame=');
+ var frame;
+ if(frameIndex !== -1) {
+ var frameStr = url.substr(frameIndex + 6);
+ frame = parseInt(frameStr);
+ url = url.substr(0, frameIndex-1);
+ }
+
+ // if multiframe and cached, use the cached data set to extract the frame
+ if(frame !== undefined &&
+ cornerstoneWADOImageLoader.internal.multiFrameCacheHack.hasOwnProperty(url))
+ {
+ var dataSet = cornerstoneWADOImageLoader.internal.multiFrameCacheHack[url];
+ var imagePromise = cornerstoneWADOImageLoader.createImageObject(dataSet, imageId, frame);
+ imagePromise.then(function(image) {
+ deferred.resolve(image);
+ }, function(error) {
+ deferred.reject(error);
+ });
+ return deferred;
+ }
+
+ var fileIndex = parseInt(url);
+ var file = cornerstoneWADOImageLoader.fileManager.get(fileIndex);
+ if(file === undefined) {
+ deferred.reject('unknown file index ' + url);
+ return deferred;
+ }
+
+
+ var fileReader = new FileReader();
+ fileReader.onload = function(e) {
+ // Parse the DICOM File
+ var dicomPart10AsArrayBuffer = e.target.result;
+ var byteArray = new Uint8Array(dicomPart10AsArrayBuffer);
+ var dataSet = dicomParser.parseDicom(byteArray);
+
+ // if multiframe, cache the parsed data set to speed up subsequent
+ // requests for the other frames
+ if(frame !== undefined) {
+ var dataSet = cornerstoneWADOImageLoader.internal.multiFrameCacheHack[url];
+ var imagePromise = cornerstoneWADOImageLoader.createImageObject(dataSet, imageId, frame);
+ imagePromise.then(function(image) {
+ deferred.resolve(image);
+ }, function(error) {
+ deferred.reject(error);
+ });
+ return deferred;
+ }
+
+ var imagePromise = cornerstoneWADOImageLoader.createImageObject(dataSet, imageId, frame);
+ imagePromise.then(function(image) {
+ deferred.resolve(image);
+ }, function() {
+ deferred.reject();
+ });
+ };
+ fileReader.readAsArrayBuffer(file);
+
+ return deferred;
+ }
+
+ // registery dicomweb and wadouri image loader prefixes
+ cornerstone.registerImageLoader('dicomfile', loadImage);
+
+}($, cornerstone, cornerstoneWADOImageLoader));
+/**
+ */
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ var files = [];
+
+ function add(file) {
+ var fileIndex = files.push(file);
+ return 'dicomfile:' + (fileIndex - 1);
+ }
+
+ function get(index) {
+ return files[index];
+ }
+
+ function remove(index) {
+ files[index] = undefined;
+ }
+
+ function purge() {
+ files = [];
+ }
+
+ // module exports
+ cornerstoneWADOImageLoader.fileManager = {
+ add : add,
+ get : get,
+ remove:remove,
+ purge: purge
+ };
+
+}(cornerstoneWADOImageLoader));
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ function getMinMax(storedPixelData)
+ {
+ // we always calculate the min max values since they are not always
+ // present in DICOM and we don't want to trust them anyway as cornerstone
+ // depends on us providing reliable values for these
+ var min = 65535;
+ var max = -32768;
+ var numPixels = storedPixelData.length;
+ var pixelData = storedPixelData;
+ for(var index = 0; index < numPixels; index++) {
+ var spv = pixelData[index];
+ // TODO: test to see if it is faster to use conditional here rather than calling min/max functions
+ min = Math.min(min, spv);
+ max = Math.max(max, spv);
+ }
+
+ return {
+ min: min,
+ max: max
+ };
+ }
+
+ // module exports
+ cornerstoneWADOImageLoader.getMinMax = getMinMax;
+
+}(cornerstoneWADOImageLoader));
+
+
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ function getPixelFormat(dataSet) {
+ var pixelRepresentation = dataSet.uint16('x00280103');
+ var bitsAllocated = dataSet.uint16('x00280100');
+ if(pixelRepresentation === 0 && bitsAllocated === 8) {
+ return 1; // unsigned 8 bit
+ } else if(pixelRepresentation === 0 && bitsAllocated === 16) {
+ return 2; // unsigned 16 bit
+ } else if(pixelRepresentation === 1 && bitsAllocated === 16) {
+ return 3; // signed 16 bit data
+ }
+ }
+
+
+ // module exports
+ cornerstoneWADOImageLoader.getPixelFormat = getPixelFormat;
+
+}(cornerstoneWADOImageLoader));
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ function getPixelSpacing(dataSet)
+ {
+ // NOTE - these are not required for all SOP Classes
+ // so we return them as undefined. We also do not
+ // deal with the complexity associated with projection
+ // radiographs here and leave that to a higher layer
+ var pixelSpacing = dataSet.string('x00280030');
+ if(pixelSpacing && pixelSpacing.length > 0) {
+ var split = pixelSpacing.split('\\');
+ return {
+ row: parseFloat(split[0]),
+ column: parseFloat(split[1])
+ };
+ }
+ else {
+ return {
+ row: undefined,
+ column: undefined
+ };
+ }
+ }
+ // module exports
+ cornerstoneWADOImageLoader.getPixelSpacing = getPixelSpacing;
+}(cornerstoneWADOImageLoader));
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ function getRescaleSlopeAndIntercept(dataSet)
+ {
+ // NOTE - we default these to an identity transform since modality LUT
+ // module is not required for all SOP Classes
+ var result = {
+ intercept : 0.0,
+ slope: 1.0
+ };
+
+ if(dataSet.elements.x00281052 && dataSet.elements.x00281053) {
+ result.intercept = dataSet.floatString('x00281052');
+ result.slope = dataSet.floatString('x00281053');
+ }
+
+ return result;
+ }
+
+ // module exports
+ cornerstoneWADOImageLoader.getRescaleSlopeAndIntercept = getRescaleSlopeAndIntercept;
+}(cornerstoneWADOImageLoader));
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ function getWindowWidthAndCenter(dataSet)
+ {
+ // NOTE - Default these to undefined since they may not be present as
+ // they are not present or required for all sop classes. We leave it up
+ // to a higher layer to determine reasonable default values for these
+ // if they are not provided. We also use the first ww/wc values if
+ // there are multiple and again leave it up the higher levels to deal with
+ // this
+ var result = {
+ windowCenter : undefined,
+ windowWidth: undefined
+ };
+
+ if(dataSet.elements.x00281050 && dataSet.elements.x00281051) {
+ result.windowCenter = dataSet.floatString('x00281050');
+ result.windowWidth = dataSet.floatString('x00281051');
+ }
+
+ return result;
+ }
+
+ // module exports
+ cornerstoneWADOImageLoader.getWindowWidthAndCenter = getWindowWidthAndCenter;
+}(cornerstoneWADOImageLoader));
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ var options = {
+ // callback allowing customization of the xhr (e.g. adding custom auth headers, cors, etc)
+ beforeSend : function(xhr) {}
+ };
+
+ function configure(opts) {
+ options = opts;
+ }
+
+ function isColorImage(photoMetricInterpretation)
+ {
+ if(photoMetricInterpretation === "RGB" ||
+ photoMetricInterpretation === "PALETTE COLOR" ||
+ photoMetricInterpretation === "YBR_FULL" ||
+ photoMetricInterpretation === "YBR_FULL_422" ||
+ photoMetricInterpretation === "YBR_PARTIAL_422" ||
+ photoMetricInterpretation === "YBR_PARTIAL_420" ||
+ photoMetricInterpretation === "YBR_RCT" ||
+ photoMetricInterpretation === "YBR_ICT")
+ {
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ cornerstoneWADOImageLoader.isColorImage = isColorImage;
+
+}(cornerstoneWADOImageLoader));
+(function ($, cornerstone, cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ var canvas = document.createElement('canvas');
+ var lastImageIdDrawn = "";
+
+ function extractStoredPixels(dataSet, frame) {
+
+ // special case for JPEG Baseline 8 bit
+ if(cornerstoneWADOImageLoader.isJPEGBaseline8Bit(dataSet) === true)
+ {
+ return cornerstoneWADOImageLoader.decodeJPEGBaseline8Bit(canvas, dataSet, frame);
+ }
+
+ var decodedImageFrame = cornerstoneWADOImageLoader.decodeTransferSyntax(dataSet, frame);
+
+ return cornerstoneWADOImageLoader.convertColorSpace(canvas, dataSet, decodedImageFrame);
+ }
+
+ function makeColorImage(imageId, dataSet, frame) {
+
+ // extract the DICOM attributes we need
+ var pixelSpacing = cornerstoneWADOImageLoader.getPixelSpacing(dataSet);
+ var rows = dataSet.uint16('x00280010');
+ var columns = dataSet.uint16('x00280011');
+ var rescaleSlopeAndIntercept = cornerstoneWADOImageLoader.getRescaleSlopeAndIntercept(dataSet);
+ var bytesPerPixel = 4;
+ var numPixels = rows * columns;
+ var sizeInBytes = numPixels * bytesPerPixel;
+ var windowWidthAndCenter = cornerstoneWADOImageLoader.getWindowWidthAndCenter(dataSet);
+
+ // clear the lastImageIdDrawn so we update the canvas
+ lastImageIdDrawn = undefined;
+
+ var deferred = $.Deferred();
+
+ // Decompress and decode the pixel data for this image
+ var imageDataPromise;
+ try {
+ imageDataPromise = extractStoredPixels(dataSet, frame);
+ }
+ catch(err) {
+ deferred.reject(err);
+ return deferred;
+ }
+
+ imageDataPromise.then(function(imageData) {
+ function getPixelData() {
+ return imageData.data;
+ }
+
+ function getImageData() {
+ return imageData;
+ }
+
+ function getCanvas() {
+ if(lastImageIdDrawn === imageId) {
+ return canvas;
+ }
+
+ canvas.height = rows;
+ canvas.width = columns;
+ var context = canvas.getContext('2d');
+ context.putImageData(imageData, 0, 0 );
+ lastImageIdDrawn = imageId;
+ return canvas;
+ }
+
+ // Extract the various attributes we need
+ var image = {
+ imageId : imageId,
+ minPixelValue : 0,
+ maxPixelValue : 255,
+ slope: rescaleSlopeAndIntercept.slope,
+ intercept: rescaleSlopeAndIntercept.intercept,
+ windowCenter : windowWidthAndCenter.windowCenter,
+ windowWidth : windowWidthAndCenter.windowWidth,
+ render: cornerstone.renderColorImage,
+ getPixelData: getPixelData,
+ getImageData: getImageData,
+ getCanvas: getCanvas,
+ rows: rows,
+ columns: columns,
+ height: rows,
+ width: columns,
+ color: true,
+ columnPixelSpacing: pixelSpacing.column,
+ rowPixelSpacing: pixelSpacing.row,
+ data: dataSet,
+ invert: false,
+ sizeInBytes: sizeInBytes
+ };
+
+ if(image.windowCenter === undefined) {
+ image.windowWidth = 255;
+ image.windowCenter = 128;
+ }
+ deferred.resolve(image);
+ }, function(error) {
+ deferred.reject(error);
+ });
+
+ return deferred;
+ }
+
+ // module exports
+ cornerstoneWADOImageLoader.makeColorImage = makeColorImage;
+}($, cornerstone, cornerstoneWADOImageLoader));
+(function ($, cornerstone, cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ function getBytesPerPixel(dataSet)
+ {
+ var pixelFormat = cornerstoneWADOImageLoader.getPixelFormat(dataSet);
+ if(pixelFormat ===1) {
+ return 1;
+ }
+ else if(pixelFormat ===2 || pixelFormat ===3){
+ return 2;
+ }
+ throw "unknown pixel format";
+ }
+
+ function getLUT(image, pixelRepresentation, lutDataSet) {
+ var numLUTEntries = lutDataSet.uint16('x00283002', 0);
+ if(numLUTEntries === 0) {
+ numLUTEntries = 65535;
+ }
+ var firstValueMapped = 0;
+ if(pixelRepresentation === 0) {
+ firstValueMapped = lutDataSet.uint16('x00283002', 1);
+ } else {
+ firstValueMapped = lutDataSet.int16('x00283002', 1);
+ }
+ var numBitsPerEntry = lutDataSet.uint16('x00283002', 2);
+ //console.log('LUT(', numLUTEntries, ',', firstValueMapped, ',', numBitsPerEntry, ')');
+ var lut = {
+ id : '1',
+ firstValueMapped: firstValueMapped,
+ numBitsPerEntry : numBitsPerEntry,
+ lut : []
+ };
+
+ //console.log("minValue=", minValue, "; maxValue=", maxValue);
+ for (var i = 0; i < numLUTEntries; i++) {
+ if(pixelRepresentation === 0) {
+ lut.lut[i] = lutDataSet.uint16('x00283006', i);
+ } else {
+ lut.lut[i] = lutDataSet.int16('x00283006', i);
+ }
+ }
+ return lut;
+ }
+
+ function makeGrayscaleImage(imageId, dataSet, frame) {
+ var deferred = $.Deferred();
+
+ // extract the DICOM attributes we need
+ var pixelSpacing = cornerstoneWADOImageLoader.getPixelSpacing(dataSet);
+ var rows = dataSet.uint16('x00280010');
+ var columns = dataSet.uint16('x00280011');
+ var rescaleSlopeAndIntercept = cornerstoneWADOImageLoader.getRescaleSlopeAndIntercept(dataSet);
+
+ var bytesPerPixel;
+ try {
+ bytesPerPixel = getBytesPerPixel(dataSet);
+ } catch(error) {
+ deferred.reject(error);
+ return deferred;
+ }
+
+ var numPixels = rows * columns;
+ var sizeInBytes = numPixels * bytesPerPixel;
+ var photometricInterpretation = dataSet.string('x00280004');
+ var invert = (photometricInterpretation === "MONOCHROME1");
+ var windowWidthAndCenter = cornerstoneWADOImageLoader.getWindowWidthAndCenter(dataSet);
+
+ // Decompress and decode the pixel data for this image
+ var storedPixelData;
+ try {
+ storedPixelData = cornerstoneWADOImageLoader.decodeTransferSyntax(dataSet, frame);
+ }
+ catch(err) {
+ deferred.reject(err);
+ return deferred;
+ }
+
+ var minMax = cornerstoneWADOImageLoader.getMinMax(storedPixelData);
+
+ function getPixelData() {
+ return storedPixelData;
+ }
+
+
+ // Extract the various attributes we need
+ var image = {
+ imageId : imageId,
+ minPixelValue : minMax.min,
+ maxPixelValue : minMax.max,
+ slope: rescaleSlopeAndIntercept.slope,
+ intercept: rescaleSlopeAndIntercept.intercept,
+ windowCenter : windowWidthAndCenter.windowCenter,
+ windowWidth : windowWidthAndCenter.windowWidth,
+ render: cornerstone.renderGrayscaleImage,
+ getPixelData: getPixelData,
+ rows: rows,
+ columns: columns,
+ height: rows,
+ width: columns,
+ color: false,
+ columnPixelSpacing: pixelSpacing.column,
+ rowPixelSpacing: pixelSpacing.row,
+ data: dataSet,
+ invert: invert,
+ sizeInBytes: sizeInBytes
+ };
+
+ // modality LUT
+ var pixelRepresentation = dataSet.uint16('x00280103');
+ if(dataSet.elements.x00283000) {
+ image.modalityLUT = getLUT(image, pixelRepresentation, dataSet.elements.x00283000.items[0].dataSet);
+ }
+
+ // VOI LUT
+ if(dataSet.elements.x00283010) {
+ pixelRepresentation = 0;
+ // if modality LUT can produce negative values, the data is signed
+ if(image.minPixelValue * image.slope + image.intercept < 0) {
+ pixelRepresentation = 1;
+ }
+ image.voiLUT = getLUT(image, pixelRepresentation, dataSet.elements.x00283010.items[0].dataSet);
+ }
+
+ // TODO: deal with pixel padding and all of the various issues by setting it to min pixel value (or lower)
+ // TODO: Mask out overlays embedded in pixel data above high bit
+
+ if(image.windowCenter === undefined) {
+ var maxVoi = image.maxPixelValue * image.slope + image.intercept;
+ var minVoi = image.minPixelValue * image.slope + image.intercept;
+ image.windowWidth = maxVoi - minVoi;
+ image.windowCenter = (maxVoi + minVoi) / 2;
+ }
+
+ deferred.resolve(image);
+ return deferred;
+ }
+
+ // module exports
+ cornerstoneWADOImageLoader.makeGrayscaleImage = makeGrayscaleImage;
+}($, cornerstone, cornerstoneWADOImageLoader));
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ // module exports
+ cornerstoneWADOImageLoader.version = '0.7.2';
+
+}(cornerstoneWADOImageLoader));
+(function (cornerstoneWADOImageLoader) {
+
+ function checkToken(token, data, dataOffset) {
+
+ if(dataOffset + token.length > data.length) {
+ //console.log('dataOffset >> ', dataOffset);
+ return false;
+ }
+
+ var endIndex = dataOffset;
+
+ 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;
+ }
+
+ function stringToUint8Array(str) {
+ var uint=new Uint8Array(str.length);
+ for(var i=0,j=str.length;i '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;
+ return metaHeaderDataSet;
+ }
+
+ 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(littleEndianByteStream.byteArray, transferSyntaxElement.dataOffset, transferSyntaxElement.length);
+ }
+
+ 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;
+ }
+
+ function getDataSetByteStream(transferSyntax) {
+ if(transferSyntax === '1.2.840.10008.1.2.2') // explicit big endian
+ {
+ return new dicomParser.ByteStream(dicomParser.bigEndianByteArrayParser, byteArray, littleEndianByteStream.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, littleEndianByteStream.position);
+ }
+ }
+
+ 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();
+ };
+
+ return dicomParser;
+ })(dicomParser);
+
+ var dicomParser = (function (dicomParser) {
+ "use strict";
+
+ if (dicomParser === undefined) {
+ dicomParser = {};
+ }
+
+ /**
+ * converts an explicit dataSet to a javascript object
+ * @param dataSet
+ * @param options
+ */
+ 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(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 dicomParser;
+ }(dicomParser));
+ var dicomParser = (function (dicomParser) {
+ "use strict";
+
+ if (dicomParser === undefined) {
+ dicomParser = {};
+ }
+
+ /**
+ * Converts an explicit VR element to a string or undefined if it is not possible to convert.
+ * Throws an error if an implicit element is supplied
+ * @param dataSet
+ * @param element
+ * @returns {*}
+ */
+ 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();
+ }
+ 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 (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 textResult;
+ };
+ return dicomParser;
+ }(dicomParser));
+ /**
+ * Utility functions for dealing with DICOM
+ */
+
+ var dicomParser = (function (dicomParser)
+ {
+ "use strict";
+
+ 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
+ }
+ }
+
+ 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
+ */
+
+ 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 {
+ hours: hh,
+ minutes: mm,
+ seconds: ss,
+ fractionalSeconds: ffffff
+ };
+ }
+
+ if(validate) {
+ throw "invalid TM '" + time + "'";
+ }
+
+ return undefined;
+ };
+
+ return dicomParser;
+ }(dicomParser));
+ /**
+ * Utility functions for dealing with DICOM
+ */
+
+ var dicomParser = (function (dicomParser)
+ {
+ "use strict";
+
+ if(dicomParser === undefined)
+ {
+ 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
+ };
+
+ /**
+ * Tests to see if vr is a string or not.
+ * @param vr
+ * @returns true if string, false it not string, undefined if unknown vr or UN type
+ */
+ dicomParser.isStringVr = function(vr)
+ {
+ return stringVrs[vr];
+ };
+
+ /**
+ * Tests to see if a given tag in the format xggggeeee is a private tag or not
+ * @param tag
+ * @returns {boolean}
+ */
+ dicomParser.isPrivateTag = function(tag)
+ {
+ var lastGroupDigit = parseInt(tag[4]);
+ var groupIsOdd = (lastGroupDigit % 2) === 1;
+ return groupIsOdd;
+ };
+
+ /**
+ * Parses a PN formatted string into a javascript object with properties for givenName, familyName, middleName, prefix and suffix
+ * @param personName a string in the PN VR format
+ * @param index
+ * @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]
+ };
+ };
+
+
+
+ return dicomParser;
+ }(dicomParser));
+ /**
+ * Functionality for extracting encapsulated pixel data
+ */
+
+ var dicomParser = (function (dicomParser)
+ {
+ "use strict";
+
+ if(dicomParser === undefined)
+ {
+ 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++];
+ }
+ }
+
+ 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;
+ }
+ 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;
+ }
+
+ 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";
+ }
+
+ // move to the start of this frame
+ var frameOffset = pixelDataElement.basicOffsetTable[frame];
+ 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;
+ }
+
+ // 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';
+ }
+
+ // read this frame
+ var endOfFrame = byteStream.position + pixelDataElement.length;
+ var pixelData = readFragmentsUntil(byteStream, endOfFrame);
+ return pixelData;
+ }
+
+ /**
+ * Returns the pixel data for the specified frame in an encapsulated pixel data element
+ *
+ * @param dataSet - the dataSet containing the encapsulated pixel data
+ * @param pixelDataElement - the pixel data element (x7fe00010) to extract the frame from
+ * @param frame - the zero based frame index
+ * @returns Uint8Array with the encapsulated pixel data
+ */
+ 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";
+ }
+
+ // 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);
+ }
+ };
+
+ return dicomParser;
+ }(dicomParser));
+
+ /**
+ * Internal helper functions for parsing different types from a big-endian byte array
+ */
+
+ var dicomParser = (function (dicomParser)
+ {
+ "use strict";
+
+ if(dicomParser === undefined)
+ {
+ 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';
+ }
+ 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
+ */
+
+ var dicomParser = (function (dicomParser)
+ {
+ "use strict";
+
+ if(dicomParser === undefined)
+ {
+ dicomParser = {};
+ }
+
+ /**
+ * Reads a string of 8-bit characters from an array of bytes and advances
+ * the position by length bytes. A null terminator will end the string
+ * but will not effect advancement of the position. Trailing and leading
+ * spaces are preserved (not trimmed)
+ * @param byteArray the byteArray to read from
+ * @param position the position in the byte array to read from
+ * @param length the maximum number of bytes to parse
+ * @returns {string} the parsed string
+ * @throws error if buffer overread would occur
+ * @access private
+ */
+ 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;
+ }
+ 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)
+ {
+ "use strict";
+
+ if(dicomParser === undefined)
+ {
+ dicomParser = {};
+ }
+
+ /**
+ * Constructor for ByteStream objects.
+ * @param byteArrayParser a parser for parsing the byte array
+ * @param byteArray a Uint8Array containing the byte stream
+ * @param position (optional) the position to start reading from. 0 if not specified
+ * @constructor
+ * @throws will throw an error if the byteArrayParser parameter is not present
+ * @throws will throw an error if the byteArray parameter is not present or invalid
+ * @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";
+
+ }
+ this.byteArrayParser = byteArrayParser;
+ this.byteArray = byteArray;
+ this.position = position ? position : 0;
+ this.warnings = []; // array of string warnings encountered while parsing
+ };
+
+ /**
+ * Safely seeks through the byte stream. Will throw an exception if an attempt
+ * is made to seek outside of the byte array.
+ * @param offset the number of bytes to add to the position
+ * @throws error if seek would cause position to be outside of the byteArray
+ */
+ dicomParser.ByteStream.prototype.seek = function(offset)
+ {
+ if(this.position + offset < 0)
+ {
+ throw "cannot seek to position < 0";
+ }
+ this.position += offset;
+ };
+
+ /**
+ * Returns a new ByteStream object from the current position and of the requested number of bytes
+ * @param numBytes the length of the byte array for the ByteStream to contain
+ * @returns {dicomParser.ByteStream}
+ * @throws error if buffer overread would occur
+ */
+ 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);
+ };
+
+ /**
+ *
+ * Parses an unsigned int 16 from a byte array and advances
+ * the position by 2 bytes
+ *
+ * @returns {*} the parsed unsigned int 16
+ * @throws error if buffer overread would occur
+ */
+ dicomParser.ByteStream.prototype.readUint16 = function()
+ {
+ var result = this.byteArrayParser.readUint16(this.byteArray, this.position);
+ this.position += 2;
+ return result;
+ };
+
+ /**
+ * Parses an unsigned int 32 from a byte array and advances
+ * the position by 2 bytes
+ *
+ * @returns {*} the parse unsigned int 32
+ * @throws error if buffer overread would occur
+ */
+ dicomParser.ByteStream.prototype.readUint32 = function()
+ {
+ var result = this.byteArrayParser.readUint32(this.byteArray, this.position);
+ this.position += 4;
+ return result;
+ };
+
+ /**
+ * Reads a string of 8-bit characters from an array of bytes and advances
+ * the position by length bytes. A null terminator will end the string
+ * but will not effect advancement of the position.
+ * @param length the maximum number of bytes to parse
+ * @returns {string} the parsed string
+ * @throws error if buffer overread would occur
+ */
+ dicomParser.ByteStream.prototype.readFixedString = function(length)
+ {
+ 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)
+ {
+ "use strict";
+
+ if(dicomParser === undefined)
+ {
+ dicomParser = {};
+ }
+
+ function getByteArrayParser(element, defaultParser)
+ {
+ return (element.parser !== undefined ? element.parser : defaultParser);
+ }
+
+ /**
+ * Constructs a new DataSet given byteArray and collection of elements
+ * @param byteArrayParser
+ * @param byteArray
+ * @param elements
+ * @constructor
+ */
+ dicomParser.DataSet = function(byteArrayParser, byteArray, elements)
+ {
+ this.byteArrayParser = byteArrayParser;
+ this.byteArray = byteArray;
+ this.elements = elements;
+ };
+
+ /**
+ * Finds the element for tag and returns an unsigned int 16 if it exists and has data
+ * @param tag The DICOM tag in the format xGGGGEEEE
+ * @param index the index of the value in a multivalued element. Default is index 0 if not supplied
+ * @returns {*} unsigned int 16 or undefined if the attribute is not present or has data of length 0
+ */
+ 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;
+ };
+
+ /**
+ * Finds the element for tag and returns an signed int 16 if it exists and has data
+ * @param tag The DICOM tag in the format xGGGGEEEE
+ * @param index the index of the value in a multivalued element. Default is index 0 if not supplied
+ * @returns {*} signed int 16 or undefined if the attribute is not present or has data of length 0
+ */
+ 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;
+ };
+
+ /**
+ * Finds the element for tag and returns an unsigned int 32 if it exists and has data
+ * @param tag The DICOM tag in the format xGGGGEEEE
+ * @param index the index of the value in a multivalued element. Default is index 0 if not supplied
+ * @returns {*} unsigned int 32 or undefined if the attribute is not present or has data of length 0
+ */
+ 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;
+ };
+
+ /**
+ * Finds the element for tag and returns an signed int 32 if it exists and has data
+ * @param tag The DICOM tag in the format xGGGGEEEE
+ * @param index the index of the value in a multivalued element. Default is index 0 if not supplied
+ * @returns {*} signed int 32 or undefined if the attribute is not present or has data of length 0
+ */
+ 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;
+ };
+
+ /**
+ * Finds the element for tag and returns a 32 bit floating point number (VR=FL) if it exists and has data
+ * @param tag The DICOM tag in the format xGGGGEEEE
+ * @param index the index of the value in a multivalued element. Default is index 0 if not supplied
+ * @returns {*} float or undefined if the attribute is not present or has data of length 0
+ */
+ 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;
+ };
+
+ /**
+ * Finds the element for tag and returns a 64 bit floating point number (VR=FD) if it exists and has data
+ * @param tag The DICOM tag in the format xGGGGEEEE
+ * @param index the index of the value in a multivalued element. Default is index 0 if not supplied
+ * @returns {*} float or undefined if the attribute is not present or doesn't has data of length 0
+ */
+ 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;
+ };
+
+ /**
+ * Returns the number of string values for the element
+ * @param tag The DICOM tag in the format xGGGGEEEE
+ * @returns {*} the number of string values or undefined if the attribute is not present or has zero length data
+ */
+ 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)
+ {
+ return 1;
+ }
+ return numMatching.length + 1;
+ }
+ return undefined;
+ };
+
+ /**
+ * Returns a string for the element. If index is provided, the element is assumed to be
+ * multi-valued and will return the component specified by index. Undefined is returned
+ * if there is no component with the specified index, the element does not exist or is zero length.
+ *
+ * Use this function for VR types of AE, CS, SH and LO
+ *
+ * @param tag The DICOM tag in the format xGGGGEEEE
+ * @param index the index of the desired value in a multi valued string or undefined for the entire string
+ * @returns {*}
+ */
+ 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 values = fixedString.split('\\');
+ // trim trailing spaces
+ return values[index].trim();
+ }
+ else
+ {
+ // trim trailing spaces
+ return fixedString.trim();
+ }
+ }
+ return undefined;
+ };
+
+ /**
+ * Returns a string with the leading spaces preserved and trailing spaces removed.
+ *
+ * Use this function to access data for VRs of type UT, ST and LT
+ *
+ * @param tag
+ * @param index
+ * @returns {*}
+ */
+ 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 values = fixedString.split('\\');
+ return values[index].replace(/ +$/, '');
+ }
+ else
+ {
+ return fixedString.replace(/ +$/, '');
+ }
+ }
+ return undefined;
+ };
+
+ /**
+ * Parses a string to a float for the specified index in a multi-valued element. If index is not specified,
+ * the first value in a multi-valued VR will be parsed if present.
+ * @param tag The DICOM tag in the format xGGGGEEEE
+ * @param index the index of the desired value in a multi valued string or undefined for the first value
+ * @returns {*} a floating point number or undefined if not present or data not long enough
+ */
+ 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);
+ }
+ }
+ return undefined;
+ };
+
+ /**
+ * Parses a string to an integer for the specified index in a multi-valued element. If index is not specified,
+ * the first value in a multi-valued VR will be parsed if present.
+ * @param tag The DICOM tag in the format xGGGGEEEE
+ * @param index the index of the desired value in a multi valued string or undefined for the first value
+ * @returns {*} an integer or undefined if not present or data not long enough
+ */
+ 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);
+ }
+ }
+ return undefined;
+ };
+
+ //dicomParser.DataSet = DataSet;
+
+ return dicomParser;
+ }(dicomParser));
+ /**
+ * Internal helper functions for parsing DICOM elements
+ */
+
+ var dicomParser = (function (dicomParser)
+ {
+ "use strict";
+
+ if(dicomParser === undefined)
+ {
+ dicomParser = {};
+ }
+
+ /**
+ * Reads an encapsulated pixel data element and adds an array of fragments to the element
+ * containing the offset and length of each fragment and any offsets from the basic offset
+ * table
+ * @param byteStream
+ * @param element
+ */
+ 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')
+ {
+ 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
+ */
+
+ var dicomParser = (function (dicomParser)
+ {
+ "use strict";
+
+ if(dicomParser === undefined)
+ {
+ dicomParser = {};
+ }
+
+ /**
+ * reads from the byte stream until it finds the magic numbers for the item delimitation item
+ * and then sets the length of the element
+ * @param byteStream
+ * @param element
+ */
+ 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)
+ {
+ 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
+ */
+
+ var dicomParser = (function (dicomParser)
+ {
+ "use strict";
+
+ if(dicomParser === undefined)
+ {
+ 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';
+ }
+ 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
+ */
+
+ var dicomParser = (function (dicomParser)
+ {
+ "use strict";
+
+ if(dicomParser === undefined)
+ {
+ dicomParser = {};
+ }
+
+ /**
+ * reads an explicit data set
+ * @param byteStream the byte stream to read from
+ * @param maxPosition the maximum position to read up to (optional - only needed when reading sequence items)
+ */
+ dicomParser.parseDicomDataSetExplicit = function (dataSet, byteStream, maxPosition, 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.position > maxPosition) {
+ throw "dicomParser:parseDicomDataSetExplicit: buffer overrun";
+ }
+ };
+
+ /**
+ * reads an implicit data set
+ * @param byteStream the byte stream to read from
+ * @param maxPosition the maximum position to read up to (optional - only needed when reading sequence items)
+ */
+ dicomParser.parseDicomDataSetImplicit = function(dataSet, byteStream, maxPosition, 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;
+ }
+ }
+ };
+
+ return dicomParser;
+ }(dicomParser));
+
+ /**
+ * Internal helper functions for for parsing DICOM elements
+ */
+
+ var dicomParser = (function (dicomParser)
+ {
+ "use strict";
+
+ if(dicomParser === undefined)
+ {
+ dicomParser = {};
+ }
+
+ function getDataLengthSizeInBytesForVR(vr)
+ {
+ 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;
+ }
+ }
+
+ byteStream.seek(element.length);
+ return element;
+ };
+
+ return dicomParser;
+ }(dicomParser));
+ /**
+ * Internal helper functions for for parsing DICOM elements
+ */
+
+ var dicomParser = (function (dicomParser)
+ {
+ "use strict";
+
+ if(dicomParser === undefined)
+ {
+ 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 (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;
+ };
+
+
+ return dicomParser;
+ }(dicomParser));
+ /**
+ * Internal helper functions for parsing DICOM elements
+ */
+
+ var dicomParser = (function (dicomParser)
+ {
+ "use strict";
+
+ if(dicomParser === undefined)
+ {
+ dicomParser = {};
+ }
+
+ function readDicomDataSetExplicitUndefinedLength(byteStream, warnings)
+ {
+ 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')
+ {
+ 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);
+
+ 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;
+ }
+
+ 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);
+ }
+ }
+
+ 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'";
+ }
+
+ element.items = [];
+
+ if(element.length === 4294967295)
+ {
+ readSQElementUndefinedLengthExplicit(byteStream, element);
+ }
+ else
+ {
+ readSQElementKnownLengthExplicit(byteStream, element, warnings);
+ }
+ };
+
+
+ return dicomParser;
+ }(dicomParser));
+ /**
+ * Internal helper functions for parsing DICOM elements
+ */
+
+ var dicomParser = (function (dicomParser)
+ {
+ "use strict";
+
+ if(dicomParser === undefined)
+ {
+ dicomParser = {};
+ }
+
+ function readDicomDataSetImplicitUndefinedLength(byteStream)
+ {
+ 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')
+ {
+ 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);
+
+ 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;
+ }
+
+ 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);
+ }
+ }
+
+ /**
+ * Reads sequence items for an element in an implicit little endian byte stream
+ * @param byteStream the implicit little endian byte stream
+ * @param element the element to read the sequence items for
+ */
+ 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'";
+ }
+
+ element.items = [];
+
+ if(element.length === 4294967295)
+ {
+ readSQElementUndefinedLengthImplicit(byteStream, element);
+ }
+ else
+ {
+ readSQElementKnownLengthImplicit(byteStream, element);
+ }
+ };
+
+ return dicomParser;
+ }(dicomParser));
+ /**
+ * Internal helper functions for parsing DICOM elements
+ */
+
+ var dicomParser = (function (dicomParser)
+ {
+ "use strict";
+
+ if(dicomParser === undefined)
+ {
+ dicomParser = {};
+ }
+
+ /**
+ * Reads the tag and length of a sequence item and returns them as an object with the following properties
+ * tag : string for the tag of this element in the format xggggeeee
+ * length: the number of bytes in this item or 4294967295 if undefined
+ * dataOffset: the offset into the byteStream of the data for this item
+ * @param byteStream the byte
+ * @returns {{tag: string, length: integer, dataOffset: integer}}
+ */
+ dicomParser.readSequenceItem = function(byteStream)
+ {
+ if(byteStream === undefined)
+ {
+ throw "dicomParser.readSequenceItem: missing required parameter 'byteStream'";
+ }
+
+ var element = {
+ tag : dicomParser.readTag(byteStream),
+ length : byteStream.readUint32(),
+ dataOffset : byteStream.position
+ };
+
+ return element;
+ };
+
+
+ return dicomParser;
+ }(dicomParser));
+ /**
+ * Internal helper functions for parsing DICOM elements
+ */
+
+ var dicomParser = (function (dicomParser)
+ {
+ "use strict";
+
+ if(dicomParser === undefined)
+ {
+ dicomParser = {};
+ }
+
+ /**
+ * Reads a tag (group number and element number) from a byteStream
+ * @param byteStream the byte stream to read from
+ * @returns {string} the tag in format xggggeeee where gggg is the lowercase hex value of the group number
+ * and eeee is the lower case hex value of the element number
+ */
+ dicomParser.readTag = function(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;
+ };
+
+ return dicomParser;
+ }(dicomParser));
+ /**
+ * Version
+ */
+
+ var dicomParser = (function (dicomParser)
+ {
+ "use strict";
+
+ if(dicomParser === undefined)
+ {
+ dicomParser = {};
+ }
+
+ dicomParser.version = "1.1.5";
+
+ return dicomParser;
+ }(dicomParser));
+ return dicomParser;
+}));
\ No newline at end of file
diff --git a/Packages/cornerstone/client/hammer.js b/Packages/cornerstone/client/hammer.js
new file mode 100644
index 000000000..734e9205e
--- /dev/null
+++ b/Packages/cornerstone/client/hammer.js
@@ -0,0 +1,2495 @@
+/*! Hammer.JS - v2.0.4 - 2015-09-25
+ * http://hammerjs.github.io/
+ *
+ * Copyright (c) 2015 Jorik Tangelder;
+ * Licensed under the license */
+(function(window, document, exportName, undefined) {
+ 'use strict';
+
+var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o'];
+var TEST_ELEMENT = document.createElement('div');
+
+var TYPE_FUNCTION = 'function';
+
+var round = Math.round;
+var abs = Math.abs;
+var now = Date.now;
+
+/**
+ * set a timeout with a given scope
+ * @param {Function} fn
+ * @param {Number} timeout
+ * @param {Object} context
+ * @returns {number}
+ */
+function setTimeoutContext(fn, timeout, context) {
+ return setTimeout(bindFn(fn, context), timeout);
+}
+
+/**
+ * if the argument is an array, we want to execute the fn on each entry
+ * if it aint an array we don't want to do a thing.
+ * this is used by all the methods that accept a single and array argument.
+ * @param {*|Array} arg
+ * @param {String} fn
+ * @param {Object} [context]
+ * @returns {Boolean}
+ */
+function invokeArrayArg(arg, fn, context) {
+ if (Array.isArray(arg)) {
+ each(arg, context[fn], context);
+ return true;
+ }
+ return false;
+}
+
+/**
+ * walk objects and arrays
+ * @param {Object} obj
+ * @param {Function} iterator
+ * @param {Object} context
+ */
+function each(obj, iterator, context) {
+ var i;
+
+ if (!obj) {
+ return;
+ }
+
+ if (obj.forEach) {
+ obj.forEach(iterator, context);
+ } else if (obj.length !== undefined) {
+ i = 0;
+ while (i < obj.length) {
+ iterator.call(context, obj[i], i, obj);
+ i++;
+ }
+ } else {
+ for (i in obj) {
+ obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj);
+ }
+ }
+}
+
+/**
+ * extend object.
+ * means that properties in dest will be overwritten by the ones in src.
+ * @param {Object} dest
+ * @param {Object} src
+ * @param {Boolean} [merge]
+ * @returns {Object} dest
+ */
+function extend(dest, src, merge) {
+ var keys = Object.keys(src);
+ var i = 0;
+ while (i < keys.length) {
+ if (!merge || (merge && dest[keys[i]] === undefined)) {
+ dest[keys[i]] = src[keys[i]];
+ }
+ i++;
+ }
+ return dest;
+}
+
+/**
+ * merge the values from src in the dest.
+ * means that properties that exist in dest will not be overwritten by src
+ * @param {Object} dest
+ * @param {Object} src
+ * @returns {Object} dest
+ */
+function merge(dest, src) {
+ return extend(dest, src, true);
+}
+
+/**
+ * simple class inheritance
+ * @param {Function} child
+ * @param {Function} base
+ * @param {Object} [properties]
+ */
+function inherit(child, base, properties) {
+ var baseP = base.prototype,
+ childP;
+
+ childP = child.prototype = Object.create(baseP);
+ childP.constructor = child;
+ childP._super = baseP;
+
+ if (properties) {
+ extend(childP, properties);
+ }
+}
+
+/**
+ * simple function bind
+ * @param {Function} fn
+ * @param {Object} context
+ * @returns {Function}
+ */
+function bindFn(fn, context) {
+ return function boundFn() {
+ return fn.apply(context, arguments);
+ };
+}
+
+/**
+ * let a boolean value also be a function that must return a boolean
+ * this first item in args will be used as the context
+ * @param {Boolean|Function} val
+ * @param {Array} [args]
+ * @returns {Boolean}
+ */
+function boolOrFn(val, args) {
+ if (typeof val == TYPE_FUNCTION) {
+ return val.apply(args ? args[0] || undefined : undefined, args);
+ }
+ return val;
+}
+
+/**
+ * use the val2 when val1 is undefined
+ * @param {*} val1
+ * @param {*} val2
+ * @returns {*}
+ */
+function ifUndefined(val1, val2) {
+ return (val1 === undefined) ? val2 : val1;
+}
+
+/**
+ * addEventListener with multiple events at once
+ * @param {EventTarget} target
+ * @param {String} types
+ * @param {Function} handler
+ */
+function addEventListeners(target, types, handler) {
+ each(splitStr(types), function(type) {
+ target.addEventListener(type, handler, false);
+ });
+}
+
+/**
+ * removeEventListener with multiple events at once
+ * @param {EventTarget} target
+ * @param {String} types
+ * @param {Function} handler
+ */
+function removeEventListeners(target, types, handler) {
+ each(splitStr(types), function(type) {
+ target.removeEventListener(type, handler, false);
+ });
+}
+
+/**
+ * find if a node is in the given parent
+ * @method hasParent
+ * @param {HTMLElement} node
+ * @param {HTMLElement} parent
+ * @return {Boolean} found
+ */
+function hasParent(node, parent) {
+ while (node) {
+ if (node == parent) {
+ return true;
+ }
+ node = node.parentNode;
+ }
+ return false;
+}
+
+/**
+ * small indexOf wrapper
+ * @param {String} str
+ * @param {String} find
+ * @returns {Boolean} found
+ */
+function inStr(str, find) {
+ return str.indexOf(find) > -1;
+}
+
+/**
+ * split string on whitespace
+ * @param {String} str
+ * @returns {Array} words
+ */
+function splitStr(str) {
+ return str.trim().split(/\s+/g);
+}
+
+/**
+ * find if a array contains the object using indexOf or a simple polyFill
+ * @param {Array} src
+ * @param {String} find
+ * @param {String} [findByKey]
+ * @return {Boolean|Number} false when not found, or the index
+ */
+function inArray(src, find, findByKey) {
+ if (src.indexOf && !findByKey) {
+ return src.indexOf(find);
+ } else {
+ var i = 0;
+ while (i < src.length) {
+ if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) {
+ return i;
+ }
+ i++;
+ }
+ return -1;
+ }
+}
+
+/**
+ * convert array-like objects to real arrays
+ * @param {Object} obj
+ * @returns {Array}
+ */
+function toArray(obj) {
+ return Array.prototype.slice.call(obj, 0);
+}
+
+/**
+ * unique array with objects based on a key (like 'id') or just by the array's value
+ * @param {Array} src [{id:1},{id:2},{id:1}]
+ * @param {String} [key]
+ * @param {Boolean} [sort=False]
+ * @returns {Array} [{id:1},{id:2}]
+ */
+function uniqueArray(src, key, sort) {
+ var results = [];
+ var values = [];
+ var i = 0;
+
+ while (i < src.length) {
+ var val = key ? src[i][key] : src[i];
+ if (inArray(values, val) < 0) {
+ results.push(src[i]);
+ }
+ values[i] = val;
+ i++;
+ }
+
+ if (sort) {
+ if (!key) {
+ results = results.sort();
+ } else {
+ results = results.sort(function sortUniqueArray(a, b) {
+ return a[key] > b[key];
+ });
+ }
+ }
+
+ return results;
+}
+
+/**
+ * get the prefixed property
+ * @param {Object} obj
+ * @param {String} property
+ * @returns {String|Undefined} prefixed
+ */
+function prefixed(obj, property) {
+ var prefix, prop;
+ var camelProp = property[0].toUpperCase() + property.slice(1);
+
+ var i = 0;
+ while (i < VENDOR_PREFIXES.length) {
+ prefix = VENDOR_PREFIXES[i];
+ prop = (prefix) ? prefix + camelProp : property;
+
+ if (prop in obj) {
+ return prop;
+ }
+ i++;
+ }
+ return undefined;
+}
+
+/**
+ * get a unique id
+ * @returns {number} uniqueId
+ */
+var _uniqueId = 1;
+function uniqueId() {
+ return _uniqueId++;
+}
+
+/**
+ * get the window object of an element
+ * @param {HTMLElement} element
+ * @returns {DocumentView|Window}
+ */
+function getWindowForElement(element) {
+ var doc = element.ownerDocument || element;
+ return (doc.defaultView || doc.parentWindow || window);
+}
+
+var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
+
+var SUPPORT_TOUCH = ('ontouchstart' in window);
+var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined;
+var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent);
+
+var INPUT_TYPE_TOUCH = 'touch';
+var INPUT_TYPE_PEN = 'pen';
+var INPUT_TYPE_MOUSE = 'mouse';
+var INPUT_TYPE_KINECT = 'kinect';
+
+var COMPUTE_INTERVAL = 25;
+
+var INPUT_START = 1;
+var INPUT_MOVE = 2;
+var INPUT_END = 4;
+var INPUT_CANCEL = 8;
+
+var DIRECTION_NONE = 1;
+var DIRECTION_LEFT = 2;
+var DIRECTION_RIGHT = 4;
+var DIRECTION_UP = 8;
+var DIRECTION_DOWN = 16;
+
+var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT;
+var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN;
+var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;
+
+var PROPS_XY = ['x', 'y'];
+var PROPS_CLIENT_XY = ['clientX', 'clientY'];
+
+/**
+ * create new input type manager
+ * @param {Manager} manager
+ * @param {Function} callback
+ * @returns {Input}
+ * @constructor
+ */
+function Input(manager, callback) {
+ var self = this;
+ this.manager = manager;
+ this.callback = callback;
+ this.element = manager.element;
+ this.target = manager.options.inputTarget;
+
+ // smaller wrapper around the handler, for the scope and the enabled state of the manager,
+ // so when disabled the input events are completely bypassed.
+ this.domHandler = function(ev) {
+ if (boolOrFn(manager.options.enable, [manager])) {
+ self.handler(ev);
+ }
+ };
+
+ this.init();
+
+}
+
+Input.prototype = {
+ /**
+ * should handle the inputEvent data and trigger the callback
+ * @virtual
+ */
+ handler: function() { },
+
+ /**
+ * bind the events
+ */
+ init: function() {
+ this.evEl && addEventListeners(this.element, this.evEl, this.domHandler);
+ this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler);
+ this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler);
+ },
+
+ /**
+ * unbind the events
+ */
+ destroy: function() {
+ this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler);
+ this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler);
+ this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler);
+ }
+};
+
+/**
+ * create new input type manager
+ * called by the Manager constructor
+ * @param {Hammer} manager
+ * @returns {Input}
+ */
+function createInputInstance(manager) {
+ var Type;
+ var inputClass = manager.options.inputClass;
+
+ if (inputClass) {
+ Type = inputClass;
+ } else if (SUPPORT_POINTER_EVENTS) {
+ Type = PointerEventInput;
+ } else if (SUPPORT_ONLY_TOUCH) {
+ Type = TouchInput;
+ } else if (!SUPPORT_TOUCH) {
+ Type = MouseInput;
+ } else {
+ Type = TouchMouseInput;
+ }
+ return new (Type)(manager, inputHandler);
+}
+
+/**
+ * handle input events
+ * @param {Manager} manager
+ * @param {String} eventType
+ * @param {Object} input
+ */
+function inputHandler(manager, eventType, input) {
+ var pointersLen = input.pointers.length;
+ var changedPointersLen = input.changedPointers.length;
+ var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0));
+ var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0));
+
+ input.isFirst = !!isFirst;
+ input.isFinal = !!isFinal;
+
+ if (isFirst) {
+ manager.session = {};
+ }
+
+ // source event is the normalized value of the domEvents
+ // like 'touchstart, mouseup, pointerdown'
+ input.eventType = eventType;
+
+ // compute scale, rotation etc
+ computeInputData(manager, input);
+
+ // emit secret event
+ manager.emit('hammer.input', input);
+
+ manager.recognize(input);
+ manager.session.prevInput = input;
+}
+
+/**
+ * extend the data with some usable properties like scale, rotate, velocity etc
+ * @param {Object} manager
+ * @param {Object} input
+ */
+function computeInputData(manager, input) {
+ var session = manager.session;
+ var pointers = input.pointers;
+ var pointersLength = pointers.length;
+
+ // store the first input to calculate the distance and direction
+ if (!session.firstInput) {
+ session.firstInput = simpleCloneInputData(input);
+ }
+
+ // to compute scale and rotation we need to store the multiple touches
+ if (pointersLength > 1 && !session.firstMultiple) {
+ session.firstMultiple = simpleCloneInputData(input);
+ } else if (pointersLength === 1) {
+ session.firstMultiple = false;
+ }
+
+ var firstInput = session.firstInput;
+ var firstMultiple = session.firstMultiple;
+ var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center;
+
+ var center = input.center = getCenter(pointers);
+ input.timeStamp = now();
+ input.deltaTime = input.timeStamp - firstInput.timeStamp;
+
+ input.angle = getAngle(offsetCenter, center);
+ input.distance = getDistance(offsetCenter, center);
+
+ computeDeltaXY(session, input);
+ input.offsetDirection = getDirection(input.deltaX, input.deltaY);
+
+ var overallVelocity = getVelocity(input.deltaTime, input.deltaX, input.deltaY);
+ input.overallVelocityX = overallVelocity.x;
+ input.overallVelocityY = overallVelocity.y;
+ input.overallVelocity = (abs(overallVelocity.x) > abs(overallVelocity.y)) ? overallVelocity.x : overallVelocity.y;
+
+ input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1;
+ input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0;
+
+ input.maxPointers = !session.prevInput ? input.pointers.length : ((input.pointers.length >
+ session.prevInput.maxPointers) ? input.pointers.length : session.prevInput.maxPointers);
+
+ computeIntervalInputData(session, input);
+
+ // find the correct target
+ var target = manager.element;
+ if (hasParent(input.srcEvent.target, target)) {
+ target = input.srcEvent.target;
+ }
+ input.target = target;
+}
+
+function computeDeltaXY(session, input) {
+ var center = input.center;
+ var offset = session.offsetDelta || {};
+ var prevDelta = session.prevDelta || {};
+ var prevInput = session.prevInput || {};
+
+ if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) {
+ prevDelta = session.prevDelta = {
+ x: prevInput.deltaX || 0,
+ y: prevInput.deltaY || 0
+ };
+
+ offset = session.offsetDelta = {
+ x: center.x,
+ y: center.y
+ };
+ }
+
+ input.deltaX = prevDelta.x + (center.x - offset.x);
+ input.deltaY = prevDelta.y + (center.y - offset.y);
+}
+
+/**
+ * velocity is calculated every x ms
+ * @param {Object} session
+ * @param {Object} input
+ */
+function computeIntervalInputData(session, input) {
+ var last = session.lastInterval || input,
+ deltaTime = input.timeStamp - last.timeStamp,
+ velocity, velocityX, velocityY, direction;
+
+ if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) {
+ var deltaX = input.deltaX - last.deltaX;
+ var deltaY = input.deltaY - last.deltaY;
+
+ var v = getVelocity(deltaTime, deltaX, deltaY);
+ velocityX = v.x;
+ velocityY = v.y;
+ velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y;
+ direction = getDirection(deltaX, deltaY);
+
+ session.lastInterval = input;
+ } else {
+ // use latest velocity info if it doesn't overtake a minimum period
+ velocity = last.velocity;
+ velocityX = last.velocityX;
+ velocityY = last.velocityY;
+ direction = last.direction;
+ }
+
+ input.velocity = velocity;
+ input.velocityX = velocityX;
+ input.velocityY = velocityY;
+ input.direction = direction;
+}
+
+/**
+ * create a simple clone from the input used for storage of firstInput and firstMultiple
+ * @param {Object} input
+ * @returns {Object} clonedInputData
+ */
+function simpleCloneInputData(input) {
+ // make a simple copy of the pointers because we will get a reference if we don't
+ // we only need clientXY for the calculations
+ var pointers = [];
+ var i = 0;
+ while (i < input.pointers.length) {
+ pointers[i] = {
+ clientX: round(input.pointers[i].clientX),
+ clientY: round(input.pointers[i].clientY)
+ };
+ i++;
+ }
+
+ return {
+ timeStamp: now(),
+ pointers: pointers,
+ center: getCenter(pointers),
+ deltaX: input.deltaX,
+ deltaY: input.deltaY
+ };
+}
+
+/**
+ * get the center of all the pointers
+ * @param {Array} pointers
+ * @return {Object} center contains `x` and `y` properties
+ */
+function getCenter(pointers) {
+ var pointersLength = pointers.length;
+
+ // no need to loop when only one touch
+ if (pointersLength === 1) {
+ return {
+ x: round(pointers[0].clientX),
+ y: round(pointers[0].clientY)
+ };
+ }
+
+ var x = 0, y = 0, i = 0;
+ while (i < pointersLength) {
+ x += pointers[i].clientX;
+ y += pointers[i].clientY;
+ i++;
+ }
+
+ return {
+ x: round(x / pointersLength),
+ y: round(y / pointersLength)
+ };
+}
+
+/**
+ * calculate the velocity between two points. unit is in px per ms.
+ * @param {Number} deltaTime
+ * @param {Number} x
+ * @param {Number} y
+ * @return {Object} velocity `x` and `y`
+ */
+function getVelocity(deltaTime, x, y) {
+ return {
+ x: x / deltaTime || 0,
+ y: y / deltaTime || 0
+ };
+}
+
+/**
+ * get the direction between two points
+ * @param {Number} x
+ * @param {Number} y
+ * @return {Number} direction
+ */
+function getDirection(x, y) {
+ if (x === y) {
+ return DIRECTION_NONE;
+ }
+
+ if (abs(x) >= abs(y)) {
+ return x < 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
+ }
+ return y < 0 ? DIRECTION_UP : DIRECTION_DOWN;
+}
+
+/**
+ * calculate the absolute distance between two points
+ * @param {Object} p1 {x, y}
+ * @param {Object} p2 {x, y}
+ * @param {Array} [props] containing x and y keys
+ * @return {Number} distance
+ */
+function getDistance(p1, p2, props) {
+ if (!props) {
+ props = PROPS_XY;
+ }
+ var x = p2[props[0]] - p1[props[0]],
+ y = p2[props[1]] - p1[props[1]];
+
+ return Math.sqrt((x * x) + (y * y));
+}
+
+/**
+ * calculate the angle between two coordinates
+ * @param {Object} p1
+ * @param {Object} p2
+ * @param {Array} [props] containing x and y keys
+ * @return {Number} angle
+ */
+function getAngle(p1, p2, props) {
+ if (!props) {
+ props = PROPS_XY;
+ }
+ var x = p2[props[0]] - p1[props[0]],
+ y = p2[props[1]] - p1[props[1]];
+ return Math.atan2(y, x) * 180 / Math.PI;
+}
+
+/**
+ * calculate the rotation degrees between two pointersets
+ * @param {Array} start array of pointers
+ * @param {Array} end array of pointers
+ * @return {Number} rotation
+ */
+function getRotation(start, end) {
+ return getAngle(end[1], end[0], PROPS_CLIENT_XY) + getAngle(start[1], start[0], PROPS_CLIENT_XY);
+}
+
+/**
+ * calculate the scale factor between two pointersets
+ * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
+ * @param {Array} start array of pointers
+ * @param {Array} end array of pointers
+ * @return {Number} scale
+ */
+function getScale(start, end) {
+ return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY);
+}
+
+var MOUSE_INPUT_MAP = {
+ mousedown: INPUT_START,
+ mousemove: INPUT_MOVE,
+ mouseup: INPUT_END
+};
+
+var MOUSE_ELEMENT_EVENTS = 'mousedown';
+var MOUSE_WINDOW_EVENTS = 'mousemove mouseup';
+
+/**
+ * Mouse events input
+ * @constructor
+ * @extends Input
+ */
+function MouseInput() {
+ this.evEl = MOUSE_ELEMENT_EVENTS;
+ this.evWin = MOUSE_WINDOW_EVENTS;
+
+ this.allow = true; // used by Input.TouchMouse to disable mouse events
+ this.pressed = false; // mousedown state
+
+ Input.apply(this, arguments);
+}
+
+inherit(MouseInput, Input, {
+ /**
+ * handle mouse events
+ * @param {Object} ev
+ */
+ handler: function MEhandler(ev) {
+ var eventType = MOUSE_INPUT_MAP[ev.type];
+
+ // on start we want to have the left mouse button down
+ if (eventType & INPUT_START && ev.button === 0) {
+ this.pressed = true;
+ }
+
+ if (eventType & INPUT_MOVE && ev.which !== 1) {
+ eventType = INPUT_END;
+ }
+
+ // mouse must be down, and mouse events are allowed (see the TouchMouse input)
+ if (!this.pressed || !this.allow) {
+ return;
+ }
+
+ if (eventType & INPUT_END) {
+ this.pressed = false;
+ }
+
+ this.callback(this.manager, eventType, {
+ pointers: [ev],
+ changedPointers: [ev],
+ pointerType: INPUT_TYPE_MOUSE,
+ srcEvent: ev
+ });
+ }
+});
+
+var POINTER_INPUT_MAP = {
+ pointerdown: INPUT_START,
+ pointermove: INPUT_MOVE,
+ pointerup: INPUT_END,
+ pointercancel: INPUT_CANCEL,
+ pointerout: INPUT_CANCEL
+};
+
+// in IE10 the pointer types is defined as an enum
+var IE10_POINTER_TYPE_ENUM = {
+ 2: INPUT_TYPE_TOUCH,
+ 3: INPUT_TYPE_PEN,
+ 4: INPUT_TYPE_MOUSE,
+ 5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816
+};
+
+var POINTER_ELEMENT_EVENTS = 'pointerdown';
+var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel';
+
+// IE10 has prefixed support, and case-sensitive
+if (window.MSPointerEvent) {
+ POINTER_ELEMENT_EVENTS = 'MSPointerDown';
+ POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel';
+}
+
+/**
+ * Pointer events input
+ * @constructor
+ * @extends Input
+ */
+function PointerEventInput() {
+ this.evEl = POINTER_ELEMENT_EVENTS;
+ this.evWin = POINTER_WINDOW_EVENTS;
+
+ Input.apply(this, arguments);
+
+ this.store = (this.manager.session.pointerEvents = []);
+}
+
+inherit(PointerEventInput, Input, {
+ /**
+ * handle mouse events
+ * @param {Object} ev
+ */
+ handler: function PEhandler(ev) {
+ var store = this.store;
+ var removePointer = false;
+
+ var eventTypeNormalized = ev.type.toLowerCase().replace('ms', '');
+ var eventType = POINTER_INPUT_MAP[eventTypeNormalized];
+ var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType;
+
+ var isTouch = (pointerType == INPUT_TYPE_TOUCH);
+
+ // get index of the event in the store
+ var storeIndex = inArray(store, ev.pointerId, 'pointerId');
+
+ // start and mouse must be down
+ if (eventType & INPUT_START && (ev.button === 0 || isTouch)) {
+ if (storeIndex < 0) {
+ store.push(ev);
+ storeIndex = store.length - 1;
+ }
+ } else if (eventType & (INPUT_END | INPUT_CANCEL)) {
+ removePointer = true;
+ }
+
+ // it not found, so the pointer hasn't been down (so it's probably a hover)
+ if (storeIndex < 0) {
+ return;
+ }
+
+ // update the event in the store
+ store[storeIndex] = ev;
+
+ this.callback(this.manager, eventType, {
+ pointers: store,
+ changedPointers: [ev],
+ pointerType: pointerType,
+ srcEvent: ev
+ });
+
+ if (removePointer) {
+ // remove from the store
+ store.splice(storeIndex, 1);
+ }
+ }
+});
+
+var SINGLE_TOUCH_INPUT_MAP = {
+ touchstart: INPUT_START,
+ touchmove: INPUT_MOVE,
+ touchend: INPUT_END,
+ touchcancel: INPUT_CANCEL
+};
+
+var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart';
+var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel';
+
+/**
+ * Touch events input
+ * @constructor
+ * @extends Input
+ */
+function SingleTouchInput() {
+ this.evTarget = SINGLE_TOUCH_TARGET_EVENTS;
+ this.evWin = SINGLE_TOUCH_WINDOW_EVENTS;
+ this.started = false;
+
+ Input.apply(this, arguments);
+}
+
+inherit(SingleTouchInput, Input, {
+ handler: function TEhandler(ev) {
+ var type = SINGLE_TOUCH_INPUT_MAP[ev.type];
+
+ // should we handle the touch events?
+ if (type === INPUT_START) {
+ this.started = true;
+ }
+
+ if (!this.started) {
+ return;
+ }
+
+ var touches = normalizeSingleTouches.call(this, ev, type);
+
+ // when done, reset the started state
+ if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) {
+ this.started = false;
+ }
+
+ this.callback(this.manager, type, {
+ pointers: touches[0],
+ changedPointers: touches[1],
+ pointerType: INPUT_TYPE_TOUCH,
+ srcEvent: ev
+ });
+ }
+});
+
+/**
+ * @this {TouchInput}
+ * @param {Object} ev
+ * @param {Number} type flag
+ * @returns {undefined|Array} [all, changed]
+ */
+function normalizeSingleTouches(ev, type) {
+ var all = toArray(ev.touches);
+ var changed = toArray(ev.changedTouches);
+
+ if (type & (INPUT_END | INPUT_CANCEL)) {
+ all = uniqueArray(all.concat(changed), 'identifier', true);
+ }
+
+ return [all, changed];
+}
+
+var TOUCH_INPUT_MAP = {
+ touchstart: INPUT_START,
+ touchmove: INPUT_MOVE,
+ touchend: INPUT_END,
+ touchcancel: INPUT_CANCEL
+};
+
+var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel';
+
+/**
+ * Multi-user touch events input
+ * @constructor
+ * @extends Input
+ */
+function TouchInput() {
+ this.evTarget = TOUCH_TARGET_EVENTS;
+ this.targetIds = {};
+
+ Input.apply(this, arguments);
+}
+
+inherit(TouchInput, Input, {
+ handler: function MTEhandler(ev) {
+ var type = TOUCH_INPUT_MAP[ev.type];
+ var touches = getTouches.call(this, ev, type);
+ if (!touches) {
+ return;
+ }
+
+ this.callback(this.manager, type, {
+ pointers: touches[0],
+ changedPointers: touches[1],
+ pointerType: INPUT_TYPE_TOUCH,
+ srcEvent: ev
+ });
+ }
+});
+
+/**
+ * @this {TouchInput}
+ * @param {Object} ev
+ * @param {Number} type flag
+ * @returns {undefined|Array} [all, changed]
+ */
+function getTouches(ev, type) {
+ var allTouches = toArray(ev.touches);
+ var targetIds = this.targetIds;
+
+ // when there is only one touch, the process can be simplified
+ if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) {
+ targetIds[allTouches[0].identifier] = true;
+ return [allTouches, allTouches];
+ }
+
+ var i,
+ targetTouches,
+ changedTouches = toArray(ev.changedTouches),
+ changedTargetTouches = [],
+ target = this.target;
+
+ // get target touches from touches
+ targetTouches = allTouches.filter(function(touch) {
+ return hasParent(touch.target, target);
+ });
+
+ // collect touches
+ if (type === INPUT_START) {
+ i = 0;
+ while (i < targetTouches.length) {
+ targetIds[targetTouches[i].identifier] = true;
+ i++;
+ }
+ }
+
+ // filter changed touches to only contain touches that exist in the collected target ids
+ i = 0;
+ while (i < changedTouches.length) {
+ if (targetIds[changedTouches[i].identifier]) {
+ changedTargetTouches.push(changedTouches[i]);
+ }
+
+ // cleanup removed touches
+ if (type & (INPUT_END | INPUT_CANCEL)) {
+ delete targetIds[changedTouches[i].identifier];
+ }
+ i++;
+ }
+
+ if (!changedTargetTouches.length) {
+ return;
+ }
+
+ return [
+ // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel'
+ uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true),
+ changedTargetTouches
+ ];
+}
+
+/**
+ * Combined touch and mouse input
+ *
+ * Touch has a higher priority then mouse, and while touching no mouse events are allowed.
+ * This because touch devices also emit mouse events while doing a touch.
+ *
+ * @constructor
+ * @extends Input
+ */
+function TouchMouseInput() {
+ Input.apply(this, arguments);
+
+ var handler = bindFn(this.handler, this);
+ this.touch = new TouchInput(this.manager, handler);
+ this.mouse = new MouseInput(this.manager, handler);
+}
+
+inherit(TouchMouseInput, Input, {
+ /**
+ * handle mouse and touch events
+ * @param {Hammer} manager
+ * @param {String} inputEvent
+ * @param {Object} inputData
+ */
+ handler: function TMEhandler(manager, inputEvent, inputData) {
+ var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH),
+ isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE);
+
+ // when we're in a touch event, so block all upcoming mouse events
+ // most mobile browser also emit mouseevents, right after touchstart
+ if (isTouch) {
+ this.mouse.allow = false;
+ } else if (isMouse && !this.mouse.allow) {
+ return;
+ }
+
+ // reset the allowMouse when we're done
+ if (inputEvent & (INPUT_END | INPUT_CANCEL)) {
+ this.mouse.allow = true;
+ }
+
+ this.callback(manager, inputEvent, inputData);
+ },
+
+ /**
+ * remove the event listeners
+ */
+ destroy: function destroy() {
+ this.touch.destroy();
+ this.mouse.destroy();
+ }
+});
+
+var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction');
+var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined;
+
+// magical touchAction value
+var TOUCH_ACTION_COMPUTE = 'compute';
+var TOUCH_ACTION_AUTO = 'auto';
+var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented
+var TOUCH_ACTION_NONE = 'none';
+var TOUCH_ACTION_PAN_X = 'pan-x';
+var TOUCH_ACTION_PAN_Y = 'pan-y';
+
+/**
+ * Touch Action
+ * sets the touchAction property or uses the js alternative
+ * @param {Manager} manager
+ * @param {String} value
+ * @constructor
+ */
+function TouchAction(manager, value) {
+ this.manager = manager;
+ this.set(value);
+}
+
+TouchAction.prototype = {
+ /**
+ * set the touchAction value on the element or enable the polyfill
+ * @param {String} value
+ */
+ set: function(value) {
+ // find out the touch-action by the event handlers
+ if (value == TOUCH_ACTION_COMPUTE) {
+ value = this.compute();
+ }
+
+ if (NATIVE_TOUCH_ACTION && this.manager.element.style) {
+ this.manager.element.style[PREFIXED_TOUCH_ACTION] = value;
+ }
+ this.actions = value.toLowerCase().trim();
+ },
+
+ /**
+ * just re-set the touchAction value
+ */
+ update: function() {
+ this.set(this.manager.options.touchAction);
+ },
+
+ /**
+ * compute the value for the touchAction property based on the recognizer's settings
+ * @returns {String} value
+ */
+ compute: function() {
+ var actions = [];
+ each(this.manager.recognizers, function(recognizer) {
+ if (boolOrFn(recognizer.options.enable, [recognizer])) {
+ actions = actions.concat(recognizer.getTouchAction());
+ }
+ });
+ return cleanTouchActions(actions.join(' '));
+ },
+
+ /**
+ * this method is called on each input cycle and provides the preventing of the browser behavior
+ * @param {Object} input
+ */
+ preventDefaults: function(input) {
+ // not needed with native support for the touchAction property
+ if (NATIVE_TOUCH_ACTION) {
+ return;
+ }
+
+ var srcEvent = input.srcEvent;
+ var direction = input.offsetDirection;
+
+ // if the touch action did prevented once this session
+ if (this.manager.session.prevented) {
+ srcEvent.preventDefault();
+ return;
+ }
+
+ var actions = this.actions;
+ var hasNone = inStr(actions, TOUCH_ACTION_NONE);
+ var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y);
+ var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X);
+
+ if (hasNone) {
+ //do not prevent defaults if this is a tap gesture
+
+ var isTapPointer = input.pointers.length === 1;
+ var isTapMovement = input.distance < 2;
+ var isTapTouchTime = input.deltaTime < 250;
+
+ if (isTapPointer && isTapMovement && isTapTouchTime) {
+ return;
+ }
+ }
+
+ if (hasNone ||
+ (hasPanY && direction & DIRECTION_HORIZONTAL) ||
+ (hasPanX && direction & DIRECTION_VERTICAL)) {
+ return this.preventSrc(srcEvent);
+ }
+ },
+
+ /**
+ * call preventDefault to prevent the browser's default behavior (scrolling in most cases)
+ * @param {Object} srcEvent
+ */
+ preventSrc: function(srcEvent) {
+ this.manager.session.prevented = true;
+ srcEvent.preventDefault();
+ }
+};
+
+/**
+ * when the touchActions are collected they are not a valid value, so we need to clean things up. *
+ * @param {String} actions
+ * @returns {*}
+ */
+function cleanTouchActions(actions) {
+ // none
+ if (inStr(actions, TOUCH_ACTION_NONE)) {
+ return TOUCH_ACTION_NONE;
+ }
+
+ var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X);
+ var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y);
+
+ // pan-x and pan-y can be combined
+ if (hasPanX && hasPanY) {
+ return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y;
+ }
+
+ // pan-x OR pan-y
+ if (hasPanX || hasPanY) {
+ return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y;
+ }
+
+ // manipulation
+ if (inStr(actions, TOUCH_ACTION_MANIPULATION)) {
+ return TOUCH_ACTION_MANIPULATION;
+ }
+
+ return TOUCH_ACTION_AUTO;
+}
+
+/**
+ * Recognizer flow explained; *
+ * All recognizers have the initial state of POSSIBLE when a input session starts.
+ * The definition of a input session is from the first input until the last input, with all it's movement in it. *
+ * Example session for mouse-input: mousedown -> mousemove -> mouseup
+ *
+ * On each recognizing cycle (see Manager.recognize) the .recognize() method is executed
+ * which determines with state it should be.
+ *
+ * If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to
+ * POSSIBLE to give it another change on the next cycle.
+ *
+ * Possible
+ * |
+ * +-----+---------------+
+ * | |
+ * +-----+-----+ |
+ * | | |
+ * Failed Cancelled |
+ * +-------+------+
+ * | |
+ * Recognized Began
+ * |
+ * Changed
+ * |
+ * Ended/Recognized
+ */
+var STATE_POSSIBLE = 1;
+var STATE_BEGAN = 2;
+var STATE_CHANGED = 4;
+var STATE_ENDED = 8;
+var STATE_RECOGNIZED = STATE_ENDED;
+var STATE_CANCELLED = 16;
+var STATE_FAILED = 32;
+
+/**
+ * Recognizer
+ * Every recognizer needs to extend from this class.
+ * @constructor
+ * @param {Object} options
+ */
+function Recognizer(options) {
+ // make sure, options are copied over to a new object to prevent leaking it outside
+ options = extend({}, options || {});
+
+ this.id = uniqueId();
+
+ this.manager = null;
+ this.options = merge(options, this.defaults);
+
+ // default is enable true
+ this.options.enable = ifUndefined(this.options.enable, true);
+
+ this.state = STATE_POSSIBLE;
+
+ this.simultaneous = {};
+ this.requireFail = [];
+}
+
+Recognizer.prototype = {
+ /**
+ * @virtual
+ * @type {Object}
+ */
+ defaults: {},
+
+ /**
+ * set options
+ * @param {Object} options
+ * @return {Recognizer}
+ */
+ set: function(options) {
+ extend(this.options, options);
+
+ // also update the touchAction, in case something changed about the directions/enabled state
+ this.manager && this.manager.touchAction.update();
+ return this;
+ },
+
+ /**
+ * recognize simultaneous with an other recognizer.
+ * @param {Recognizer} otherRecognizer
+ * @returns {Recognizer} this
+ */
+ recognizeWith: function(otherRecognizer) {
+ if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) {
+ return this;
+ }
+
+ var simultaneous = this.simultaneous;
+ otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
+ if (!simultaneous[otherRecognizer.id]) {
+ simultaneous[otherRecognizer.id] = otherRecognizer;
+ otherRecognizer.recognizeWith(this);
+ }
+ return this;
+ },
+
+ /**
+ * drop the simultaneous link. it doesnt remove the link on the other recognizer.
+ * @param {Recognizer} otherRecognizer
+ * @returns {Recognizer} this
+ */
+ dropRecognizeWith: function(otherRecognizer) {
+ if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) {
+ return this;
+ }
+
+ otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
+ delete this.simultaneous[otherRecognizer.id];
+ return this;
+ },
+
+ /**
+ * recognizer can only run when an other is failing
+ * @param {Recognizer} otherRecognizer
+ * @returns {Recognizer} this
+ */
+ requireFailure: function(otherRecognizer) {
+ if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) {
+ return this;
+ }
+
+ var requireFail = this.requireFail;
+ otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
+ if (inArray(requireFail, otherRecognizer) === -1) {
+ requireFail.push(otherRecognizer);
+ otherRecognizer.requireFailure(this);
+ }
+ return this;
+ },
+
+ /**
+ * drop the requireFailure link. it does not remove the link on the other recognizer.
+ * @param {Recognizer} otherRecognizer
+ * @returns {Recognizer} this
+ */
+ dropRequireFailure: function(otherRecognizer) {
+ if (invokeArrayArg(otherRecognizer, 'dropRequireFailure', this)) {
+ return this;
+ }
+
+ otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
+ var index = inArray(this.requireFail, otherRecognizer);
+ if (index > -1) {
+ this.requireFail.splice(index, 1);
+ }
+ return this;
+ },
+
+ /**
+ * has require failures boolean
+ * @returns {boolean}
+ */
+ hasRequireFailures: function() {
+ return this.requireFail.length > 0;
+ },
+
+ /**
+ * if the recognizer can recognize simultaneous with an other recognizer
+ * @param {Recognizer} otherRecognizer
+ * @returns {Boolean}
+ */
+ canRecognizeWith: function(otherRecognizer) {
+ return !!this.simultaneous[otherRecognizer.id];
+ },
+
+ /**
+ * You should use `tryEmit` instead of `emit` directly to check
+ * that all the needed recognizers has failed before emitting.
+ * @param {Object} input
+ */
+ emit: function(input) {
+ var self = this;
+ var state = this.state;
+
+ function emit(event) {
+ self.manager.emit(event, input);
+ }
+
+ // 'panstart' and 'panmove'
+ if (state < STATE_ENDED) {
+ emit(self.options.event + stateStr(state));
+ }
+
+ emit(self.options.event); // simple 'eventName' events
+
+ if (input.additionalEvent) { // additional event(panleft, panright, pinchin, pinchout...)
+ emit(input.additionalEvent);
+ }
+
+ // panend and pancancel
+ if (state >= STATE_ENDED) {
+ emit(self.options.event + stateStr(state));
+ }
+ },
+
+ /**
+ * Check that all the require failure recognizers has failed,
+ * if true, it emits a gesture event,
+ * otherwise, setup the state to FAILED.
+ * @param {Object} input
+ */
+ tryEmit: function(input) {
+ if (this.canEmit()) {
+ return this.emit(input);
+ }
+ // it's failing anyway
+ this.state = STATE_FAILED;
+ },
+
+ /**
+ * can we emit?
+ * @returns {boolean}
+ */
+ canEmit: function() {
+ var i = 0;
+ while (i < this.requireFail.length) {
+ if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) {
+ return false;
+ }
+ i++;
+ }
+ return true;
+ },
+
+ /**
+ * update the recognizer
+ * @param {Object} inputData
+ */
+ recognize: function(inputData) {
+ // make a new copy of the inputData
+ // so we can change the inputData without messing up the other recognizers
+ var inputDataClone = extend({}, inputData);
+
+ // is is enabled and allow recognizing?
+ if (!boolOrFn(this.options.enable, [this, inputDataClone])) {
+ this.reset();
+ this.state = STATE_FAILED;
+ return;
+ }
+
+ // reset when we've reached the end
+ if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) {
+ this.state = STATE_POSSIBLE;
+ }
+
+ this.state = this.process(inputDataClone);
+
+ // the recognizer has recognized a gesture
+ // so trigger an event
+ if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) {
+ this.tryEmit(inputDataClone);
+ }
+ },
+
+ /**
+ * return the state of the recognizer
+ * the actual recognizing happens in this method
+ * @virtual
+ * @param {Object} inputData
+ * @returns {Const} STATE
+ */
+ process: function(inputData) { }, // jshint ignore:line
+
+ /**
+ * return the preferred touch-action
+ * @virtual
+ * @returns {Array}
+ */
+ getTouchAction: function() { },
+
+ /**
+ * called when the gesture isn't allowed to recognize
+ * like when another is being recognized or it is disabled
+ * @virtual
+ */
+ reset: function() { }
+};
+
+/**
+ * get a usable string, used as event postfix
+ * @param {Const} state
+ * @returns {String} state
+ */
+function stateStr(state) {
+ if (state & STATE_CANCELLED) {
+ return 'cancel';
+ } else if (state & STATE_ENDED) {
+ return 'end';
+ } else if (state & STATE_CHANGED) {
+ return 'move';
+ } else if (state & STATE_BEGAN) {
+ return 'start';
+ }
+ return '';
+}
+
+/**
+ * direction cons to string
+ * @param {Const} direction
+ * @returns {String}
+ */
+function directionStr(direction) {
+ if (direction == DIRECTION_DOWN) {
+ return 'down';
+ } else if (direction == DIRECTION_UP) {
+ return 'up';
+ } else if (direction == DIRECTION_LEFT) {
+ return 'left';
+ } else if (direction == DIRECTION_RIGHT) {
+ return 'right';
+ }
+ return '';
+}
+
+/**
+ * get a recognizer by name if it is bound to a manager
+ * @param {Recognizer|String} otherRecognizer
+ * @param {Recognizer} recognizer
+ * @returns {Recognizer}
+ */
+function getRecognizerByNameIfManager(otherRecognizer, recognizer) {
+ var manager = recognizer.manager;
+ if (manager) {
+ return manager.get(otherRecognizer);
+ }
+ return otherRecognizer;
+}
+
+/**
+ * This recognizer is just used as a base for the simple attribute recognizers.
+ * @constructor
+ * @extends Recognizer
+ */
+function AttrRecognizer() {
+ Recognizer.apply(this, arguments);
+}
+
+inherit(AttrRecognizer, Recognizer, {
+ /**
+ * @namespace
+ * @memberof AttrRecognizer
+ */
+ defaults: {
+ /**
+ * @type {Number}
+ * @default 1
+ */
+ pointers: 1
+ },
+
+ /**
+ * Used to check if it the recognizer receives valid input, like input.distance > 10.
+ * @memberof AttrRecognizer
+ * @param {Object} input
+ * @returns {Boolean} recognized
+ */
+ attrTest: function(input) {
+ var optionPointers = this.options.pointers;
+ return optionPointers === 0 || input.pointers.length === optionPointers;
+ },
+
+ /**
+ * Process the input and return the state for the recognizer
+ * @memberof AttrRecognizer
+ * @param {Object} input
+ * @returns {*} State
+ */
+ process: function(input) {
+ var state = this.state;
+ var eventType = input.eventType;
+
+ var isRecognized = state & (STATE_BEGAN | STATE_CHANGED);
+ var isValid = this.attrTest(input);
+
+ // on cancel input and we've recognized before, return STATE_CANCELLED
+ if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) {
+ return state | STATE_CANCELLED;
+ } else if (isRecognized || isValid) {
+ if (eventType & INPUT_END) {
+ return state | STATE_ENDED;
+ } else if (!(state & STATE_BEGAN)) {
+ return STATE_BEGAN;
+ }
+ return state | STATE_CHANGED;
+ }
+ return STATE_FAILED;
+ }
+});
+
+/**
+ * Pan
+ * Recognized when the pointer is down and moved in the allowed direction.
+ * @constructor
+ * @extends AttrRecognizer
+ */
+function PanRecognizer() {
+ AttrRecognizer.apply(this, arguments);
+
+ this.pX = null;
+ this.pY = null;
+}
+
+inherit(PanRecognizer, AttrRecognizer, {
+ /**
+ * @namespace
+ * @memberof PanRecognizer
+ */
+ defaults: {
+ event: 'pan',
+ threshold: 10,
+ pointers: 1,
+ direction: DIRECTION_ALL
+ },
+
+ getTouchAction: function() {
+ var direction = this.options.direction;
+ var actions = [];
+ if (direction & DIRECTION_HORIZONTAL) {
+ actions.push(TOUCH_ACTION_PAN_X);
+ }
+ if (direction & DIRECTION_VERTICAL) {
+ actions.push(TOUCH_ACTION_PAN_Y);
+ }
+ return actions;
+ },
+
+ directionTest: function(input) {
+ var options = this.options;
+ var hasMoved = true;
+ var distance = input.distance;
+ var direction = input.direction;
+ var x = input.deltaX;
+ var y = input.deltaY;
+
+ // lock to axis?
+ if (!(direction & options.direction)) {
+ if (options.direction & DIRECTION_HORIZONTAL) {
+ direction = (x === 0) ? DIRECTION_NONE : (x < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT;
+ hasMoved = x != this.pX;
+ distance = Math.abs(input.deltaX);
+ } else {
+ direction = (y === 0) ? DIRECTION_NONE : (y < 0) ? DIRECTION_UP : DIRECTION_DOWN;
+ hasMoved = y != this.pY;
+ distance = Math.abs(input.deltaY);
+ }
+ }
+ input.direction = direction;
+ return hasMoved && distance > options.threshold && direction & options.direction;
+ },
+
+ attrTest: function(input) {
+ return AttrRecognizer.prototype.attrTest.call(this, input) &&
+ (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input)));
+ },
+
+ emit: function(input) {
+
+ this.pX = input.deltaX;
+ this.pY = input.deltaY;
+
+ var direction = directionStr(input.direction);
+
+ if (direction) {
+ input.additionalEvent = this.options.event + direction;
+ }
+ this._super.emit.call(this, input);
+ }
+});
+
+/**
+ * Pinch
+ * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out).
+ * @constructor
+ * @extends AttrRecognizer
+ */
+function PinchRecognizer() {
+ AttrRecognizer.apply(this, arguments);
+}
+
+inherit(PinchRecognizer, AttrRecognizer, {
+ /**
+ * @namespace
+ * @memberof PinchRecognizer
+ */
+ defaults: {
+ event: 'pinch',
+ threshold: 0,
+ pointers: 2
+ },
+
+ getTouchAction: function() {
+ return [TOUCH_ACTION_NONE];
+ },
+
+ attrTest: function(input) {
+ return this._super.attrTest.call(this, input) &&
+ (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN);
+ },
+
+ emit: function(input) {
+ if (input.scale !== 1) {
+ var inOut = input.scale < 1 ? 'in' : 'out';
+ input.additionalEvent = this.options.event + inOut;
+ }
+ this._super.emit.call(this, input);
+ }
+});
+
+/**
+ * Press
+ * Recognized when the pointer is down for x ms without any movement.
+ * @constructor
+ * @extends Recognizer
+ */
+function PressRecognizer() {
+ Recognizer.apply(this, arguments);
+
+ this._timer = null;
+ this._input = null;
+}
+
+inherit(PressRecognizer, Recognizer, {
+ /**
+ * @namespace
+ * @memberof PressRecognizer
+ */
+ defaults: {
+ event: 'press',
+ pointers: 1,
+ time: 500, // minimal time of the pointer to be pressed
+ threshold: 5 // a minimal movement is ok, but keep it low
+ },
+
+ getTouchAction: function() {
+ return [TOUCH_ACTION_AUTO];
+ },
+
+ process: function(input) {
+ var options = this.options;
+ var validPointers = input.pointers.length === options.pointers;
+ var validMovement = input.distance < options.threshold;
+ var validTime = input.deltaTime > options.time;
+
+ this._input = input;
+
+ // we only allow little movement
+ // and we've reached an end event, so a tap is possible
+ if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) {
+ this.reset();
+ } else if (input.eventType & INPUT_START) {
+ this.reset();
+ this._timer = setTimeoutContext(function() {
+ this.state = STATE_RECOGNIZED;
+ this.tryEmit();
+ }, options.time, this);
+ } else if (input.eventType & INPUT_END) {
+ return STATE_RECOGNIZED;
+ }
+ return STATE_FAILED;
+ },
+
+ reset: function() {
+ clearTimeout(this._timer);
+ },
+
+ emit: function(input) {
+ if (this.state !== STATE_RECOGNIZED) {
+ return;
+ }
+
+ if (input && (input.eventType & INPUT_END)) {
+ this.manager.emit(this.options.event + 'up', input);
+ } else {
+ this._input.timeStamp = now();
+ this.manager.emit(this.options.event, this._input);
+ }
+ }
+});
+
+/**
+ * Rotate
+ * Recognized when two or more pointer are moving in a circular motion.
+ * @constructor
+ * @extends AttrRecognizer
+ */
+function RotateRecognizer() {
+ AttrRecognizer.apply(this, arguments);
+}
+
+inherit(RotateRecognizer, AttrRecognizer, {
+ /**
+ * @namespace
+ * @memberof RotateRecognizer
+ */
+ defaults: {
+ event: 'rotate',
+ threshold: 0,
+ pointers: 2
+ },
+
+ getTouchAction: function() {
+ return [TOUCH_ACTION_NONE];
+ },
+
+ attrTest: function(input) {
+ return this._super.attrTest.call(this, input) &&
+ (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN);
+ }
+});
+
+/**
+ * Swipe
+ * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction.
+ * @constructor
+ * @extends AttrRecognizer
+ */
+function SwipeRecognizer() {
+ AttrRecognizer.apply(this, arguments);
+}
+
+inherit(SwipeRecognizer, AttrRecognizer, {
+ /**
+ * @namespace
+ * @memberof SwipeRecognizer
+ */
+ defaults: {
+ event: 'swipe',
+ threshold: 10,
+ velocity: 0.65,
+ direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL,
+ pointers: 1
+ },
+
+ getTouchAction: function() {
+ return PanRecognizer.prototype.getTouchAction.call(this);
+ },
+
+ attrTest: function(input) {
+ var direction = this.options.direction;
+ var velocity;
+
+ if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) {
+ velocity = input.overallVelocity;
+ } else if (direction & DIRECTION_HORIZONTAL) {
+ velocity = input.overallVelocityX;
+ } else if (direction & DIRECTION_VERTICAL) {
+ velocity = input.overallVelocityY;
+ }
+
+ return this._super.attrTest.call(this, input) &&
+ direction & input.offsetDirection &&
+ input.distance > this.options.threshold &&
+ input.maxPointers == this.options.pointers &&
+ abs(velocity) > this.options.velocity && input.eventType & INPUT_END;
+ },
+
+ emit: function(input) {
+ var direction = directionStr(input.offsetDirection);
+ if (direction) {
+ this.manager.emit(this.options.event + direction, input);
+ }
+
+ this.manager.emit(this.options.event, input);
+ }
+});
+
+/**
+ * A tap is ecognized when the pointer is doing a small tap/click. Multiple taps are recognized if they occur
+ * between the given interval and position. The delay option can be used to recognize multi-taps without firing
+ * a single tap.
+ *
+ * The eventData from the emitted event contains the property `tapCount`, which contains the amount of
+ * multi-taps being recognized.
+ * @constructor
+ * @extends Recognizer
+ */
+function TapRecognizer() {
+ Recognizer.apply(this, arguments);
+
+ // previous time and center,
+ // used for tap counting
+ this.pTime = false;
+ this.pCenter = false;
+
+ this._timer = null;
+ this._input = null;
+ this.count = 0;
+}
+
+inherit(TapRecognizer, Recognizer, {
+ /**
+ * @namespace
+ * @memberof PinchRecognizer
+ */
+ defaults: {
+ event: 'tap',
+ pointers: 1,
+ taps: 1,
+ interval: 300, // max time between the multi-tap taps
+ time: 250, // max time of the pointer to be down (like finger on the screen)
+ threshold: 2, // a minimal movement is ok, but keep it low
+ posThreshold: 10 // a multi-tap can be a bit off the initial position
+ },
+
+ getTouchAction: function() {
+ return [TOUCH_ACTION_MANIPULATION];
+ },
+
+ process: function(input) {
+ var options = this.options;
+
+ var validPointers = input.pointers.length === options.pointers;
+ var validMovement = input.distance < options.threshold;
+ var validTouchTime = input.deltaTime < options.time;
+
+ this.reset();
+
+ if ((input.eventType & INPUT_START) && (this.count === 0)) {
+ return this.failTimeout();
+ }
+
+ // we only allow little movement
+ // and we've reached an end event, so a tap is possible
+ if (validMovement && validTouchTime && validPointers) {
+ if (input.eventType != INPUT_END) {
+ return this.failTimeout();
+ }
+
+ var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true;
+ var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold;
+
+ this.pTime = input.timeStamp;
+ this.pCenter = input.center;
+
+ if (!validMultiTap || !validInterval) {
+ this.count = 1;
+ } else {
+ this.count += 1;
+ }
+
+ this._input = input;
+
+ // if tap count matches we have recognized it,
+ // else it has began recognizing...
+ var tapCount = this.count % options.taps;
+ if (tapCount === 0) {
+ // no failing requirements, immediately trigger the tap event
+ // or wait as long as the multitap interval to trigger
+ if (!this.hasRequireFailures()) {
+ return STATE_RECOGNIZED;
+ } else {
+ this._timer = setTimeoutContext(function() {
+ this.state = STATE_RECOGNIZED;
+ this.tryEmit();
+ }, options.interval, this);
+ return STATE_BEGAN;
+ }
+ }
+ }
+ return STATE_FAILED;
+ },
+
+ failTimeout: function() {
+ this._timer = setTimeoutContext(function() {
+ this.state = STATE_FAILED;
+ }, this.options.interval, this);
+ return STATE_FAILED;
+ },
+
+ reset: function() {
+ clearTimeout(this._timer);
+ },
+
+ emit: function() {
+ if (this.state == STATE_RECOGNIZED) {
+ this._input.tapCount = this.count;
+ this.manager.emit(this.options.event, this._input);
+ }
+ }
+});
+
+/**
+ * Simple way to create an manager with a default set of recognizers.
+ * @param {HTMLElement} element
+ * @param {Object} [options]
+ * @constructor
+ */
+function Hammer(element, options) {
+ options = options || {};
+ options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset);
+ return new Manager(element, options);
+}
+
+/**
+ * @const {string}
+ */
+Hammer.VERSION = '2.0.4';
+
+/**
+ * default settings
+ * @namespace
+ */
+Hammer.defaults = {
+ /**
+ * set if DOM events are being triggered.
+ * But this is slower and unused by simple implementations, so disabled by default.
+ * @type {Boolean}
+ * @default false
+ */
+ domEvents: false,
+
+ /**
+ * The value for the touchAction property/fallback.
+ * When set to `compute` it will magically set the correct value based on the added recognizers.
+ * @type {String}
+ * @default compute
+ */
+ touchAction: TOUCH_ACTION_COMPUTE,
+
+ /**
+ * @type {Boolean}
+ * @default true
+ */
+ enable: true,
+
+ /**
+ * EXPERIMENTAL FEATURE -- can be removed/changed
+ * Change the parent input target element.
+ * If Null, then it is being set the to main element.
+ * @type {Null|EventTarget}
+ * @default null
+ */
+ inputTarget: null,
+
+ /**
+ * force an input class
+ * @type {Null|Function}
+ * @default null
+ */
+ inputClass: null,
+
+ /**
+ * Default recognizer setup when calling `Hammer()`
+ * When creating a new Manager these will be skipped.
+ * @type {Array}
+ */
+ preset: [
+ // RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...]
+ [RotateRecognizer, {enable: false}],
+ [PinchRecognizer, {enable: false}, ['rotate']],
+ [SwipeRecognizer, {direction: DIRECTION_HORIZONTAL}],
+ [PanRecognizer, {direction: DIRECTION_HORIZONTAL}, ['swipe']],
+ [TapRecognizer],
+ [TapRecognizer, {event: 'doubletap', taps: 2}, ['tap']],
+ [PressRecognizer]
+ ],
+
+ /**
+ * Some CSS properties can be used to improve the working of Hammer.
+ * Add them to this method and they will be set when creating a new Manager.
+ * @namespace
+ */
+ cssProps: {
+ /**
+ * Disables text selection to improve the dragging gesture. Mainly for desktop browsers.
+ * @type {String}
+ * @default 'none'
+ */
+ userSelect: 'none',
+
+ /**
+ * Disable the Windows Phone grippers when pressing an element.
+ * @type {String}
+ * @default 'none'
+ */
+ touchSelect: 'none',
+
+ /**
+ * Disables the default callout shown when you touch and hold a touch target.
+ * On iOS, when you touch and hold a touch target such as a link, Safari displays
+ * a callout containing information about the link. This property allows you to disable that callout.
+ * @type {String}
+ * @default 'none'
+ */
+ touchCallout: 'none',
+
+ /**
+ * Specifies whether zooming is enabled. Used by IE10>
+ * @type {String}
+ * @default 'none'
+ */
+ contentZooming: 'none',
+
+ /**
+ * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers.
+ * @type {String}
+ * @default 'none'
+ */
+ userDrag: 'none',
+
+ /**
+ * Overrides the highlight color shown when the user taps a link or a JavaScript
+ * clickable element in iOS. This property obeys the alpha value, if specified.
+ * @type {String}
+ * @default 'rgba(0,0,0,0)'
+ */
+ tapHighlightColor: 'rgba(0,0,0,0)'
+ }
+};
+
+var STOP = 1;
+var FORCED_STOP = 2;
+
+/**
+ * Manager
+ * @param {HTMLElement} element
+ * @param {Object} [options]
+ * @constructor
+ */
+function Manager(element, options) {
+ options = options || {};
+
+ this.options = merge(options, Hammer.defaults);
+ this.options.inputTarget = this.options.inputTarget || element;
+
+ this.handlers = {};
+ this.session = {};
+ this.recognizers = [];
+
+ this.element = element;
+ this.input = createInputInstance(this);
+ this.touchAction = new TouchAction(this, this.options.touchAction);
+
+ toggleCssProps(this, true);
+
+ each(options.recognizers, function(item) {
+ var recognizer = this.add(new (item[0])(item[1]));
+ item[2] && recognizer.recognizeWith(item[2]);
+ item[3] && recognizer.requireFailure(item[3]);
+ }, this);
+}
+
+Manager.prototype = {
+ /**
+ * set options
+ * @param {Object} options
+ * @returns {Manager}
+ */
+ set: function(options) {
+ extend(this.options, options);
+
+ // Options that need a little more setup
+ if (options.touchAction) {
+ this.touchAction.update();
+ }
+ if (options.inputTarget) {
+ // Clean up existing event listeners and reinitialize
+ this.input.destroy();
+ this.input.target = options.inputTarget;
+ this.input.init();
+ }
+ return this;
+ },
+
+ /**
+ * stop recognizing for this session.
+ * This session will be discarded, when a new [input]start event is fired.
+ * When forced, the recognizer cycle is stopped immediately.
+ * @param {Boolean} [force]
+ */
+ stop: function(force) {
+ this.session.stopped = force ? FORCED_STOP : STOP;
+ },
+
+ /**
+ * run the recognizers!
+ * called by the inputHandler function on every movement of the pointers (touches)
+ * it walks through all the recognizers and tries to detect the gesture that is being made
+ * @param {Object} inputData
+ */
+ recognize: function(inputData) {
+ var session = this.session;
+ if (session.stopped) {
+ return;
+ }
+
+ // run the touch-action polyfill
+ this.touchAction.preventDefaults(inputData);
+
+ var recognizer;
+ var recognizers = this.recognizers;
+
+ // this holds the recognizer that is being recognized.
+ // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED
+ // if no recognizer is detecting a thing, it is set to `null`
+ var curRecognizer = session.curRecognizer;
+
+ // reset when the last recognizer is recognized
+ // or when we're in a new session
+ if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) {
+ curRecognizer = session.curRecognizer = null;
+ }
+
+ var i = 0;
+ while (i < recognizers.length) {
+ recognizer = recognizers[i];
+
+ // find out if we are allowed try to recognize the input for this one.
+ // 1. allow if the session is NOT forced stopped (see the .stop() method)
+ // 2. allow if we still haven't recognized a gesture in this session, or the this recognizer is the one
+ // that is being recognized.
+ // 3. allow if the recognizer is allowed to run simultaneous with the current recognized recognizer.
+ // this can be setup with the `recognizeWith()` method on the recognizer.
+ if (session.stopped !== FORCED_STOP && ( // 1
+ !curRecognizer || recognizer == curRecognizer || // 2
+ recognizer.canRecognizeWith(curRecognizer))) { // 3
+ recognizer.recognize(inputData);
+ } else {
+ recognizer.reset();
+ }
+
+ // if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the
+ // current active recognizer. but only if we don't already have an active recognizer
+ if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) {
+ curRecognizer = session.curRecognizer = recognizer;
+ }
+ i++;
+ }
+ },
+
+ /**
+ * get a recognizer by its event name.
+ * @param {Recognizer|String} recognizer
+ * @returns {Recognizer|Null}
+ */
+ get: function(recognizer) {
+ if (recognizer instanceof Recognizer) {
+ return recognizer;
+ }
+
+ var recognizers = this.recognizers;
+ for (var i = 0; i < recognizers.length; i++) {
+ if (recognizers[i].options.event == recognizer) {
+ return recognizers[i];
+ }
+ }
+ return null;
+ },
+
+ /**
+ * add a recognizer to the manager
+ * existing recognizers with the same event name will be removed
+ * @param {Recognizer} recognizer
+ * @returns {Recognizer|Manager}
+ */
+ add: function(recognizer) {
+ if (invokeArrayArg(recognizer, 'add', this)) {
+ return this;
+ }
+
+ // remove existing
+ var existing = this.get(recognizer.options.event);
+ if (existing) {
+ this.remove(existing);
+ }
+
+ this.recognizers.push(recognizer);
+ recognizer.manager = this;
+
+ this.touchAction.update();
+ return recognizer;
+ },
+
+ /**
+ * remove a recognizer by name or instance
+ * @param {Recognizer|String} recognizer
+ * @returns {Manager}
+ */
+ remove: function(recognizer) {
+ if (invokeArrayArg(recognizer, 'remove', this)) {
+ return this;
+ }
+
+ var recognizers = this.recognizers;
+ recognizer = this.get(recognizer);
+ recognizers.splice(inArray(recognizers, recognizer), 1);
+
+ this.touchAction.update();
+ return this;
+ },
+
+ /**
+ * bind event
+ * @param {String} events
+ * @param {Function} handler
+ * @returns {EventEmitter} this
+ */
+ on: function(events, handler) {
+ var handlers = this.handlers;
+ each(splitStr(events), function(event) {
+ handlers[event] = handlers[event] || [];
+ handlers[event].push(handler);
+ });
+ return this;
+ },
+
+ /**
+ * unbind event, leave emit blank to remove all handlers
+ * @param {String} events
+ * @param {Function} [handler]
+ * @returns {EventEmitter} this
+ */
+ off: function(events, handler) {
+ var handlers = this.handlers;
+ each(splitStr(events), function(event) {
+ if (!handler) {
+ delete handlers[event];
+ } else {
+ handlers[event].splice(inArray(handlers[event], handler), 1);
+ }
+ });
+ return this;
+ },
+
+ /**
+ * emit event to the listeners
+ * @param {String} event
+ * @param {Object} data
+ */
+ emit: function(event, data) {
+ // we also want to trigger dom events
+ if (this.options.domEvents) {
+ triggerDomEvent(event, data);
+ }
+
+ // no handlers, so skip it all
+ var handlers = this.handlers[event] && this.handlers[event].slice();
+ if (!handlers || !handlers.length) {
+ return;
+ }
+
+ data.type = event;
+ data.preventDefault = function() {
+ data.srcEvent.preventDefault();
+ };
+
+ var i = 0;
+ while (i < handlers.length) {
+ handlers[i](data);
+ i++;
+ }
+ },
+
+ /**
+ * destroy the manager and unbinds all events
+ * it doesn't unbind dom events, that is the user own responsibility
+ */
+ destroy: function() {
+ this.element && toggleCssProps(this, false);
+
+ this.handlers = {};
+ this.session = {};
+ this.input.destroy();
+ this.element = null;
+ }
+};
+
+/**
+ * add/remove the css properties as defined in manager.options.cssProps
+ * @param {Manager} manager
+ * @param {Boolean} add
+ */
+function toggleCssProps(manager, add) {
+ var element = manager.element;
+ if (!element.style) {
+ return;
+ }
+ each(manager.options.cssProps, function(value, name) {
+ element.style[prefixed(element.style, name)] = add ? value : '';
+ });
+}
+
+/**
+ * trigger dom event
+ * @param {String} event
+ * @param {Object} data
+ */
+function triggerDomEvent(event, data) {
+ var gestureEvent = document.createEvent('Event');
+ gestureEvent.initEvent(event, true, true);
+ gestureEvent.gesture = data;
+ data.target.dispatchEvent(gestureEvent);
+}
+
+extend(Hammer, {
+ INPUT_START: INPUT_START,
+ INPUT_MOVE: INPUT_MOVE,
+ INPUT_END: INPUT_END,
+ INPUT_CANCEL: INPUT_CANCEL,
+
+ STATE_POSSIBLE: STATE_POSSIBLE,
+ STATE_BEGAN: STATE_BEGAN,
+ STATE_CHANGED: STATE_CHANGED,
+ STATE_ENDED: STATE_ENDED,
+ STATE_RECOGNIZED: STATE_RECOGNIZED,
+ STATE_CANCELLED: STATE_CANCELLED,
+ STATE_FAILED: STATE_FAILED,
+
+ DIRECTION_NONE: DIRECTION_NONE,
+ DIRECTION_LEFT: DIRECTION_LEFT,
+ DIRECTION_RIGHT: DIRECTION_RIGHT,
+ DIRECTION_UP: DIRECTION_UP,
+ DIRECTION_DOWN: DIRECTION_DOWN,
+ DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL,
+ DIRECTION_VERTICAL: DIRECTION_VERTICAL,
+ DIRECTION_ALL: DIRECTION_ALL,
+
+ Manager: Manager,
+ Input: Input,
+ TouchAction: TouchAction,
+
+ TouchInput: TouchInput,
+ MouseInput: MouseInput,
+ PointerEventInput: PointerEventInput,
+ TouchMouseInput: TouchMouseInput,
+ SingleTouchInput: SingleTouchInput,
+
+ Recognizer: Recognizer,
+ AttrRecognizer: AttrRecognizer,
+ Tap: TapRecognizer,
+ Pan: PanRecognizer,
+ Swipe: SwipeRecognizer,
+ Pinch: PinchRecognizer,
+ Rotate: RotateRecognizer,
+ Press: PressRecognizer,
+
+ on: addEventListeners,
+ off: removeEventListeners,
+ each: each,
+ merge: merge,
+ extend: extend,
+ inherit: inherit,
+ bindFn: bindFn,
+ prefixed: prefixed
+});
+
+if (typeof define == TYPE_FUNCTION && define.amd) {
+ define(function() {
+ return Hammer;
+ });
+} else if (typeof module != 'undefined' && module.exports) {
+ module.exports = Hammer;
+} else {
+ window[exportName] = Hammer;
+}
+
+})(window, document, 'Hammer');
diff --git a/Packages/cornerstone/package.js b/Packages/cornerstone/package.js
new file mode 100644
index 000000000..645fed6a3
--- /dev/null
+++ b/Packages/cornerstone/package.js
@@ -0,0 +1,27 @@
+Package.describe({
+ name: "cornerstone",
+ summary: "Cornerstone Web-based Medical Imaging libraries",
+ version: '0.0.1'
+});
+
+Package.onUse(function (api) {
+ api.versionsFrom('1.2.0.2');
+
+ api.use('jquery');
+
+ api.addFiles('client/cornerstone.js', 'client', {bare: true});
+ api.addFiles('client/cornerstoneMath.js', 'client', {bare: true});
+ api.addFiles('client/cornerstoneTools.js', 'client', {bare: true});
+ api.addFiles('client/cornerstoneWADOImageLoader.js', 'client', {bare: true});
+ api.addFiles('client/cornerstoneWADORSImageLoader.js', 'client', {bare: true});
+ api.addFiles('client/dicomParser.js', 'client', {bare: true});
+ api.addFiles('client/hammer.js', 'client', {bare: true});
+
+ api.export("cornerstone", 'client');
+ api.export("cornerstoneMath", 'client');
+ api.export("cornerstoneTools", 'client');
+ api.export("cornerstoneWADOImageLoader", 'client');
+ api.export("cornerstoneWADORSImageLoader", 'client');
+ api.export("dicomParser", 'client');
+});
+
diff --git a/Packages/dicomweb/package.js b/Packages/dicomweb/package.js
new file mode 100644
index 000000000..a96d9fea4
--- /dev/null
+++ b/Packages/dicomweb/package.js
@@ -0,0 +1,17 @@
+Package.describe({
+ name: "dicomweb",
+ summary: "DICOM Web Helper Functions",
+ version: '0.0.1'
+});
+
+Package.onUse(function (api) {
+ api.addFiles('server/namespace.js', 'server');
+ api.addFiles('server/getImageFrame.js', 'server');
+ api.addFiles('server/getJSON.js', 'server');
+ api.addFiles('server/getName.js', 'server');
+ api.addFiles('server/getNumber.js', 'server');
+ api.addFiles('server/getString.js', 'server');
+
+ api.export("DICOMWeb", 'server');
+});
+
diff --git a/Packages/dicomweb/server/getImageFrame.js b/Packages/dicomweb/server/getImageFrame.js
new file mode 100644
index 000000000..da2f02706
--- /dev/null
+++ b/Packages/dicomweb/server/getImageFrame.js
@@ -0,0 +1,70 @@
+function findBoundary(header) {
+ for(var i=0; i < header.length; i++) {
+ if(header[i].substr(0,2) === '--') {
+ return header[i];
+ }
+ }
+ return undefined;
+}
+
+function findContentType(header) {
+ for(var i=0; i < header.length; i++) {
+ if(header[i].substr(0,13) === 'Content-Type:') {
+ return header[i].substr(13).trim();
+ }
+ }
+ return undefined;
+}
+
+DICOMWeb.getImageFrame = function(uri, mediaType) {
+ mediaType = mediaType || 'application/octet-stream';
+
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.responseType = "arraybuffer";
+ xhr.open("get", uri, true);
+ xhr.setRequestHeader('Accept', 'multipart/related;type=' + mediaType);
+ xhr.onreadystatechange = function (oEvent) {
+ // TODO: consider sending out progress messages here as we receive the pixel data
+ if (xhr.readyState === 4) {
+ if (xhr.status === 200) {
+ // request succeeded, Parse the multi-part mime response
+ var imageFrameAsArrayBuffer = xhr.response;
+ var response = new Uint8Array(xhr.response);
+ // First look for the multipart mime header
+ var tokenIndex = findIndexOfString(response, '\n\r\n');
+ if(tokenIndex === -1) {
+ reject('invalid response - no multipart mime header');
+ }
+ var header = uint8ArrayToString(response, 0, tokenIndex);
+ // Now find the boundary marker
+ var split = header.split('\r\n');
+ var boundary = findBoundary(split);
+ if(!boundary) {
+ reject('invalid response - no boundary marker');
+ }
+ var offset = tokenIndex + 4; // skip over the \n\r\n
+
+ // find the terminal boundary marker
+ var endIndex = findIndexOfString(response, boundary, offset);
+ if(endIndex === -1) {
+ reject('invalid response - terminating boundary not found');
+ }
+ // return the info for this pixel data
+ var length = endIndex - offset - 1;
+ resolve({
+ contentType: findContentType(split),
+ arrayBuffer: imageFrameAsArrayBuffer,
+ offset: offset,
+ length: length
+ });
+ }
+ else {
+ // request failed, reject the deferred
+ reject(xhr.response);
+ }
+ }
+ };
+ xhr.send();
+ });
+};
\ No newline at end of file
diff --git a/Packages/dicomweb/server/getJSON.js b/Packages/dicomweb/server/getJSON.js
new file mode 100644
index 000000000..f3fd168d7
--- /dev/null
+++ b/Packages/dicomweb/server/getJSON.js
@@ -0,0 +1,31 @@
+DICOMWeb.getJSON = function(url, options) {
+ var getOptions = {
+ headers: {
+ 'Accept' : 'application/json'
+ },
+ };
+
+ if (options.auth) {
+ getOptions.auth = options.auth;
+ }
+
+ if (options.logRequests) {
+ console.log(url);
+ }
+
+ if (options.logTiming) {
+ console.time(url);
+ }
+
+ var result = HTTP.get(url, getOptions);
+
+ if (options.logTiming) {
+ console.timeEnd(url);
+ }
+
+ if (options.logResponses) {
+ console.log(result.data);
+ }
+
+ return result;
+};
\ No newline at end of file
diff --git a/Packages/dicomweb/server/getName.js b/Packages/dicomweb/server/getName.js
new file mode 100644
index 000000000..1780f104e
--- /dev/null
+++ b/Packages/dicomweb/server/getName.js
@@ -0,0 +1,26 @@
+/**
+ * Returns the Alphabetic version of a PN
+ *
+ * @param element - The group/element of the element (e.g. '00200013')
+ * @param [defaultValue] - The default value to return if the element is not found
+ * @returns {*}
+ */
+DICOMWeb.getName = function(element, defaultValue) {
+ if(!element) {
+ return defaultValue;
+ }
+ // Value is not present if the attribute has a zero length value
+ if(!element.Value) {
+ return defaultValue;
+ }
+ // Sanity check to make sure we have at least one entry in the array.
+ if(!element.Value.length) {
+ return defaultValue;
+ }
+ // Return the Alphabetic component group
+ if(element.Value[0].Alphabetic) {
+ return element.Value[0].Alphabetic;
+ }
+ // Orthanc does not return PN properly so this is a temporary workaround
+ return element.Value[0];
+};
\ No newline at end of file
diff --git a/Packages/dicomweb/server/getNumber.js b/Packages/dicomweb/server/getNumber.js
new file mode 100644
index 000000000..ae9651b14
--- /dev/null
+++ b/Packages/dicomweb/server/getNumber.js
@@ -0,0 +1,20 @@
+/**
+ * Returns the first string value as a Javascript Number
+ * @param element - The group/element of the element (e.g. '00200013')
+ * @param [defaultValue] - The default value to return if the element does not exist
+ * @returns {*}
+ */
+DICOMWeb.getNumber = function(element, defaultValue) {
+ if(!element) {
+ return defaultValue;
+ }
+ // Value is not present if the attribute has a zero length value
+ if(!element.Value) {
+ return defaultValue;
+ }
+ // Sanity check to make sure we have at least one entry in the array.
+ if(!element.Value.length) {
+ return defaultValue;
+ }
+ return parseFloat(element.Value[0]);
+};
\ No newline at end of file
diff --git a/Packages/dicomweb/server/getString.js b/Packages/dicomweb/server/getString.js
new file mode 100644
index 000000000..8cb0cf3b3
--- /dev/null
+++ b/Packages/dicomweb/server/getString.js
@@ -0,0 +1,23 @@
+/**
+ * Returns the specified element as a string. Multi-valued elements will be separated by a backslash
+ *
+ * @param element - The group/element of the element (e.g. '00200013')
+ * @param [defaultValue] - The value to return if the element is not present
+ * @returns {*}
+ */
+DICOMWeb.getString = function(element, defaultValue) {
+ if(!element) {
+ return defaultValue;
+ }
+ // Value is not present if the attribute has a zero length value
+ if(!element.Value) {
+ return defaultValue;
+ }
+ // Sanity check to make sure we have at least one entry in the array.
+ if(!element.Value.length) {
+ return defaultValue;
+ }
+ // Join the array together separated by backslash
+ // NOTE: Orthanc does not correctly split values into an array so the join is a no-op
+ return element.Value.join('\\');
+};
\ No newline at end of file
diff --git a/Packages/dicomweb/server/namespace.js b/Packages/dicomweb/server/namespace.js
new file mode 100644
index 000000000..7ee760f79
--- /dev/null
+++ b/Packages/dicomweb/server/namespace.js
@@ -0,0 +1 @@
+DICOMWeb = {};
diff --git a/Packages/viewerbase/components/basic/layout/layout.html b/Packages/viewerbase/components/basic/layout/layout.html
new file mode 100644
index 000000000..186f53b63
--- /dev/null
+++ b/Packages/viewerbase/components/basic/layout/layout.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+ {{> yield}}
+
\ No newline at end of file
diff --git a/Packages/viewerbase/components/basic/layout/layout.styl b/Packages/viewerbase/components/basic/layout/layout.styl
new file mode 100644
index 000000000..2629a51f8
--- /dev/null
+++ b/Packages/viewerbase/components/basic/layout/layout.styl
@@ -0,0 +1,18 @@
+.navbar
+ margin-bottom: 0
+ border-radius: 0px
+
+.navbar-form
+ margin: 0
+ padding: 15px 3px
+
+.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
diff --git a/Packages/viewerbase/components/basic/login/login.html b/Packages/viewerbase/components/basic/login/login.html
new file mode 100644
index 000000000..75c97e1c2
--- /dev/null
+++ b/Packages/viewerbase/components/basic/login/login.html
@@ -0,0 +1,3 @@
+
+ {{> atForm}}
+
\ No newline at end of file
diff --git a/Packages/viewerbase/components/basic/notFound/notFound.html b/Packages/viewerbase/components/basic/notFound/notFound.html
new file mode 100644
index 000000000..4fcbea508
--- /dev/null
+++ b/Packages/viewerbase/components/basic/notFound/notFound.html
@@ -0,0 +1,3 @@
+
+ Not found
+
\ No newline at end of file
diff --git a/Packages/viewerbase/components/studyBrowser/imageThumbnail/imageThumbnail.html b/Packages/viewerbase/components/studyBrowser/imageThumbnail/imageThumbnail.html
new file mode 100644
index 000000000..7e2a05d2b
--- /dev/null
+++ b/Packages/viewerbase/components/studyBrowser/imageThumbnail/imageThumbnail.html
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/Packages/viewerbase/components/studyBrowser/imageThumbnail/imageThumbnail.js b/Packages/viewerbase/components/studyBrowser/imageThumbnail/imageThumbnail.js
new file mode 100644
index 000000000..b0fc8b0ba
--- /dev/null
+++ b/Packages/viewerbase/components/studyBrowser/imageThumbnail/imageThumbnail.js
@@ -0,0 +1,11 @@
+Template.imageThumbnail.onRendered(function() {
+ var instance = this.data.instances[0];
+ var element = this.find('.imageThumbnail');
+ cornerstone.enable(element);
+
+ var imageId = getImageId(instance);
+
+ cornerstone.loadAndCacheImage(imageId).then(function(image) {
+ cornerstone.displayImage(element, image);
+ });
+});
\ No newline at end of file
diff --git a/Packages/viewerbase/components/studyBrowser/imageThumbnail/imageThumbnail.styl b/Packages/viewerbase/components/studyBrowser/imageThumbnail/imageThumbnail.styl
new file mode 100644
index 000000000..342eeeef9
--- /dev/null
+++ b/Packages/viewerbase/components/studyBrowser/imageThumbnail/imageThumbnail.styl
@@ -0,0 +1,5 @@
+.imageThumbnail
+ width: 90px
+ height: 90px
+ background-color: black
+ margin: 0 auto
\ No newline at end of file
diff --git a/Packages/viewerbase/components/studyBrowser/studyBrowser/studyBrowser.html b/Packages/viewerbase/components/studyBrowser/studyBrowser/studyBrowser.html
new file mode 100644
index 000000000..5a58d862c
--- /dev/null
+++ b/Packages/viewerbase/components/studyBrowser/studyBrowser/studyBrowser.html
@@ -0,0 +1,7 @@
+
+
+ {{#each studies}}
+ {{> thumbnails}}
+ {{/each}}
+
+
\ No newline at end of file
diff --git a/Packages/viewerbase/components/studyBrowser/studyBrowser/studyBrowser.js b/Packages/viewerbase/components/studyBrowser/studyBrowser/studyBrowser.js
new file mode 100644
index 000000000..4f304d33e
--- /dev/null
+++ b/Packages/viewerbase/components/studyBrowser/studyBrowser/studyBrowser.js
@@ -0,0 +1,6 @@
+Template.studyBrowser.helpers({
+ studies : function() {
+ var studies = Session.get('studies');
+ return studies;
+ }
+});
\ No newline at end of file
diff --git a/Packages/viewerbase/components/studyBrowser/studyBrowser/studyBrowser.styl b/Packages/viewerbase/components/studyBrowser/studyBrowser/studyBrowser.styl
new file mode 100644
index 000000000..f5487f6a8
--- /dev/null
+++ b/Packages/viewerbase/components/studyBrowser/studyBrowser/studyBrowser.styl
@@ -0,0 +1,6 @@
+.studyBrowser
+ float: left
+ height: 100%
+ width: 120px
+ overflow: auto
+ background-color: black
\ No newline at end of file
diff --git a/Packages/viewerbase/components/studyBrowser/thumbnailEntry/thumbnailEntry.html b/Packages/viewerbase/components/studyBrowser/thumbnailEntry/thumbnailEntry.html
new file mode 100644
index 000000000..5cc678709
--- /dev/null
+++ b/Packages/viewerbase/components/studyBrowser/thumbnailEntry/thumbnailEntry.html
@@ -0,0 +1,6 @@
+
+
+ {{ > imageThumbnail }}
+
{{seriesDescription}}
+
+
\ No newline at end of file
diff --git a/Packages/viewerbase/components/studyBrowser/thumbnailEntry/thumbnailEntry.js b/Packages/viewerbase/components/studyBrowser/thumbnailEntry/thumbnailEntry.js
new file mode 100644
index 000000000..ef8ceb0eb
--- /dev/null
+++ b/Packages/viewerbase/components/studyBrowser/thumbnailEntry/thumbnailEntry.js
@@ -0,0 +1,221 @@
+function cloneElement(element, targetId) {
+ // Clone the DOM element
+ var clone = element.cloneNode(true);
+
+ // Find any canvas children to clone
+ var clonedCanvases = $(clone).find('canvas');
+ clonedCanvases.each(function(canvasIndex, clonedCanvas) {
+ // Draw from the original canvas to the cloned canvas
+ var context = clonedCanvas.getContext('2d');
+ var thumbnailCanvas = $(element).find('canvas').get(canvasIndex);
+ context.drawImage(thumbnailCanvas, 0, 0);
+ });
+
+ // Update the clone with the targetId
+ clone.id = targetId;
+ clone.style.visibility = 'hidden';
+ return clone;
+}
+
+function thumbnailDragStartHandler(e) {
+ // Prevent any scrolling behaviour normally caused by the original event
+ e.originalEvent.preventDefault();
+
+ // Identify the current study and series index from the thumbnail's DOM position
+ var targetThumbnail = e.currentTarget;
+ var imageThumbnail = $(targetThumbnail);
+
+ // Store this data for use during drag and drop
+ OHIF.viewer.dragAndDropData = {
+ studyInstanceUid: imageThumbnail.data('studyInstanceUid'),
+ seriesInstanceUid: imageThumbnail.data('seriesInstanceUid')
+ };
+
+ // Clone the image thumbnail
+ var targetId = targetThumbnail.id + 'DragClone';
+ var clone = cloneElement(targetThumbnail, targetId);
+ clone.classList.add('imageThumbnailClone');
+
+ // Set pointerEvents to pass through the clone DOM element
+ // This is necessary in order to identify what is below it
+ // when using document.elementFromPoint
+ clone.style.pointerEvents = "none";
+
+ // Append the clone to the parent of the target
+ targetThumbnail.parentNode.appendChild(clone);
+
+ // Set the cursor x and y positions from the current touch/mouse coordinates
+ // Handle touchStart cases
+ if (e.type === 'touchstart') {
+ cursorX = e.originalEvent.touches[0].pageX;
+ cursorY = e.originalEvent.touches[0].pageY;
+ } else {
+ cursorX = e.pageX;
+ cursorY = e.pageY;
+
+ // Also hook up event handlers for mouse events
+ $(document).on('mousemove', function(e) {
+ thumbnailDragHandler(e, targetThumbnail);
+ });
+ $(document).on('mouseup', function(e) {
+ thumbnailDragEndHandler(e, targetThumbnail);
+ });
+ }
+
+ // This block gets the current position of the touch/mouse
+ // relative to the thumbnail itself
+ //
+ // i.e. Where did the user grab it from?
+ var position = imageThumbnail.offset();
+ var left = position.left;
+ var top = position.top;
+
+ // This difference is saved for later so the element movement looks normal
+ diffX = cursorX - left;
+ diffY = cursorY - top;
+
+ // This sets the default style properties of the cloned element so it is
+ // ready to be dragged around the page
+ $(clone).css({
+ top: top,
+ left: left,
+ position: 'absolute',
+ 'z-index': 100,
+ visibility: 'hidden'
+ });
+}
+
+function thumbnailDragHandler(e, target) {
+ // Get the touch/mouse coordinates from the event
+ var cursorX,
+ cursorY;
+ if (e.type === 'touchmove') {
+ cursorX = e.originalEvent.changedTouches[0].pageX;
+ cursorY = e.originalEvent.changedTouches[0].pageY;
+ } else {
+ cursorX = e.pageX;
+ cursorY = e.pageY;
+ }
+
+ // Find the clone element and update it's position on the page
+ var clone = $('#' + target.id + 'DragClone');
+ clone.css({
+ top: cursorY - diffY,
+ left: cursorX - diffX,
+ position: 'absolute',
+ 'z-index': 100,
+ visibility: 'visible'
+ });
+
+ // Identify the element below the current cursor position
+ var elemBelow = document.elementFromPoint(cursorX + diffX, cursorY + diffY);
+
+ // If none exists, stop here
+ if (!elemBelow) {
+ return;
+ }
+
+ // Remove any current faded effects on viewports
+ $('.imageViewerViewport canvas').removeClass('faded');
+
+ // Figure out what to do depending on what we're dragging over
+ var viewportsDraggedOver = $(elemBelow).parents('.imageViewerViewport');
+ if (viewportsDraggedOver.length) {
+ // If we're dragging over a non-empty viewport, fade it and change the cursor style
+ viewportsDraggedOver.find('canvas').not('.magnifyTool').addClass('faded');
+ document.body.style.cursor = "copy";
+ } else if (elemBelow.classList.contains('imageViewerViewport') && elemBelow.classList.contains('empty')) {
+ // If we're dragging over an empty viewport, just change the cursor style
+ document.body.style.cursor = "copy";
+ } else {
+ // Otherwise, keep the cursor as no-drop style
+ document.body.style.cursor = "no-drop";
+ }
+}
+
+function thumbnailDragEndHandler(e, target) {
+ // Remove the mouse event listeners
+ $(document).off('mousemove mouseup');
+
+ // Reset the cursor style to the default
+ document.body.style.cursor = "auto";
+
+ // Get the cloned element
+ var clone = $('#' + target.id + 'DragClone');
+
+ // If it doesn't exist, stop here
+ if (!clone.length) {
+ return;
+ }
+
+ var top = clone.offset().top;
+ var left = clone.offset().left;
+
+ // Identify the element below the cloned element position
+ var elemBelow = document.elementFromPoint(left + diffX, top + diffY);
+
+ // Remove all cloned elements from the page
+ $('.imageThumbnailClone').remove();
+
+ // Remove any current faded effects on viewports
+ $('.imageViewerViewport canvas').removeClass('faded');
+
+ // If none exists, stop here
+ if (!elemBelow) {
+ return;
+ }
+
+ // Remove any fade effects on the element below
+ elemBelow.classList.remove('faded');
+
+ var element;
+ var viewportsDraggedOver = $(elemBelow).parents('.imageViewerViewport');
+ if (viewportsDraggedOver.length) {
+ // If we're dragging over a non-empty viewport, retrieve it
+ element = viewportsDraggedOver.get(0);
+ } else if (elemBelow.classList.contains('imageViewerViewport') &&
+ elemBelow.classList.contains('empty')) {
+ // If we're dragging over an empty viewport, retrieve that instead
+ element = elemBelow;
+ } else {
+ // Otherwise, stop here
+ return false;
+ }
+
+ // If there is no store drag and drop data, stop here
+ if (!OHIF.viewer.dragAndDropData) {
+ return false;
+ }
+
+ // Rerender the viewport using the drag and drop data
+ rerenderViewportWithNewSeries(element, OHIF.viewer.dragAndDropData);
+ return false;
+}
+
+Template.thumbnailEntry.onRendered(function() {
+ var entry = this.find('.thumbnailEntry');
+ $(entry).data('seriesInstanceUid', Template.parentData(0).seriesInstanceUid);
+ $(entry).data('studyInstanceUid', Template.parentData(1).studyInstanceUid);
+});
+
+Template.thumbnailEntry.events({
+ // Touch drag/drop events
+ 'touchstart .thumbnailEntry, mousedown .thumbnailEntry': function(e) {
+ thumbnailDragStartHandler(e);
+ },
+ 'touchmove .thumbnailEntry': function(e) {
+ thumbnailDragHandler(e, e.currentTarget);
+ },
+ 'touchend .thumbnailEntry': function(e){
+ thumbnailDragEndHandler(e, e.currentTarget);
+ }
+});
+
+Template.thumbnailEntry.helpers({
+ seriesDescription : function() {
+ if (!this.seriesDescription) {
+ return '';
+ }
+ return this.seriesDescription;
+ }
+});
diff --git a/Packages/viewerbase/components/studyBrowser/thumbnailEntry/thumbnailEntry.styl b/Packages/viewerbase/components/studyBrowser/thumbnailEntry/thumbnailEntry.styl
new file mode 100644
index 000000000..c001f0937
--- /dev/null
+++ b/Packages/viewerbase/components/studyBrowser/thumbnailEntry/thumbnailEntry.styl
@@ -0,0 +1,21 @@
+.thumbnailEntry
+ width: 105px
+ padding: 5px
+ background-color: black
+ border: 1px solid #424242
+ margin: 0 auto
+
+ .seriesDescription
+ text-align: center
+ color: #555
+ word-wrap: break-word
+
+ &:hover
+ background-color: white
+
+ &.active
+ background-color: #424242
+ border-color: #4e4e4e
+
+ .seriesDescription
+ color: white
\ No newline at end of file
diff --git a/Packages/viewerbase/components/studyBrowser/thumbnails/thumbnails.html b/Packages/viewerbase/components/studyBrowser/thumbnails/thumbnails.html
new file mode 100644
index 000000000..94bc4a28d
--- /dev/null
+++ b/Packages/viewerbase/components/studyBrowser/thumbnails/thumbnails.html
@@ -0,0 +1,5 @@
+
+ {{#each thumbnails}}
+ {{ >thumbnailEntry }}
+ {{/each}}
+
\ No newline at end of file
diff --git a/Packages/viewerbase/components/studyBrowser/thumbnails/thumbnails.js b/Packages/viewerbase/components/studyBrowser/thumbnails/thumbnails.js
new file mode 100644
index 000000000..a3849adee
--- /dev/null
+++ b/Packages/viewerbase/components/studyBrowser/thumbnails/thumbnails.js
@@ -0,0 +1,6 @@
+Template.thumbnails.helpers({
+ thumbnails: function() {
+ var stacks = createStacks(this);
+ return stacks;
+ }
+});
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/imageViewerViewport/imageViewerViewport.html b/Packages/viewerbase/components/viewer/imageViewerViewport/imageViewerViewport.html
new file mode 100644
index 000000000..770877ba9
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/imageViewerViewport/imageViewerViewport.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Please drag a stack here to view images.
+
+ {{ >loadingIndicator }}
+ {{ >viewportOverlay }}
+ {{ >viewportOrientationMarkers }}
+
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/imageViewerViewport/imageViewerViewport.js b/Packages/viewerbase/components/viewer/imageViewerViewport/imageViewerViewport.js
new file mode 100644
index 000000000..ecbc7826c
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/imageViewerViewport/imageViewerViewport.js
@@ -0,0 +1,230 @@
+/**
+ * This function enables stack prefetching for a specified element (viewport)
+ * It first disables any prefetching currently occurring on any other viewports.
+ * Prefetching will not be enabled if the series is a clip.
+ *
+ * @param element
+ */
+function enablePrefetchOnElement() {
+ var viewportIndex = Session.get('ActiveViewport');
+ var element = $('.imageViewerViewport').get(viewportIndex);
+ console.log('Enabling prefetch on new element');
+
+ // Loop through all viewports and disable stackPrefetch
+ $('.imageViewerViewport').each(function() {
+ if (!$(this).find('canvas').length) {
+ return;
+ }
+ cornerstoneTools.stackPrefetch.disable(this);
+ });
+
+ // Make sure there is a stack to fetch
+ var stack = cornerstoneTools.getToolState(element, 'stack');
+ if (stack && stack.data.length && stack.data[0].imageIds.length > 1) {
+ cornerstoneTools.stackPrefetch.enable(element);
+ }
+}
+
+function displayReferenceLines() {
+ console.log("Changing reference line display");
+ var viewportIndex = Session.get('ActiveViewport');
+ var element = $('.imageViewerViewport').get(viewportIndex);
+
+ $('.imageViewerViewport').each(function(index, element) {
+ if (!$(this).find('canvas').length) {
+ return;
+ }
+
+ if (index === viewportIndex) {
+ cornerstoneTools.referenceLines.tool.disable(element);
+ return;
+ }
+ cornerstoneTools.referenceLines.tool.enable(element, OHIF.viewer.updateImageSynchronizer);
+ });
+}
+
+function loadSeriesIntoViewport(data) {
+ if (!data.series || !data.viewport) {
+ return;
+ }
+
+ var study = data.study;
+ var series = data.series;
+ var element = data.viewport;
+ var viewportIndex = $(".imageViewerViewport").index(data.viewport);
+
+ var allEvents = 'CornerstoneToolsMouseDown CornerstoneToolsMouseDownActivate ' +
+ 'CornerstoneToolsMouseClick CornerstoneToolsMouseDrag CornerstoneToolsMouseUp ' +
+ 'CornerstoneToolsMouseWheel CornerstoneToolsTap CornerstoneToolsTouchPress ' +
+ 'CornerstoneToolsTouchStart CornerstoneToolsTouchStartActive ' +
+ 'CornerstoneToolsDragStartActive CornerstoneToolsMultiTouchDragStart';
+
+ var imageIds = [];
+ var numImages = series.instances.length;
+ series.instances.forEach(function(instance, imageIndex) {
+ var imageId = getImageId(instance);
+ imageIds.push(imageId);
+ var data = {
+ instance: instance,
+ series: series,
+ study: study,
+ numImages: numImages,
+ imageIndex: imageIndex + 1
+ };
+ addMetaData(imageId, data);
+ });
+
+ var stack = {
+ currentImageIdIndex: 0,
+ imageIds: imageIds
+ };
+
+ var imageId = imageIds[stack.currentImageIdIndex];
+
+ var templateData = Template.currentData();
+ templateData.imageId = imageId;
+
+ // NOTE: This uses the experimental WebGL renderer for Cornerstone!
+ cornerstone.enable(element, cornerstone.webGL.renderer.render);
+ // If you have problems, replace it with this line instead:
+ // cornerstone.enable(element);
+
+ cornerstone.loadAndCacheImage(imageId).then(function(image) {
+ cornerstone.displayImage(element, image);
+ element.classList.remove('empty');
+ $(element).siblings('.viewportInstructions').hide();
+ $(element).siblings('.imageViewerViewportOverlay').show();
+
+ cornerstone.resize(element, true);
+
+ var imagePlane = cornerstoneTools.metaData.get('imagePlane', image.imageId);
+
+ cornerstoneTools.addStackStateManager(element, [ 'stack', 'playClip', 'referenceLines' ]);
+
+ // Enable orientation markers, if applicable
+ var viewport = cornerstone.getViewport(element);
+ updateOrientationMarkers(element, viewport);
+
+ // Clear any old stack data
+ cornerstoneTools.clearToolState(element, 'stack');
+ cornerstoneTools.addToolState(element, 'stack', stack);
+
+ // Enable mouse input
+ cornerstoneTools.mouseInput.enable(element);
+ cornerstoneTools.touchInput.enable(element);
+ cornerstoneTools.mouseWheelInput.enable(element);
+
+ var activeTool = toolManager.getActiveTool();
+ toolManager.setActiveTool(activeTool);
+
+ cornerstoneTools.magnify.enable(element);
+
+ function onImageRendered(e, eventData) {
+ Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
+ }
+
+ $(element).off('CornerstoneImageRendered', onImageRendered);
+ $(element).on('CornerstoneImageRendered', onImageRendered);
+ Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
+
+ function onNewImage(e, eventData) {
+ // Update the templateData with the new imageId
+ // This allows the template helpers to update reactively
+ templateData.imageId = eventData.enabledElement.image.imageId;
+ Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
+ }
+
+ $(element).off('CornerstoneNewImage', onNewImage);
+ $(element).on('CornerstoneNewImage', onNewImage);
+
+ function sendActivationTrigger(e, eventData) {
+ var activeViewportIndex = Session.get('ActiveViewport');
+ var viewportIndex = $(".imageViewerViewport").index(eventData.element);
+ if (viewportIndex === activeViewportIndex) {
+ return;
+ }
+ eventData.viewportIndex = viewportIndex;
+ var customEvent = jQuery.Event('ActivateViewport', eventData);
+ customEvent.type = 'ActivateViewport'; // Need to overwrite the type set in the touch tools
+ $(e.target).trigger(customEvent, eventData);
+ }
+
+ $(element).off(allEvents, sendActivationTrigger);
+ $(element).on(allEvents, sendActivationTrigger);
+
+ Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
+
+ if (OHIF.viewer.refLinesEnabled && imagePlane && imagePlane.frameOfReferenceUID) {
+ OHIF.viewer.updateImageSynchronizer.add(element);
+ }
+
+ enablePrefetchOnElement();
+ displayReferenceLines();
+ });
+}
+
+Template.imageViewerViewport.onRendered(function() {
+ var studies = Session.get('studies');
+ var viewport = this.find(".imageViewerViewport");
+ var viewportIndex = $(".imageViewerViewport").index(viewport);
+
+ this.data.viewportIndex = viewportIndex;
+
+ var data = {
+ viewport: viewport
+ };
+
+ if (this.data.seriesInstanceUid !== undefined && this.data.studyInstanceUid !== undefined) {
+ var studyInstanceUid = this.data.studyInstanceUid;
+ var seriesInstanceUid = this.data.seriesInstanceUid;
+
+ studies.every(function(study) {
+ if (study.studyInstanceUid === studyInstanceUid) {
+ data.study = study;
+ study.seriesList.every(function(series) {
+ if (series.seriesInstanceUid === seriesInstanceUid) {
+ data.series = series;
+ return false;
+ }
+ return true;
+ });
+ return false;
+ }
+ return true;
+ });
+ } else {
+ var stacks = [];
+ studies.forEach(function(study) {
+ study.seriesList.forEach(function(series) {
+ var stack = {
+ series: series,
+ study: study
+ };
+ stacks.push(stack);
+ });
+ });
+
+ if (viewportIndex >= stacks.length) {
+ viewport.classList.add('empty');
+ $(viewport).siblings('.viewportInstructions').show();
+ return;
+ }
+ data.series = stacks[viewportIndex].series;
+ data.study = stacks[viewportIndex].study;
+ }
+ loadSeriesIntoViewport(data);
+});
+
+Template.imageViewerViewport.onDestroyed(function() {
+ var element = this.find(".imageViewerViewport");
+ cornerstone.disable(element);
+});
+
+Template.imageViewerViewport.events({
+ 'ActivateViewport .imageViewerViewport': function(e) {
+ console.log('ActivateViewport: ' + e.viewportIndex);
+ Session.set('ActiveViewport', e.viewportIndex);
+ enablePrefetchOnElement();
+ displayReferenceLines();
+ },
+});
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/imageViewerViewport/imageViewerViewport.styl b/Packages/viewerbase/components/viewer/imageViewerViewport/imageViewerViewport.styl
new file mode 100644
index 000000000..6f3c17da0
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/imageViewerViewport/imageViewerViewport.styl
@@ -0,0 +1,23 @@
+.imageViewerViewport
+ width: 100%
+ height: 100%
+ background-color: black
+
+ // Prevents the entire page from getting larger
+ // when the magnify tool is near the sides/corners of the page
+ overflow: hidden
+
+.viewportInstructions
+ display: none
+ font-size: 13px
+ color: #e4ad00
+ line-height: 18px
+ pointer-events: none // Necessary for drag/drop through to cornerstone element below
+ text-align: center
+ position: absolute
+ top: 0
+ bottom: 0
+ right: 0
+ left: 0
+ margin: auto
+ height: 20px
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/imageViewerViewports/imageViewerViewports.html b/Packages/viewerbase/components/viewer/imageViewerViewports/imageViewerViewports.html
new file mode 100644
index 000000000..f0abf88e2
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/imageViewerViewports/imageViewerViewports.html
@@ -0,0 +1,11 @@
+
+
+ {{ #each viewportArray }}
+
+
+ {{ > imageViewerViewport}}
+
+
+ {{ /each }}
+
+
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/imageViewerViewports/imageViewerViewports.js b/Packages/viewerbase/components/viewer/imageViewerViewports/imageViewerViewports.js
new file mode 100644
index 000000000..2145834b8
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/imageViewerViewports/imageViewerViewports.js
@@ -0,0 +1,30 @@
+function getNumViewports() {
+ var viewportRows = Session.get('viewportRows');
+ var viewportColumns = Session.get('viewportColumns');
+ return viewportRows * viewportColumns;
+}
+
+Template.imageViewerViewports.helpers({
+ height: function() {
+ var viewportRows = Session.get('viewportRows');
+ return 100 / viewportRows;
+ },
+ width: function() {
+ var viewportColumns = Session.get('viewportColumns');
+ return 100 / viewportColumns;
+ },
+ viewportArray: function() {
+ // This is a really annoying thing to have to do, but Meteor
+ // doesn't want to let me use another type of helper.
+ var numViewports = getNumViewports();
+
+ var array = [];
+ for (var i=0; i < numViewports; ++i) {
+ var data = {
+ viewportIndex: i
+ };
+ array.push(data);
+ }
+ return array;
+ }
+});
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/imageViewerViewports/imageViewerViewports.styl b/Packages/viewerbase/components/viewer/imageViewerViewports/imageViewerViewports.styl
new file mode 100644
index 000000000..dc1244d31
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/imageViewerViewports/imageViewerViewports.styl
@@ -0,0 +1,12 @@
+#imageViewerViewports
+ height: calc(100% - 30px)
+ width: 100%
+
+ .viewportContainer
+ float: left
+ border: 2px solid #777
+
+ .removable
+ width: 100%
+ height: 100%
+ position: relative // Necessary so that the viewportOverlay is on top of the viewports
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/layoutChooser/layoutChooser.html b/Packages/viewerbase/components/viewer/layoutChooser/layoutChooser.html
new file mode 100644
index 000000000..031ba6eba
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/layoutChooser/layoutChooser.html
@@ -0,0 +1,21 @@
+
+
+
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/layoutChooser/layoutChooser.js b/Packages/viewerbase/components/viewer/layoutChooser/layoutChooser.js
new file mode 100644
index 000000000..9bfcc616c
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/layoutChooser/layoutChooser.js
@@ -0,0 +1,41 @@
+function highlightCells(currentCell) {
+ var cells = $('.layoutChooser table td');
+ cells.removeClass('hover');
+
+ currentCell = $(currentCell);
+ var table = currentCell.parents('.layoutChooser table').get(0);
+ var rowIndex = currentCell.closest('tr').index();
+ var columnIndex = currentCell.index();
+
+ // Loop through the table row by row
+ // and cell by cell to apply the highlighting
+ for (var i = 0; i < table.rows.length; i++) {
+ row = table.rows[i];
+ if (i <= rowIndex) {
+ for (var j = 0; j < row.cells.length; j++) {
+ if (j <= columnIndex) {
+ cell = row.cells[j];
+ cell.classList.add('hover');
+ }
+ }
+ }
+ }
+}
+Template.layoutChooser.events({
+ 'touchstart .layoutChooser table td, mouseenter .layoutChooser table td': function(evt) {
+ highlightCells(evt.currentTarget);
+ },
+ 'click .layoutChooser table td': function(evt) {
+ $('#imageViewerViewports').remove();
+ var container = $(".viewerMain").get(0);
+ UI.render(Template.imageViewerViewports, container);
+
+ var currentCell = $(evt.currentTarget);
+ var rowIndex = currentCell.closest('tr').index();
+ var columnIndex = currentCell.index();
+
+ // Add 1 because the indices start from zero
+ Session.set('viewportRows', rowIndex + 1);
+ Session.set('viewportColumns', columnIndex + 1);
+ }
+});
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/layoutChooser/layoutChooser.styl b/Packages/viewerbase/components/viewer/layoutChooser/layoutChooser.styl
new file mode 100644
index 000000000..314b3c60e
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/layoutChooser/layoutChooser.styl
@@ -0,0 +1,17 @@
+.layoutChooser
+ width: 75px
+ min-width: 75px // to override bootstrap's dropdown-menu class
+ height: 75px
+
+ table
+ margin: 0 auto
+
+ td
+ cursor: pointer
+ border: 2px solid black
+ width: 20px
+ height: 20px
+ transition: background-color 0.1s linear
+
+ &:hover, &.hover // Add the hover class here to be triggered by mouseenter/mouseleave
+ background-color: gray
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/loadingIndicator/loadingIndicator.html b/Packages/viewerbase/components/viewer/loadingIndicator/loadingIndicator.html
new file mode 100644
index 000000000..f840e6372
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/loadingIndicator/loadingIndicator.html
@@ -0,0 +1,14 @@
+
+
+
+
+
Error Loading Image
+
An error has occurred.
+
+
+
+
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/loadingIndicator/loadingIndicator.js b/Packages/viewerbase/components/viewer/loadingIndicator/loadingIndicator.js
new file mode 100644
index 000000000..eedae9c7b
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/loadingIndicator/loadingIndicator.js
@@ -0,0 +1,61 @@
+Meteor.startup(function() {
+ cornerstoneTools.loadHandlerManager.setStartLoadHandler(startLoadingHandler);
+ cornerstoneTools.loadHandlerManager.setEndLoadHandler(doneLoadingHandler);
+ cornerstoneTools.loadHandlerManager.setErrorLoadingHandler(errorLoadingHandler);
+});
+
+var loadHandlerTimeout;
+
+startLoadingHandler = function(element) {
+ clearTimeout(loadHandlerTimeout);
+ loadHandlerTimeout = setTimeout(function() {
+ var elem = $(element);
+ elem.siblings('.imageViewerErrorLoadingIndicator').css('display', 'none');
+ elem.find('canvas').not('.magnifyTool').addClass("faded");
+ elem.siblings('.imageViewerLoadingIndicator').css('display', 'block');
+ }, OHIF.viewer.loadIndicatorDelay);
+};
+
+doneLoadingHandler = function(element) {
+ clearTimeout(loadHandlerTimeout);
+ var elem = $(element);
+ elem.siblings('.imageViewerErrorLoadingIndicator').css('display', 'none');
+ elem.find('canvas').not('.magnifyTool').removeClass("faded");
+ elem.siblings('.imageViewerLoadingIndicator').css('display', 'none');
+};
+
+errorLoadingHandler = function(element, imageId, error, source) {
+ clearTimeout(loadHandlerTimeout);
+ var elem = $(element);
+
+ // Could probably chain all of these, but this is more readable
+ elem.find('canvas').not('.magnifyTool').removeClass("faded");
+ elem.siblings('.imageViewerLoadingIndicator').css('display', 'none');
+
+ // Don't display errors from the stackPrefetch tool
+ if (source === "stackPrefetch") {
+ return;
+ }
+
+ var errorLoadingIndicator = elem.siblings('.imageViewerErrorLoadingIndicator');
+ errorLoadingIndicator.css('display', 'block');
+
+ var cleanedImageId = imageId;
+
+ // This is just used to expand upon some error messages that are sent
+ // when things fail. An example is a network error throwing the error
+ // which is only described as "network".
+ var errorDetails = {
+ network: "A network error has occurred"
+ // We need to expand this further when we see more obscure error messages
+ };
+
+ if (errorDetails.hasOwnProperty(error)) {
+ error = errorDetails[error];
+ }
+
+ errorLoadingIndicator.find('.description').text("An error has occurred while loading image: " + cleanedImageId);
+ if (error) {
+ errorLoadingIndicator.find('.details').text("Details: " + error);
+ }
+};
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/loadingIndicator/loadingIndicator.styl b/Packages/viewerbase/components/viewer/loadingIndicator/loadingIndicator.styl
new file mode 100644
index 000000000..e227e5b84
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/loadingIndicator/loadingIndicator.styl
@@ -0,0 +1,31 @@
+.imageViewerLoadingIndicator
+ display: none
+ color: #ffa500
+
+.faded
+ opacity: 0.5
+
+.imageViewerErrorLoadingIndicator
+ display: none
+ background-color: rgba(0,0,0,0.75)
+
+ p, h4
+ text-align: center
+ color: #ffa500
+ padding: 4px 0px
+
+ p
+ font-size: 11pt
+
+.loadingIndicator
+ pointer-events: none // Necessary for click-through to cornerstone element below
+ width: 100%
+ height: 100%
+ font-size: 18px
+ z-index: 100
+
+ .indicatorContents
+ position: absolute
+ top: 40%
+ width: 100%
+ text-align: center
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.html b/Packages/viewerbase/components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.html
new file mode 100644
index 000000000..533199159
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.html
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.js b/Packages/viewerbase/components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.js
new file mode 100644
index 000000000..c7de59c96
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.js
@@ -0,0 +1,49 @@
+updateOrientationMarkers = function(element, viewport) {
+ // Updates the orientation labels on the viewport
+ var enabledElement = cornerstone.getEnabledElement(element);
+ var imagePlane = cornerstoneTools.metaData.get('imagePlane', enabledElement.image.imageId);
+
+ if (!imagePlane || !imagePlane.rowCosines || !imagePlane.columnCosines) {
+ return;
+ }
+
+ var rowString = cornerstoneTools.orientation.getOrientationString(imagePlane.rowCosines);
+ var columnString = cornerstoneTools.orientation.getOrientationString(imagePlane.columnCosines);
+ var oppositeRowString = cornerstoneTools.orientation.invertOrientationString(rowString);
+ var oppositeColumnString = cornerstoneTools.orientation.invertOrientationString(columnString);
+
+ var markers = {
+ top: oppositeColumnString,
+ left: oppositeRowString,
+ };
+
+ // If any vertical or horizontal flips are applied, change the orientation strings ahead of
+ // the rotation applications
+ if (viewport.vflip) {
+ markers.top = cornerstoneTools.orientation.invertOrientationString(markers.top);
+ }
+ if (viewport.hflip) {
+ markers.left = cornerstoneTools.orientation.invertOrientationString(markers.left);
+ }
+
+ // Get the viewport orientation marker DOM elements
+ var viewportOrientationMarkers = $(element).siblings(".viewportOrientationMarkers");
+ var topMarker = viewportOrientationMarkers.find('.topMid');
+ var leftMarker = viewportOrientationMarkers.find('.leftMid');
+
+ // Swap the labels accordingly if the viewport has been rotated
+ // This could be done in a more complex way for intermediate rotation values (e.g. 45 degrees)
+ if (viewport.rotation === 90 || viewport.rotation === -270) {
+ topMarker.text(markers.left);
+ leftMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.top));
+ } else if (viewport.rotation === -90 || viewport.rotation === 270) {
+ topMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.left));
+ leftMarker.text(markers.top);
+ } else if (viewport.rotation === 180 || viewport.rotation === -180) {
+ topMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.top));
+ leftMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.left));
+ } else {
+ topMarker.text(markers.top);
+ leftMarker.text(markers.left);
+ }
+};
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.styl b/Packages/viewerbase/components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.styl
new file mode 100644
index 000000000..740449286
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.styl
@@ -0,0 +1,18 @@
+.viewportOrientationMarkers
+ pointer-events: none // Necessary for click-through to cornerstone element below
+
+ font-size: 15px
+ color: rgb(204, 204, 204)
+ line-height: 18px
+
+ .orientationMarker
+ position: absolute
+
+ .topMid
+ top: 5px
+ left: 50%
+
+ .leftMid
+ top: 47%
+ left: 5px
+
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/viewportOverlay/viewportOverlay.html b/Packages/viewerbase/components/viewer/viewportOverlay/viewportOverlay.html
new file mode 100644
index 000000000..6f80f9a30
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/viewportOverlay/viewportOverlay.html
@@ -0,0 +1,25 @@
+
+
+
+
{{patientName}}
+
{{patientId}}
+
+
+
+
{{studyDescription}}
+
{{studyDate}} {{studyTime}}
+
+
+
Zoom: {{formatNumberPrecision zoom 2}}%
+
{{compression}}
+
{{wwwc}}
+
+
+
Ser: {{seriesNumber}}
+
Img: {{imageNumber}} ({{imageIndex}}/{{numImages}})
+
{{frameRate}}
+
{{imageDimensions}}
+
{{seriesDescription}}
+
+
+
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/viewportOverlay/viewportOverlay.js b/Packages/viewerbase/components/viewer/viewportOverlay/viewportOverlay.js
new file mode 100644
index 000000000..00eff947a
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/viewportOverlay/viewportOverlay.js
@@ -0,0 +1,155 @@
+function getElementIfNotEmpty(viewportIndex) {
+ // Meteor template helpers run more often than expected
+ // They often seem to run just before the whole template is rendered
+ // This meant that the onRendered event hadn't fired yet, so the
+ // element wasn't enabled / set empty yet. The check here
+ // for canvases under the 'enabled' element div is to prevent
+ // 'undefined' errors from the helper functions
+
+ var imageViewerViewports = $('.imageViewerViewport'),
+ element = imageViewerViewports.get(viewportIndex),
+ canvases = imageViewerViewports.eq(viewportIndex).find('canvas');
+
+ if (!element || $(element).hasClass("empty") || canvases.length === 0) {
+ return;
+ }
+
+ // Check to make sure the element is enabled.
+ try {
+ var enabledElement = cornerstone.getEnabledElement(element);
+ } catch(error) {
+ return;
+ }
+ return element;
+}
+
+function getPatient(property) {
+ Session.get('CornerstoneNewImage' + this.viewportIndex);
+ if (!this.imageId) {
+ return false;
+ }
+ var patient = cornerstoneTools.metaData.get('patient', this.imageId);
+ if (!patient) {
+ return '';
+ }
+ return patient[property];
+}
+
+function getStudy(property) {
+ Session.get('CornerstoneNewImage' + this.viewportIndex);
+ if (!this.imageId) {
+ return false;
+ }
+ var study = cornerstoneTools.metaData.get('study', this.imageId);
+ if (!study) {
+ return '';
+ }
+ return study[property];
+}
+
+function getSeries(property) {
+ Session.get('CornerstoneNewImage' + this.viewportIndex);
+ if (!this.imageId) {
+ return false;
+ }
+ var series = cornerstoneTools.metaData.get('series', this.imageId);
+ if (!series) {
+ return '';
+ }
+ return series[property];
+}
+
+function getInstance(property) {
+ Session.get('CornerstoneNewImage' + this.viewportIndex);
+ if (!this.imageId) {
+ return false;
+ }
+ var instance = cornerstoneTools.metaData.get('instance', this.imageId);
+ if (!instance) {
+ return '';
+ }
+ return instance[property];
+}
+
+function getImage(viewportIndex) {
+ var element = getElementIfNotEmpty(viewportIndex);
+ if (!element) {
+ return false;
+ }
+ var enabledElement;
+ try {
+ enabledElement = cornerstone.getEnabledElement(element);
+ } catch(error) {
+ return false;
+ }
+ if (!enabledElement || !enabledElement.image) {
+ return false;
+ }
+ return enabledElement.image;
+}
+
+Template.viewportOverlay.helpers({
+ wwwc: function() {
+ Session.get('CornerstoneImageRendered' + this.viewportIndex);
+ var element = getElementIfNotEmpty(this.viewportIndex);
+ if (!element) {
+ return '';
+ }
+ var viewport = cornerstone.getViewport(element);
+ if (!viewport) {
+ return '';
+ }
+ return 'W ' + viewport.voi.windowWidth.toFixed(0) + ' L ' + viewport.voi.windowCenter.toFixed(0);
+ },
+ zoom: function() {
+ Session.get('CornerstoneImageRendered' + this.viewportIndex);
+ var element = getElementIfNotEmpty(this.viewportIndex);
+ if (!element) {
+ return '';
+ }
+ var viewport = cornerstone.getViewport(element);
+ if (!viewport) {
+ return '';
+ }
+ return (viewport.scale * 100.0);
+ },
+ imageDimensions: function() {
+ Session.get('CornerstoneNewImage' + this.viewportIndex);
+
+ var image = getImage(this.viewportIndex);
+ if (!image) {
+ return '';
+ }
+ return image.width + ' x ' + image.height;
+ },
+ patientName : function() {
+ return getPatient.call(this, 'name');
+ },
+ patientId : function() {
+ return getPatient.call(this, 'id');
+ },
+ studyDate : function() {
+ return getStudy.call(this, 'date');
+ },
+ studyTime : function() {
+ return getStudy.call(this, 'time');
+ },
+ studyDescription : function() {
+ return getStudy.call(this, 'description');
+ },
+ seriesDescription : function() {
+ return getSeries.call(this, 'description');
+ },
+ seriesNumber : function() {
+ return getSeries.call(this, 'number');
+ },
+ imageNumber : function() {
+ return getInstance.call(this, 'number');
+ },
+ imageIndex : function() {
+ return getInstance.call(this, 'index');
+ },
+ numImages : function() {
+ return getSeries.call(this, 'numImages');
+ }
+});
\ No newline at end of file
diff --git a/Packages/viewerbase/components/viewer/viewportOverlay/viewportOverlay.styl b/Packages/viewerbase/components/viewer/viewportOverlay/viewportOverlay.styl
new file mode 100644
index 000000000..7aa2261ee
--- /dev/null
+++ b/Packages/viewerbase/components/viewer/viewportOverlay/viewportOverlay.styl
@@ -0,0 +1,27 @@
+.imageViewerViewportOverlay
+ font-size: 13px
+ color: #e4ad00
+ line-height: 18px
+ display: none // Shown when an image is loaded
+
+ .dicomTag
+ pointer-events: none // Necessary for drag/drop through to cornerstone element below
+ position: absolute
+
+ .topleft
+ top: 3px
+ left: 3px
+
+ .topright
+ top: 3px
+ right: 3px
+ text-align: right
+
+ .bottomleft
+ bottom: 3px
+ left: 3px
+
+ .bottomright
+ bottom: 3px
+ right: 3px
+ text-align: right
\ No newline at end of file
diff --git a/Packages/viewerbase/lib/accountsConfig.js b/Packages/viewerbase/lib/accountsConfig.js
new file mode 100644
index 000000000..b0dec6aad
--- /dev/null
+++ b/Packages/viewerbase/lib/accountsConfig.js
@@ -0,0 +1,5 @@
+/*AccountsTemplates.configure({
+ forbidClientAccountCreation: true // do not allow account creation
+});
+*/
+
diff --git a/Packages/viewerbase/lib/createStacks.js b/Packages/viewerbase/lib/createStacks.js
new file mode 100644
index 000000000..8bb695b98
--- /dev/null
+++ b/Packages/viewerbase/lib/createStacks.js
@@ -0,0 +1,18 @@
+createStacks = function(study) {
+ var stacks = [];
+ // TODO: Split by multi-frame, modality, image size, etc
+ study.seriesList.forEach(function(series) {
+ if (!series.instances) {
+ return;
+ }
+
+ // Don't display thumbnails for non-image modalities
+ // All imaging modalities must have a valid value for rows (or columns)
+ var anInstance = series.instances[0];
+ if (!anInstance || !anInstance.rows) {
+ return;
+ }
+ stacks.push(series);
+ });
+ return stacks;
+};
\ No newline at end of file
diff --git a/Packages/viewerbase/lib/encodeQueryData.js b/Packages/viewerbase/lib/encodeQueryData.js
new file mode 100644
index 000000000..10355e65a
--- /dev/null
+++ b/Packages/viewerbase/lib/encodeQueryData.js
@@ -0,0 +1,17 @@
+/**
+ * Converts the properties to URL query parameters. Based on:
+ * http://stackoverflow.com/questions/111529/create-query-parameters-in-javascript
+ *
+ * @param data
+ * @returns {string}
+ */
+encodeQueryData = function(data)
+{
+ var ret = [];
+ for (var d in data) {
+ if(data[d]) {
+ ret.push(encodeURIComponent(d) + "=" + encodeURIComponent(data[d]));
+ }
+ }
+ return ret.join("&");
+};
diff --git a/Packages/viewerbase/lib/findIndexOfString.js b/Packages/viewerbase/lib/findIndexOfString.js
new file mode 100644
index 000000000..4b0746dd2
--- /dev/null
+++ b/Packages/viewerbase/lib/findIndexOfString.js
@@ -0,0 +1,39 @@
+function checkToken(token, data, dataOffset) {
+
+ if(dataOffset + token.length > data.length) {
+ //console.log('dataOffset >> ', dataOffset);
+ return false;
+ }
+
+ var endIndex = dataOffset;
+
+ 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(token[0] === data[i]) {
+ //console.log('match @', i);
+ if(checkToken(token, data, i)) {
+ return i;
+ }
+ }
+ }
+ return -1;
+}
\ No newline at end of file
diff --git a/Packages/viewerbase/lib/getActiveViewportElement.js b/Packages/viewerbase/lib/getActiveViewportElement.js
new file mode 100644
index 000000000..1f71ff1db
--- /dev/null
+++ b/Packages/viewerbase/lib/getActiveViewportElement.js
@@ -0,0 +1,5 @@
+getActiveViewportElement = function() {
+ // TODO: Fix this, just hardcoded for now
+ var element = $('.imageViewerViewport').get(0);
+ return element;
+};
\ No newline at end of file
diff --git a/Packages/viewerbase/lib/getImageId.js b/Packages/viewerbase/lib/getImageId.js
new file mode 100644
index 000000000..bd75fe237
--- /dev/null
+++ b/Packages/viewerbase/lib/getImageId.js
@@ -0,0 +1,10 @@
+getImageId = function(instance) {
+ if (!instance) {
+ return;
+ }
+ if (instance.wadouri) {
+ return 'dicomweb:' + instance.wadouri; // WADO-URI
+ } else {
+ return getWADORSImageId(instance); // WADO-RS Retrieve Frame
+ }
+};
\ No newline at end of file
diff --git a/Packages/viewerbase/lib/getWADORSImageId.js b/Packages/viewerbase/lib/getWADORSImageId.js
new file mode 100644
index 000000000..21dd3cc7f
--- /dev/null
+++ b/Packages/viewerbase/lib/getWADORSImageId.js
@@ -0,0 +1,45 @@
+getWADORSImageId = function(instance) {
+ var columnPixelSpacing = 1.0;
+ var rowPixelSpacing = 1.0;
+ if (instance.pixelSpacing) {
+ var split = instance.pixelSpacing.split('\\');
+ rowPixelSpacing = parseFloat(split[0]);
+ columnPixelSpacing = parseFloat(split[1]);
+ }
+
+ var windowWidth;
+ var windowCenter;
+
+ if (instance.windowWidth && instance.windowCenter) {
+ windowWidth = parseFloat(instance.windowWidth.split('\\')[0]);
+ windowCenter = parseFloat(instance.windowCenter.split('\\')[0]);
+ }
+
+ var image = {
+ uri: instance.wadorsuri,
+ //imageId : '',
+ //minPixelValue : 0,
+ //maxPixelValue : 255,
+ slope: instance.rescaleSlope,
+ intercept: instance.rescaleIntercept,
+ windowCenter : windowCenter,
+ windowWidth : windowWidth,
+ //render: cornerstone.renderColorImage,
+ //getPixelData: getPixelData,
+ //getImageData: getImageData,
+ //getCanvas: getCanvas,
+ rows: instance.rows,
+ columns: instance.columns,
+ height: instance.rows,
+ width: instance.columns,
+ color: false,
+ columnPixelSpacing: columnPixelSpacing,
+ rowPixelSpacing: rowPixelSpacing,
+ invert: false,
+ sizeInBytes: instance.rows * instance.columns * (instance.bitsAllocated / 8),
+ instance: instance
+ };
+
+ var imageId = cornerstoneWADORSImageLoader.addImage(image);
+ return imageId;
+};
\ No newline at end of file
diff --git a/Packages/viewerbase/lib/helpers/formatDA.js b/Packages/viewerbase/lib/helpers/formatDA.js
new file mode 100644
index 000000000..420064e36
--- /dev/null
+++ b/Packages/viewerbase/lib/helpers/formatDA.js
@@ -0,0 +1,16 @@
+UI.registerHelper('formatDA', function (context, format, options) {
+ if (!context) {
+ return undefined;
+ }
+ /*var date = DICOMUtilities.toDate(context, '', false);
+ if(!date) {
+ return context;
+ }*/
+ var date = context;
+ var dateAsMoment = moment(date);
+ var strFormat = "MMM D, YYYY";
+ if (options) {
+ strFormat = format;
+ }
+ return dateAsMoment.format(strFormat);
+});
\ No newline at end of file
diff --git a/Packages/viewerbase/lib/helpers/formatNumberPrecision.js b/Packages/viewerbase/lib/helpers/formatNumberPrecision.js
new file mode 100644
index 000000000..e575df50f
--- /dev/null
+++ b/Packages/viewerbase/lib/helpers/formatNumberPrecision.js
@@ -0,0 +1,5 @@
+UI.registerHelper('formatNumberPrecision', function(context, precision, options) {
+ if(context) {
+ return parseFloat(context).toFixed(precision);
+ }
+});
\ No newline at end of file
diff --git a/Packages/viewerbase/lib/helpers/formatPN.js b/Packages/viewerbase/lib/helpers/formatPN.js
new file mode 100644
index 000000000..d1b762591
--- /dev/null
+++ b/Packages/viewerbase/lib/helpers/formatPN.js
@@ -0,0 +1,20 @@
+UI.registerHelper('formatDA', function (context, format, options) {
+ if (!context) {
+ return undefined;
+ }
+ var pn = context;
+ var str = '';
+ if(pn.prefix) {
+ str = pn.prefix + ' ';
+ }
+ str += (pn.familyName || '') + ', ';
+ str += (pn.givenName || '');
+ if(pn.middleName) {
+ str += ' ' + pn.middleName;
+ }
+ if(pn.suffix) {
+ str += ' ' + pn.suffix;
+ }
+
+ return str;
+});
\ No newline at end of file
diff --git a/Packages/viewerbase/lib/helpers/formatTM.js b/Packages/viewerbase/lib/helpers/formatTM.js
new file mode 100644
index 000000000..842571724
--- /dev/null
+++ b/Packages/viewerbase/lib/helpers/formatTM.js
@@ -0,0 +1,13 @@
+UI.registerHelper('formatTM', function(context, format, options) {
+ if (!context) {
+ return undefined;
+ }
+ var date = context;
+ var dateAsMoment = moment(date);
+ var strFormat = "HH:mm:ss";
+ if (options) {
+ strFormat = format;
+ }
+
+ return dateAsMoment.format(strFormat);
+});
diff --git a/Packages/viewerbase/lib/isTouchDevice.js b/Packages/viewerbase/lib/isTouchDevice.js
new file mode 100644
index 000000000..6a1fe84e9
--- /dev/null
+++ b/Packages/viewerbase/lib/isTouchDevice.js
@@ -0,0 +1,5 @@
+isTouchDevice = function() {
+ return (('ontouchstart' in window) ||
+ (navigator.MaxTouchPoints > 0) ||
+ (navigator.msMaxTouchPoints > 0));
+};
\ No newline at end of file
diff --git a/Packages/viewerbase/lib/metaDataProvider.js b/Packages/viewerbase/lib/metaDataProvider.js
new file mode 100644
index 000000000..b38f995e0
--- /dev/null
+++ b/Packages/viewerbase/lib/metaDataProvider.js
@@ -0,0 +1,82 @@
+var metaDataLookup = {};
+
+addMetaData = function(imageId, data) {
+ var instanceMetaData = data.instance;
+ var seriesMetaData = data.series;
+ var studyMetaData = data.study;
+ var imageIndex = data.imageIndex;
+ var numImages = data.numImages;
+
+ var metaData = {};
+
+ metaData.study = {
+ date: instanceMetaData.studyDate,
+ time: instanceMetaData.studyTime,
+ description: instanceMetaData.studyDescription
+ };
+
+ metaData.series = {
+ description: seriesMetaData.seriesDescription,
+ number: seriesMetaData.seriesNumber,
+ modality: seriesMetaData.modality,
+ instanceUid: seriesMetaData.instanceUid,
+ numImages: numImages
+ };
+
+ metaData.instance = {
+ number: instanceMetaData.instanceNumber,
+ index: imageIndex
+ };
+
+ metaData.patient = {
+ name: instanceMetaData.patientName,
+ id: instanceMetaData.patientId
+ };
+
+ // If there is sufficient information, populate
+ // the imagePlane object for easier use in the Viewer
+ if (instanceMetaData.frameOfReferenceUID &&
+ instanceMetaData.imageOrientationPatient &&
+ instanceMetaData.imagePositionPatient) {
+
+ var imageOrientation = instanceMetaData.imageOrientationPatient.split('\\');
+ var imagePosition = instanceMetaData.imagePositionPatient.split('\\');
+
+ var columnPixelSpacing = 1.0;
+ var rowPixelSpacing = 1.0;
+ if (instanceMetaData.pixelSpacing) {
+ var split = instanceMetaData.pixelSpacing.split('\\');
+ rowPixelSpacing = parseFloat(split[0]);
+ columnPixelSpacing = parseFloat(split[1]);
+ }
+
+ metaData.imagePlane = {
+ frameOfReferenceUID: instanceMetaData.frameOfReferenceUID,
+ rows: instanceMetaData.rows,
+ columns: instanceMetaData.columns,
+ rowCosines: new cornerstoneMath.Vector3(parseFloat(imageOrientation[0]), parseFloat(imageOrientation[1]), parseFloat(imageOrientation[2])),
+ columnCosines: new cornerstoneMath.Vector3(parseFloat(imageOrientation[3]), parseFloat(imageOrientation[4]), parseFloat(imageOrientation[5])),
+ imagePositionPatient: new cornerstoneMath.Vector3(parseFloat(imagePosition[0]), parseFloat(imagePosition[1]), parseFloat(imagePosition[2])),
+ rowPixelSpacing: rowPixelSpacing,
+ columnPixelSpacing: columnPixelSpacing,
+ };
+ }
+
+ // Add the metaData to the imageId lookup object
+ metaDataLookup[imageId] = metaData;
+};
+
+function provider(type, imageId) {
+ var imageMetaData = metaDataLookup[imageId];
+ if (!imageMetaData) {
+ return;
+ }
+
+ if (imageMetaData.hasOwnProperty(type)) {
+ return imageMetaData[type];
+ }
+}
+
+Meteor.startup(function() {
+ cornerstoneTools.metaData.addProvider(provider);
+});
diff --git a/Packages/viewerbase/lib/rerenderViewportWithNewSeries.js b/Packages/viewerbase/lib/rerenderViewportWithNewSeries.js
new file mode 100644
index 000000000..e6efb4820
--- /dev/null
+++ b/Packages/viewerbase/lib/rerenderViewportWithNewSeries.js
@@ -0,0 +1,42 @@
+/**
+ * This function destroys and rerenders the imageViewerViewport template.
+ * It uses the data provided (object containing seriesIndex and studyIndex) to
+ * load a new series into the produced viewport.
+ *
+ * @param element
+ * @param data An object containing a seriesIndex and studyIndex for a study to load into this viewport
+ */
+rerenderViewportWithNewSeries = function(element, data) {
+ var container = $(element).parents('.viewportContainer').get(0);
+ var viewportIndex = $(container).index();
+ data.viewportIndex = viewportIndex;
+
+ // Update the dictionary of loaded series for the specified viewport
+ OHIF.viewer.imageViewerLoadedSeriesDictionary[viewportIndex] = {
+ seriesInstanceUid: data.seriesInstanceUid,
+ studyInstanceUid: data.studyInstanceUid
+ };
+
+ // Remove the hover styling
+ $(element).find('canvas').not('.magnifyTool').removeClass("faded");
+
+ // Reset the current image index for this viewport
+ delete OHIF.viewer.imageViewerCurrentImageIdIndexDictionary[viewportIndex];
+
+ // Remove the whole template, add in the new one
+ var viewportContainer = $(element).parents('.removable');
+
+ var newViewportContainer = document.createElement('div');
+ newViewportContainer.className = 'removable';
+
+ // Remove the parent element of the template
+ // This is a workaround since otherwise Blaze UI onDestroyed doesn't fire
+ viewportContainer.remove();
+
+ container.appendChild(newViewportContainer);
+
+ // Render and insert the template
+ UI.renderWithData(Template.imageViewerViewport, data, newViewportContainer);
+
+ Session.set('loadedSeriesDictionary', OHIF.viewer.imageViewerLoadedSeriesDictionary);
+};
\ No newline at end of file
diff --git a/Packages/viewerbase/lib/sortStudy.js b/Packages/viewerbase/lib/sortStudy.js
new file mode 100644
index 000000000..adad39941
--- /dev/null
+++ b/Packages/viewerbase/lib/sortStudy.js
@@ -0,0 +1,15 @@
+sortStudy = function(study) {
+ if (!study) {
+ return;
+ }
+
+ study.seriesList.sort(function(a, b) {
+ return a.seriesNumber - b.seriesNumber;
+ });
+
+ study.seriesList.forEach(function(series){
+ series.instances.sort(function(a, b) {
+ return a.instanceNumber - b.instanceNumber;
+ });
+ });
+};
\ No newline at end of file
diff --git a/Packages/viewerbase/lib/stringToUint8Array.js b/Packages/viewerbase/lib/stringToUint8Array.js
new file mode 100644
index 000000000..484bbc82a
--- /dev/null
+++ b/Packages/viewerbase/lib/stringToUint8Array.js
@@ -0,0 +1,7 @@
+stringToUint8Array = function(str) {
+ var uint=new Uint8Array(str.length);
+ for(var i=0,j=str.length;i= 3);
+ }
+ };
+ cornerstoneTools.panMultiTouch.setConfiguration(multiTouchPanConfig);
+}
+
+toolManager = {
+ init: function() {
+ toolManager.addTool('wwwc', {
+ mouse: cornerstoneTools.wwwc,
+ touch: cornerstoneTools.wwwcTouchDrag
+ });
+ toolManager.addTool('zoom', {
+ mouse: cornerstoneTools.zoom,
+ touch: cornerstoneTools.zoomTouchDrag
+ });
+ toolManager.addTool('wwwcRegion', {
+ mouse: cornerstoneTools.wwwcRegion,
+ touch: cornerstoneTools.wwwcRegionTouch
+ });
+ toolManager.addTool('dragProbe', {
+ mouse: cornerstoneTools.dragProbe,
+ touch: cornerstoneTools.dragProbeTouch
+ });
+ toolManager.addTool('pan', {
+ mouse: cornerstoneTools.pan,
+ touch: cornerstoneTools.panTouchDrag
+ });
+ toolManager.addTool('stackScroll', {
+ mouse: cornerstoneTools.stackScroll,
+ touch: cornerstoneTools.stackScrollTouchDrag
+ });
+ toolManager.addTool('length', {
+ mouse: cornerstoneTools.length,
+ touch: cornerstoneTools.lengthTouch
+ });
+ toolManager.addTool('angle', {
+ mouse: cornerstoneTools.simpleAngle,
+ touch: cornerstoneTools.simpleAngleTouch
+ });
+ toolManager.addTool('magnify', {
+ mouse: cornerstoneTools.magnify,
+ touch: cornerstoneTools.magnifyTouchDrag
+ });
+ toolManager.addTool('ellipticalRoi', {
+ mouse: cornerstoneTools.ellipticalRoi,
+ touch: cornerstoneTools.ellipticalRoiTouch
+ });
+ toolManager.addTool('rectangleRoi', {
+ mouse: cornerstoneTools.rectangleRoi,
+ touch: cornerstoneTools.rectangleRoiTouch
+ });
+ toolManager.addTool('annotate', {
+ mouse: cornerstoneTools.arrowAnnotate,
+ touch: cornerstoneTools.arrowAnnotateTouch
+ });
+ activeTool = OHIF.viewer.defaultTool;
+
+ configureTools();
+ initialized = true;
+ },
+ addTool: function(name, base) {
+ tools[name] = base;
+ },
+ setActiveTool: function(tool) {
+ if (!initialized) {
+ toolManager.init();
+ }
+
+ $('#toolRow .btn-group button').removeClass('active');
+ var toolButton = document.getElementById(tool);
+ toolButton.classList.add('active');
+
+ $('.imageViewerViewport').each(function(index, element) {
+ var canvases = $(element).find('canvas');
+ if (!element.classList.contains('empty') && canvases.length > 0) {
+ // First, deactivate the current active tool
+ tools[activeTool].mouse.deactivate(element, 1);
+ tools[activeTool].touch.deactivate(element);
+
+ // First, get the stack toolData
+ var toolData = cornerstoneTools.getToolState(element, 'stack');
+ if (toolData === undefined || toolData.data === undefined || toolData.data.length === 0) {
+ return;
+ }
+
+ // Get the imageIds for this element
+ var imageIds = toolData.data[0].imageIds;
+
+ // Deactivate all the middle mouse, right click, and scroll wheel tools
+ cornerstoneTools.pan.deactivate(element);
+ cornerstoneTools.zoom.deactivate(element);
+ cornerstoneTools.zoomWheel.deactivate(element);
+ cornerstoneTools.stackScrollWheel.deactivate(element);
+
+ // Reactivate the relevant scrollwheel tool for this element
+ if (imageIds.length > 1) {
+ // scroll is the default tool for middle mouse wheel for stacks
+ cornerstoneTools.stackScrollWheel.activate(element);
+ } else {
+ // zoom is the default tool for middle mouse wheel for single images (non stacks)
+ cornerstoneTools.zoomWheel.activate(element);
+ }
+
+ // This block ensures that the middle mouse and scroll tools keep working
+ if (tool === 'pan') {
+ cornerstoneTools.pan.activate(element, 3); // 3 means left mouse button and middle mouse button
+ cornerstoneTools.zoom.activate(element, 4); // zoom is the default tool for right mouse button
+ } else if (tool === 'zoom') {
+ cornerstoneTools.pan.activate(element, 2); // pan is the default tool for middle mouse button
+ cornerstoneTools.zoom.activate(element, 5); // 5 means left mouse button and right mouse button
+ } else {
+ // Reactivate the middle mouse and right click tools
+ cornerstoneTools.pan.activate(element, 2); // pan is the default tool for middle mouse button
+ cornerstoneTools.zoom.activate(element, 4); // zoom is the default tool for right mouse button
+
+ // Activate the chosen tool
+ tools[tool].mouse.activate(element, 1);
+ }
+ tools[tool].touch.activate(element);
+
+ cornerstoneTools.zoomTouchPinch.activate(element);
+ cornerstoneTools.panMultiTouch.activate(element);
+ }
+ });
+ activeTool = tool;
+ },
+ getActiveTool: function() {
+ if (!activeTool) {
+ activeTool = OHIF.viewer.defaultTool;
+ }
+ return activeTool;
+ },
+ setDefaultTool: function(tool) {
+ OHIF.viewer.defaultTool = tool;
+ },
+ getDefaultTool: function() {
+ return OHIF.viewer.defaultTool;
+ }
+};
diff --git a/Packages/viewerbase/lib/uint8ArrayToString.js b/Packages/viewerbase/lib/uint8ArrayToString.js
new file mode 100644
index 000000000..ba8369b63
--- /dev/null
+++ b/Packages/viewerbase/lib/uint8ArrayToString.js
@@ -0,0 +1,9 @@
+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;
+};
\ No newline at end of file
diff --git a/Packages/viewerbase/lib/updateOrientationMarkers.js b/Packages/viewerbase/lib/updateOrientationMarkers.js
new file mode 100644
index 000000000..c7de59c96
--- /dev/null
+++ b/Packages/viewerbase/lib/updateOrientationMarkers.js
@@ -0,0 +1,49 @@
+updateOrientationMarkers = function(element, viewport) {
+ // Updates the orientation labels on the viewport
+ var enabledElement = cornerstone.getEnabledElement(element);
+ var imagePlane = cornerstoneTools.metaData.get('imagePlane', enabledElement.image.imageId);
+
+ if (!imagePlane || !imagePlane.rowCosines || !imagePlane.columnCosines) {
+ return;
+ }
+
+ var rowString = cornerstoneTools.orientation.getOrientationString(imagePlane.rowCosines);
+ var columnString = cornerstoneTools.orientation.getOrientationString(imagePlane.columnCosines);
+ var oppositeRowString = cornerstoneTools.orientation.invertOrientationString(rowString);
+ var oppositeColumnString = cornerstoneTools.orientation.invertOrientationString(columnString);
+
+ var markers = {
+ top: oppositeColumnString,
+ left: oppositeRowString,
+ };
+
+ // If any vertical or horizontal flips are applied, change the orientation strings ahead of
+ // the rotation applications
+ if (viewport.vflip) {
+ markers.top = cornerstoneTools.orientation.invertOrientationString(markers.top);
+ }
+ if (viewport.hflip) {
+ markers.left = cornerstoneTools.orientation.invertOrientationString(markers.left);
+ }
+
+ // Get the viewport orientation marker DOM elements
+ var viewportOrientationMarkers = $(element).siblings(".viewportOrientationMarkers");
+ var topMarker = viewportOrientationMarkers.find('.topMid');
+ var leftMarker = viewportOrientationMarkers.find('.leftMid');
+
+ // Swap the labels accordingly if the viewport has been rotated
+ // This could be done in a more complex way for intermediate rotation values (e.g. 45 degrees)
+ if (viewport.rotation === 90 || viewport.rotation === -270) {
+ topMarker.text(markers.left);
+ leftMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.top));
+ } else if (viewport.rotation === -90 || viewport.rotation === 270) {
+ topMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.left));
+ leftMarker.text(markers.top);
+ } else if (viewport.rotation === 180 || viewport.rotation === -180) {
+ topMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.top));
+ leftMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.left));
+ } else {
+ topMarker.text(markers.top);
+ leftMarker.text(markers.left);
+ }
+};
\ No newline at end of file
diff --git a/Packages/viewerbase/package.js b/Packages/viewerbase/package.js
new file mode 100644
index 000000000..3fb9bc625
--- /dev/null
+++ b/Packages/viewerbase/package.js
@@ -0,0 +1,115 @@
+Package.describe({
+ name: "viewerbase",
+ summary: "Shared components and functions for Meteor DICOM Viewers",
+ version: '0.0.1'
+});
+
+Package.onUse(function (api) {
+ api.versionsFrom('1.2.0.2');
+
+ api.use('standard-app-packages');
+ api.use('jquery');
+ api.use('stylus');
+
+ // Our custom package
+ api.use('cornerstone');
+
+ // ---------- Components ----------
+
+ // Basic components
+ api.addFiles('components/basic/layout/layout.html', 'client');
+ api.addFiles('components/basic/layout/layout.styl', 'client');
+ api.addFiles('components/basic/login/login.html', 'client');
+ api.addFiles('components/basic/notFound/notFound.html', 'client');
+
+ // Study Browser components
+ api.addFiles('components/studyBrowser/studyBrowser/studyBrowser.html', 'client');
+ api.addFiles('components/studyBrowser/studyBrowser/studyBrowser.js', 'client');
+ api.addFiles('components/studyBrowser/studyBrowser/studyBrowser.styl', 'client');
+
+ api.addFiles('components/studyBrowser/thumbnailEntry/thumbnailEntry.html', 'client');
+ api.addFiles('components/studyBrowser/thumbnailEntry/thumbnailEntry.js', 'client');
+ api.addFiles('components/studyBrowser/thumbnailEntry/thumbnailEntry.styl', 'client');
+
+ api.addFiles('components/studyBrowser/thumbnails/thumbnails.html', 'client');
+ api.addFiles('components/studyBrowser/thumbnails/thumbnails.js', 'client');
+
+ api.addFiles('components/studyBrowser/imageThumbnail/imageThumbnail.html', 'client');
+ api.addFiles('components/studyBrowser/imageThumbnail/imageThumbnail.js', 'client');
+ api.addFiles('components/studyBrowser/imageThumbnail/imageThumbnail.styl', 'client');
+
+ // Viewer components
+ api.addFiles('components/viewer/imageViewerViewport/imageViewerViewport.html', 'client');
+ api.addFiles('components/viewer/imageViewerViewport/imageViewerViewport.js', 'client');
+ api.addFiles('components/viewer/imageViewerViewport/imageViewerViewport.styl', 'client');
+
+ api.addFiles('components/viewer/imageViewerViewports/imageViewerViewports.html', 'client');
+ api.addFiles('components/viewer/imageViewerViewports/imageViewerViewports.js', 'client');
+ api.addFiles('components/viewer/imageViewerViewports/imageViewerViewports.styl', 'client');
+
+ api.addFiles('components/viewer/loadingIndicator/loadingIndicator.html', 'client');
+ api.addFiles('components/viewer/loadingIndicator/loadingIndicator.js', 'client');
+ api.addFiles('components/viewer/loadingIndicator/loadingIndicator.styl', 'client');
+
+ api.addFiles('components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.html', 'client');
+ api.addFiles('components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.js', 'client');
+ api.addFiles('components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.styl', 'client');
+
+ api.addFiles('components/viewer/viewportOverlay/viewportOverlay.html', 'client');
+ api.addFiles('components/viewer/viewportOverlay/viewportOverlay.js', 'client');
+ api.addFiles('components/viewer/viewportOverlay/viewportOverlay.styl', 'client');
+
+ api.addFiles('components/viewer/layoutChooser/layoutChooser.html', 'client');
+ api.addFiles('components/viewer/layoutChooser/layoutChooser.js', 'client');
+ api.addFiles('components/viewer/layoutChooser/layoutChooser.styl', 'client');
+
+ // Library functions
+ api.addFiles('lib/accountsConfig.js', 'client');
+ api.addFiles('lib/createStacks.js', 'client');
+ api.addFiles('lib/encodeQueryData.js', ['client', 'server']);
+ api.addFiles('lib/findIndexOfString.js', 'client');
+ api.addFiles('lib/getActiveViewportElement.js', 'client');
+ api.addFiles('lib/getImageId.js', 'client');
+ api.addFiles('lib/getWADORSImageId.js', 'client');
+ api.addFiles('lib/isTouchDevice.js', 'client');
+ api.addFiles('lib/metaDataProvider.js', 'client');
+ api.addFiles('lib/rerenderViewportWithNewSeries.js', 'client');
+ api.addFiles('lib/sortStudy.js', 'client');
+ api.addFiles('lib/stringToUint8Array.js', 'client');
+ api.addFiles('lib/toolManager.js', 'client');
+ api.addFiles('lib/uint8ArrayToString.js', 'client');
+ api.addFiles('lib/updateOrientationMarkers.js', 'client');
+
+ //api.export('accountsConfig', 'client');
+ api.export('createStacks', 'client');
+ api.export('encodeQueryData', ['client', 'server']);
+ api.export('findIndexOfString', 'client');
+ api.export('getActiveViewportElement', 'client');
+ api.export('getImageId', 'client');
+ api.export('getWADORSImageId', 'client');
+ api.export('isTouchDevice', 'client');
+ api.export('metaDataProvider', 'client');
+ api.export('rerenderViewportWithNewSeries', 'client');
+ api.export('sortStudy', 'client');
+ api.export('stringToUint8Array', 'client');
+ api.export('toolManager', 'client');
+ api.export('uint8ArrayToString', 'client');
+ api.export('updateOrientationMarkers', 'client');
+
+ // UI Helpers
+ api.addFiles('lib/helpers/formatDA.js', 'client');
+ api.addFiles('lib/helpers/formatNumberPrecision.js', 'client');
+ api.addFiles('lib/helpers/formatPN.js', 'client');
+ api.addFiles('lib/helpers/formatTM.js', 'client');
+
+ // Server-side functions
+ api.addFiles('server/seed.js', 'server');
+ api.addFiles('server/lib/namespace.js', 'server');
+ api.addFiles('server/methods/getStudyMetadata.js', 'server');
+ api.addFiles('server/methods/worklistSearch.js', 'server');
+ api.addFiles('server/services/qido/instances.js', 'server');
+ api.addFiles('server/services/qido/studies.js', 'server');
+ api.addFiles('server/services/wado/retrieveMetadata.js', 'server');
+ api.export('Services', 'server');
+});
+
diff --git a/Packages/viewerbase/server/lib/namespace.js b/Packages/viewerbase/server/lib/namespace.js
new file mode 100644
index 000000000..6b26ecfce
--- /dev/null
+++ b/Packages/viewerbase/server/lib/namespace.js
@@ -0,0 +1,3 @@
+Services = {};
+Services.QIDO = {};
+Services.WADO = {};
\ No newline at end of file
diff --git a/Packages/viewerbase/server/methods/getStudyMetadata.js b/Packages/viewerbase/server/methods/getStudyMetadata.js
new file mode 100644
index 000000000..f1eeccdfe
--- /dev/null
+++ b/Packages/viewerbase/server/methods/getStudyMetadata.js
@@ -0,0 +1,10 @@
+Meteor.methods({
+ 'GetStudyMetadata' : function(studyInstanceUid) {
+ console.log('GetStudyMetadata(%s)', studyInstanceUid);
+
+ var server = Meteor.settings.dicomWeb.endpoints[0];
+
+ //return Services.QIDO.Instances(server, studyInstanceUid);
+ return Services.WADO.RetrieveMetadata(server, studyInstanceUid);
+ }
+});
\ No newline at end of file
diff --git a/Packages/viewerbase/server/methods/worklistSearch.js b/Packages/viewerbase/server/methods/worklistSearch.js
new file mode 100644
index 000000000..b41ffe987
--- /dev/null
+++ b/Packages/viewerbase/server/methods/worklistSearch.js
@@ -0,0 +1,6 @@
+Meteor.methods({
+ 'WorklistSearch' : function(filter) {
+ var server = Meteor.settings.dicomWeb.endpoints[0];
+ return Services.QIDO.Studies(server, filter);
+ }
+});
\ No newline at end of file
diff --git a/Packages/viewerbase/server/seed.js b/Packages/viewerbase/server/seed.js
new file mode 100644
index 000000000..2c16bc705
--- /dev/null
+++ b/Packages/viewerbase/server/seed.js
@@ -0,0 +1,18 @@
+var users = [
+ {
+ email: 'admin@ohif.org',
+ //username: "admin",
+ password: "admin",
+ profile: {
+
+ }
+ }
+];
+
+Meteor.startup(function() {
+ users.forEach(function(user) {
+ if(!Meteor.users.findOne({username: user.username})) {
+ Accounts.createUser(user);
+ }
+ });
+});
\ No newline at end of file
diff --git a/Packages/viewerbase/server/services/qido/instances.js b/Packages/viewerbase/server/services/qido/instances.js
new file mode 100644
index 000000000..27634ea39
--- /dev/null
+++ b/Packages/viewerbase/server/services/qido/instances.js
@@ -0,0 +1,59 @@
+function buildUrl(server, studyInstanceUid) {
+
+ var url = server.qidoRoot + '/studies/' + studyInstanceUid + '/instances';
+
+ return url;
+}
+
+function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
+ var seriesMap = {};
+ var seriesList = [];
+ resultData.forEach(function(instance) {
+ var seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
+ var series = seriesMap[seriesInstanceUid];
+ if(!series) {
+ series = {
+ seriesInstanceUid : seriesInstanceUid,
+ seriesNumber : DICOMWeb.getString(instance['00200011']),
+ instances: []
+ };
+ seriesMap[seriesInstanceUid] = series;
+ seriesList.push(series);
+ }
+
+ // The uri for the dicomweb
+ // NOTE: DCM4CHEE seems to return the data zipped
+ // NOTE: Orthanc returns the data with multi-part mime which cornerstoneWADOImageLoader doesn't
+ // know how to parse yet
+ //var uri = DICOMWeb.getString(instance['00081190']);
+ //uri = uri.replace('wado-rs', 'dicom-web');
+
+ // manually create a WADO-URI from the UID's
+ // NOTE: Haven't been able to get Orthanc's WADO-URI to work yet - maybe its not configured?
+ var sopInstanceUid = DICOMWeb.getString(instance['00080018']);
+ var uri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
+
+ series.instances.push({
+ sopClassUid: DICOMWeb.getString(instance['00080016']),
+ sopInstanceUid: sopInstanceUid,
+ uri: uri,
+ instanceNumber: DICOMWeb.getString(instance['00200013'])
+ });
+ });
+ return seriesList;
+}
+
+Services.QIDO.Instances = function(server, studyInstanceUid) {
+
+ var url = buildUrl(server, studyInstanceUid);
+
+ var result = DICOMWeb.getJSON(url, server.requestOptions);
+
+ var study = {
+ wadoUriRoot: server.wadoUriRoot,
+ studyInstanceUid: studyInstanceUid,
+ seriesList: resultDataToStudyMetadata(server, studyInstanceUid, result.data)
+ };
+
+ return study;
+};
\ No newline at end of file
diff --git a/Packages/viewerbase/server/services/qido/studies.js b/Packages/viewerbase/server/services/qido/studies.js
new file mode 100644
index 000000000..9feb9ede0
--- /dev/null
+++ b/Packages/viewerbase/server/services/qido/studies.js
@@ -0,0 +1,38 @@
+function filterToQIDOURL(server, filter) {
+ var parameters = {
+ PatientName : filter.patientName,
+ PatientID: filter.patientId,
+ AccessionNumber : filter.accessionNumber,
+ limit : filter.limit || 20,
+ includefield : server.qidoSupportsIncludeField ? 'all' : undefined
+ };
+ var url = server.qidoRoot + '/studies?' + encodeQueryData(parameters);
+ return url;
+}
+
+function resultDataToStudies(resultData) {
+ var studies = [];
+ resultData.forEach(function(study) {
+ studies.push({
+ patientName: DICOMWeb.getName(study['00100010']),
+ patientId: DICOMWeb.getString(study['00100020']),
+ accessionNumber : DICOMWeb.getString(study['00080050']),
+ studyDate: DICOMWeb.getString(study['00080020']),
+ modalities: DICOMWeb.getString(study['00080061']),
+ studyDescription: DICOMWeb.getString(study['00081030']),
+ imageCount: DICOMWeb.getString(study['00201208']),
+ studyInstanceUid: DICOMWeb.getString(study['0020000D'])
+ //url : server.wadoRoot + '/studies/' + DICOMWeb.getString(study['0020000D']) + '/metadata'
+ });
+ });
+ return studies;
+}
+
+Services.QIDO.Studies = function(server, filter) {
+
+ var url = filterToQIDOURL(server, filter);
+
+ var result = DICOMWeb.getJSON(url, server.requestOptions);
+
+ return resultDataToStudies(result.data);
+};
\ No newline at end of file
diff --git a/Packages/viewerbase/server/services/wado/retrieveMetadata.js b/Packages/viewerbase/server/services/wado/retrieveMetadata.js
new file mode 100644
index 000000000..7bfebbc95
--- /dev/null
+++ b/Packages/viewerbase/server/services/wado/retrieveMetadata.js
@@ -0,0 +1,88 @@
+function buildUrl(server, studyInstanceUid) {
+
+ var url = server.wadoRoot + '/studies/' + studyInstanceUid + '/metadata';
+
+ return url;
+}
+
+function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
+ var seriesMap = {};
+ var seriesList = [];
+ resultData.forEach(function(instance) {
+ var seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
+ var series = seriesMap[seriesInstanceUid];
+ if(!series) {
+ series = {
+ seriesDescription: DICOMWeb.getString(instance['0008103E']),
+ modality: DICOMWeb.getString(instance['00080060']),
+ seriesInstanceUid : seriesInstanceUid,
+ seriesNumber : DICOMWeb.getNumber(instance['00200011']),
+ instances: []
+ };
+ seriesMap[seriesInstanceUid] = series;
+ seriesList.push(series);
+ }
+
+ var sopInstanceUid = DICOMWeb.getString(instance['00080018']);
+
+ var instanceSummary = {
+ // -----------
+ // TODO = Fix this
+ // This doesn't seem like the best place to put this, but otherwise we have no study info
+ patientName: DICOMWeb.getName(instance['00100010']),
+ patientId: DICOMWeb.getString(instance['00100020']),
+ accessionNumber : DICOMWeb.getString(instance['00080050']),
+ studyDate: DICOMWeb.getString(instance['00080020']),
+ modalities: DICOMWeb.getString(instance['00080061']),
+ studyDescription: DICOMWeb.getString(instance['00081030']),
+ imageCount: DICOMWeb.getString(instance['00201208']),
+ studyInstanceUid: DICOMWeb.getString(instance['0020000D']),
+ // -----------
+ imageType: DICOMWeb.getString(instance['00080008']),
+ sopClassUid: DICOMWeb.getString(instance['00080016']),
+ sopInstanceUid: sopInstanceUid,
+ instanceNumber: DICOMWeb.getNumber(instance['00200013']),
+ imagePositionPatient: DICOMWeb.getString(instance['00200032']),
+ imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
+ frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
+ sliceLocation: DICOMWeb.getNumber(instance['00201041']),
+ samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
+ photometricInterpretation: DICOMWeb.getString(instance['00280004']),
+ rows: DICOMWeb.getNumber(instance['00280010']),
+ columns: DICOMWeb.getNumber(instance['00280011']),
+ pixelSpacing: DICOMWeb.getString(instance['00280030']),
+ bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
+ bitsStored: DICOMWeb.getNumber(instance['00280101']),
+ highBit: DICOMWeb.getNumber(instance['00280102']),
+ pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
+ windowCenter: DICOMWeb.getString(instance['00281050']),
+ windowWidth: DICOMWeb.getString(instance['00281051']),
+ rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
+ rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
+ };
+
+ if(server.imageRendering === 'wadouri') {
+ instanceSummary.wadouri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
+ } else {
+ instanceSummary.wadorsuri = server.wadoRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
+ }
+
+ series.instances.push(instanceSummary);
+ });
+ return seriesList;
+}
+
+Services.WADO.RetrieveMetadata = function(server, studyInstanceUid) {
+
+ var url = buildUrl(server, studyInstanceUid);
+
+ var result = DICOMWeb.getJSON(url, server.requestOptions);
+
+ var study = {
+ wadoUriRoot: server.wadoUriRoot,
+ studyInstanceUid: studyInstanceUid,
+ seriesList: resultDataToStudyMetadata(server, studyInstanceUid, result.data)
+ };
+
+ return study;
+};
\ No newline at end of file
diff --git a/Packages/worklist/components/worklist.html b/Packages/worklist/components/worklist.html
new file mode 100644
index 000000000..a2992764f
--- /dev/null
+++ b/Packages/worklist/components/worklist.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ {{> worklistSearch }}
+ {{> worklistResult }}
+
+
+
+
\ No newline at end of file
diff --git a/Packages/worklist/components/worklist.js b/Packages/worklist/components/worklist.js
new file mode 100644
index 000000000..ffdf852c7
--- /dev/null
+++ b/Packages/worklist/components/worklist.js
@@ -0,0 +1,110 @@
+var contentID = "";
+
+//Generate UUID to create unique tabs
+function generateUUID () {
+ var d = new Date().getTime();
+ var uuid = 'xxxxxxxx'.replace(/[xy]/g, function(c) {
+ var r = (d + Math.random()*8)%8 | 0;
+ d = Math.floor(d/8);
+ return (c=='x' ? r : (r&0x3|0x8)).toString(8);
+ });
+ return uuid;
+}
+
+//Remove tab
+function removeTab (uuid) {
+ var removedTabIndex = $("#tab"+uuid).index();
+ //Remove tab
+ $("#tab"+uuid).remove();
+ //Remove Content
+ $("#content"+uuid).remove();
+
+ //TODO:Activate previous tab
+
+}
+
+//Add new tab
+function addNewTab (data) {
+ //Make inactive all tabs
+ $("#tabs > li").removeClass("active");
+ $("#tabs-content > div").removeClass("active");
+
+ //Create new li element for tab
+ var tabli = document.createElement("li");
+ $(tabli).addClass("active");
+ $("#tabs").append(tabli);
+
+ //a element inside li
+ var taba = document.createElement("a");
+ var uuid = generateUUID();
+ var tabId = "tab"+uuid;
+
+
+ //activeTabId provides unique tabs content and encapsulates templates in tabs
+ Session.set("activeTabId", tabId);
+ console.log(Session.get("activeTabId"));
+
+ //Create tab
+ taba.setAttribute("data-toggle","tab");
+ taba.setAttribute("id",tabId);
+ taba.innerHTML = data.patientName;
+ tabli.appendChild(taba);
+
+ //Create close button
+ var btnClose = document.createElement('button');
+ $(btnClose).addClass("btnClose");
+ btnClose.innerHTML = "x";
+ btnClose.onclick = function() { // Note this is a function
+ removeTab(uuid);
+ };
+ taba.appendChild(btnClose);
+
+ //Create div content
+ var tabContent = document.createElement("div");
+ var contentId = "content"+uuid;
+ tabContent.setAttribute("id",contentId);
+ $(tabContent).css("background-color", "black");
+ $(tabContent).addClass("tab-pane");
+ $(tabContent).addClass("active");
+ $("#tabs-content").append(tabContent);
+
+ contentID = contentId;
+}
+
+function insertContent () {
+
+ //Include imageViewer template
+ UI.insert( UI.render( Template.viewer ), $( '#'+contentID ).get(0) );
+}
+
+Template.worklist.events({
+ 'click ul#tabs>li>a': function (event){
+ var tabId = $(event.target).attr("id");
+ if ( tabId !== undefined ) {
+ //Save activeTabId to load correct study for each tab
+ Session.set("activeTabId", tabId);
+
+ var splitTabId = tabId.split("tab");
+ var contentId = "content"+splitTabId[1];
+
+ //Make inactive all tab-pane divs
+ $("#tabs-content>div").removeClass("active");
+ $("#"+contentId).addClass("active");
+ }
+ }
+});
+
+Tracker.autorun(function () {
+ var data = Session.get('openNewTabEvent');
+ var content = Session.get('showContentInTab');
+ if ( data ) {
+ addNewTab(data);
+ }
+
+ if (content) {
+ insertContent();
+ }
+
+ Session.set('openNewTabEvent', false);
+ Session.set('showContentInTab', false);
+});
\ No newline at end of file
diff --git a/Packages/worklist/components/worklist.styl b/Packages/worklist/components/worklist.styl
new file mode 100644
index 000000000..eaae0592b
--- /dev/null
+++ b/Packages/worklist/components/worklist.styl
@@ -0,0 +1,50 @@
+html, body
+ height: 100%
+ margin: 0
+ padding: 0
+ background-color: white
+ color: black
+
+
+div
+ margin: 0
+ padding: 0
+
+
+/***********************************************/
+/* CSS3 Close button */
+/***********************************************/
+
+.btnClose
+ top: 0
+ right: 0
+ bottom: 0
+ width: 20px
+ height: 20px
+ text-decoration: none
+ border: none
+ background-color: #fcfcfc
+ -moz-border-radius: 10px
+ -webkit-border-radius: 10px
+ border-radius: 10px
+ margin-left: 5px
+ font-weight: bold
+
+.tabbable
+ cursor: pointer
+ position: absolute
+ top:0
+ left:0
+ bottom: 0
+ right: 0
+ width: 100%
+ height: 100%
+ color: black
+
+.tab-content
+ position: absolute
+ top: 42px
+ left: 0
+ right: 0
+ bottom: 3px
+ width: 100%
\ No newline at end of file
diff --git a/Packages/worklist/components/worklistResult/worklistResult.html b/Packages/worklist/components/worklistResult/worklistResult.html
new file mode 100644
index 000000000..2233871c4
--- /dev/null
+++ b/Packages/worklist/components/worklistResult/worklistResult.html
@@ -0,0 +1,20 @@
+
+
+
+
+ Patient Name
+ Patient ID
+ Accession#
+ Study Date
+ Modality
+ Study Description
+ # Images
+
+
+
+ {{#each studies}}
+ {{> worklistStudy }}
+ {{/each}}
+
+
+
\ No newline at end of file
diff --git a/Packages/worklist/components/worklistResult/worklistResult.js b/Packages/worklist/components/worklistResult/worklistResult.js
new file mode 100644
index 000000000..56a198edc
--- /dev/null
+++ b/Packages/worklist/components/worklistResult/worklistResult.js
@@ -0,0 +1,7 @@
+Studies = new Mongo.Collection(null);
+
+Template.worklistResult.helpers({
+ studies : function() {
+ return Studies.find();
+ }
+});
\ No newline at end of file
diff --git a/Packages/worklist/components/worklistResult/worklistResult.styl b/Packages/worklist/components/worklistResult/worklistResult.styl
new file mode 100644
index 000000000..68c9024ed
--- /dev/null
+++ b/Packages/worklist/components/worklistResult/worklistResult.styl
@@ -0,0 +1,6 @@
+table#tblStudyList
+ color: black
+
+ tbody tr:hover
+ background-color: #009BD2
+ color: white
\ No newline at end of file
diff --git a/Packages/worklist/components/worklistSearch/worklistSearch.html b/Packages/worklist/components/worklistSearch/worklistSearch.html
new file mode 100644
index 000000000..318eefc90
--- /dev/null
+++ b/Packages/worklist/components/worklistSearch/worklistSearch.html
@@ -0,0 +1,88 @@
+
+
+
+
Search
+
+
+
+
+
+
+
+
+
+
+ Study Description
+
+
+
+
+
+
+
+
+
+
+ Referring Physician
+
+
+
+
+
+
+
+
+
+
+ Search
+
+
+ Today
+
+
+
+
+ Clear
+
+
+ Last 7 Days
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Packages/worklist/components/worklistSearch/worklistSearch.js b/Packages/worklist/components/worklistSearch/worklistSearch.js
new file mode 100644
index 000000000..af31b5f18
--- /dev/null
+++ b/Packages/worklist/components/worklistSearch/worklistSearch.js
@@ -0,0 +1,124 @@
+//Get all studies
+search();
+
+var studyDateFrom;
+var studyDateTo;
+var checkFrom = false;
+var checkTo = false;
+
+function getFilter(filter) {
+ if(filter && filter.length && filter.substr(filter.length - 1) !== '*') {
+ filter += '*';
+ }
+ return filter;
+}
+
+//Search value
+function isIndexOf(mainVal, searchVal) {
+ if(mainVal === undefined || mainVal === '' || mainVal.indexOf(searchVal) > -1){
+ return true;
+ }
+
+ return false;
+}
+
+//Replace object is undefined
+function replaceUndefinedColumnValue (text) {
+ if (text == undefined || text === "undefined") {
+ return "";
+ } else {
+ return text;
+ }
+}
+
+//Convert string to study date
+function convertStringToStudyDate (dateStr) {
+ var y = dateStr.substring(0,4);
+ var m = dateStr.substring(4,6);
+ var d = dateStr.substring(6,8);
+ var newDateStr = y+"/"+m+"/"+d;
+ var date_ = new Date(newDateStr);
+ return date_;
+}
+
+function search() {
+ var filter = {
+ patientName: getFilter($('#patientName').val()),
+ patientId: getFilter($('#patientId').val()),
+ accessionNumber: getFilter($('#patientAccessionNumber').val())
+ };
+
+ var modality = replaceUndefinedColumnValue($('#modality').val());
+
+ Studies.remove({});
+ Meteor.call('WorklistSearch', filter, function(error, studies) {
+ if (studies) {
+ studies.forEach(function(study) {
+
+ if(isIndexOf(study.modalities, modality) &&
+ (new Date(studyDateFrom).setHours(0,0,0,0) <= convertStringToStudyDate(study.studyDate) || !checkFrom) &&
+ (convertStringToStudyDate(study.studyDate) <= new Date(studyDateTo).setHours(0,0,0,0) || !checkTo)) {
+
+ Studies.insert(study);
+ }
+ });
+ }
+ });
+
+}
+
+Template.worklistSearch.events({
+
+ //Search
+ 'click button#btnSearch' :function(event) {
+ studyDateFrom = $('#studyDateFrom').val();
+ studyDateTo = $('#studyDateTo').val();
+ checkFrom = $('#checkFrom').is(':checked');
+ checkTo = $('#checkTo').is(':checked');
+ search();
+
+ return false;
+ },
+
+ //Today
+ 'click button#btnToday' :function(event) {
+ studyDateTo = new Date();
+ studyDateFrom = studyDateTo;
+ checkFrom = true;
+ checkTo = true;
+
+ search();
+
+ return false;
+ },
+
+ //Clear
+ 'click button#btnClear' :function(event) {
+ $("#patientId").val("");
+ $("#patientName").val("");
+ $("#patientAccessionNumber").val("");
+ $("#studyDescription").val("");
+ $("#referringPhysician").val("");
+ $("#studyDateFrom").val("");
+ $("#studyDateTo").val("");
+ $("#modality").val("");
+ $('#checkFrom').prop('checked', false);
+ $('#checkTo').prop('checked', false);
+
+ },
+
+ //Last 7 Days
+ 'click button#btnLastSevenDays' :function(event) {
+ studyDateTo = new Date();
+ studyDateFrom = new Date();
+ studyDateFrom.setDate(studyDateFrom.getDate()-7);
+ checkFrom = true;
+ checkTo = true;
+
+ search();
+
+ return false;
+ }
+
+
+});
\ No newline at end of file
diff --git a/Packages/worklist/components/worklistSearch/worklistSearch.styl b/Packages/worklist/components/worklistSearch/worklistSearch.styl
new file mode 100644
index 000000000..afbf5659b
--- /dev/null
+++ b/Packages/worklist/components/worklistSearch/worklistSearch.styl
@@ -0,0 +1,35 @@
+/***********************************************/
+/* Search Panel */
+/***********************************************/
+#search-panel-head{
+ background-color: #337ab7;
+ color: white;
+ font-weight: 500;
+ font-size: 20px;
+ line-height: 20px;
+ box-shadow: 2px 2px 2px #888888;
+}
+
+#btnSearch, #btnToday, #btnClear, #btnLastSevenDays{
+ width: 100%;
+}
+
+#checkFrom, #checkTo{
+ width:20px;
+ height:20px;
+ position:absolute;
+ left:20px;
+ bottom: 22px;
+}
+/***********************************************/
+/* Tabs */
+/***********************************************/
+.tab-pane{
+ position:absolute;
+ width: 100%;
+ height:100%;
+}
+
+.nav > li > a {
+ padding: 10px 5px;
+}
\ No newline at end of file
diff --git a/Packages/worklist/components/worklistStudy/worklistStudy.html b/Packages/worklist/components/worklistStudy/worklistStudy.html
new file mode 100644
index 000000000..0ba4f96d6
--- /dev/null
+++ b/Packages/worklist/components/worklistStudy/worklistStudy.html
@@ -0,0 +1,25 @@
+
+
+
+ {{patientName}}
+
+
+ {{patientId}}
+
+
+ {{accessionNumber}}
+
+
+ {{studyDate}}
+
+
+ {{modalities}}
+
+
+ {{studyDescription}}
+
+
+ {{imageCount}}
+
+
+
\ No newline at end of file
diff --git a/Packages/worklist/components/worklistStudy/worklistStudy.js b/Packages/worklist/components/worklistStudy/worklistStudy.js
new file mode 100644
index 000000000..389460128
--- /dev/null
+++ b/Packages/worklist/components/worklistStudy/worklistStudy.js
@@ -0,0 +1,5 @@
+Template.worklistStudy.events({
+ 'click': function (){
+ Router.go('viewer', {_id: this.studyInstanceUid});
+ }
+});
\ No newline at end of file
diff --git a/Packages/worklist/components/worklistStudy/worklistStudy.styl b/Packages/worklist/components/worklistStudy/worklistStudy.styl
new file mode 100644
index 000000000..0519ecba6
--- /dev/null
+++ b/Packages/worklist/components/worklistStudy/worklistStudy.styl
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Packages/worklist/package.js b/Packages/worklist/package.js
new file mode 100644
index 000000000..267373698
--- /dev/null
+++ b/Packages/worklist/package.js
@@ -0,0 +1,31 @@
+Package.describe({
+ name: "worklist",
+ summary: "Basic worklist for web-based DICOM viewers",
+ version: '0.0.1'
+});
+
+Package.onUse(function (api) {
+ api.versionsFrom('1.2.0.2');
+
+ api.use('standard-app-packages');
+ api.use('jquery');
+ api.use('stylus');
+
+ // Components
+ api.addFiles('components/worklist.html', 'client');
+ api.addFiles('components/worklist.js', 'client');
+ api.addFiles('components/worklist.styl', 'client');
+
+ api.addFiles('components/worklistStudy/worklistStudy.html', 'client');
+ api.addFiles('components/worklistStudy/worklistStudy.js', 'client');
+ api.addFiles('components/worklistStudy/worklistStudy.styl', 'client');
+
+ api.addFiles('components/worklistResult/worklistResult.html', 'client');
+ api.addFiles('components/worklistResult/worklistResult.js', 'client');
+ api.addFiles('components/worklistResult/worklistResult.styl', 'client');
+
+ api.addFiles('components/worklistSearch/worklistSearch.html', 'client');
+ api.addFiles('components/worklistSearch/worklistSearch.js', 'client');
+ api.addFiles('components/worklistSearch/worklistSearch.styl', 'client');
+});
+
diff --git a/config/localhostOrthanc.json b/config/localhostOrthanc.json
new file mode 100644
index 000000000..0ded25ff0
--- /dev/null
+++ b/config/localhostOrthanc.json
@@ -0,0 +1,21 @@
+{
+ "dicomWeb" : {
+ "endpoints": [
+ {
+ "name": "Orthanc",
+ "wadoUriRootNOTE" : "either this uri is not correct for wado-uri or wado-uri is not configured on orthanc currently",
+ "wadoUriRoot" : "http://localhost:8042/wado",
+ "qidoRoot": "http://localhost:8042/dicom-web",
+ "wadoRoot": "http://localhost:8042/dicom-web",
+ "qidoSupportsIncludeField": false,
+ "imageRendering" : "wadouri",
+ "requestOptions" : {
+ "auth": "orthanc:orthanc",
+ "logRequests" : true,
+ "logResponses" : false,
+ "logTiming" : true
+ }
+ }
+ ]
+ }
+}
diff --git a/config/medicalConnections.json b/config/medicalConnections.json
new file mode 100644
index 000000000..faf254197
--- /dev/null
+++ b/config/medicalConnections.json
@@ -0,0 +1,19 @@
+{
+ "dicomWeb" : {
+ "endpoints": [
+ {
+ "name": "Medical Connections",
+ "wadoUriRoot" : "http://www.dicomserver.co.uk/wadouri",
+ "qidoRoot": "http://dicomserver.co.uk:81/qido",
+ "wadoRoot": "http://dicomserver.co.uk:81/wado",
+ "qidoSupportsIncludeField": false,
+ "imageRendering" : "wadouri",
+ "requestOptions" : {
+ "logRequests" : true,
+ "logResponses" : false,
+ "logTiming" : true
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/config/medkenOrthanc.json b/config/medkenOrthanc.json
new file mode 100644
index 000000000..8c142d90b
--- /dev/null
+++ b/config/medkenOrthanc.json
@@ -0,0 +1,20 @@
+{
+ "dicomWeb" : {
+ "endpoints": [
+ {
+ "name": "Medken Orthanc",
+ "wadoUriRootNOTE" : "either this uri is not correct for wado-uri or wado-uri is not configured on orthanc currently",
+ "wadoUriRoot" : "http://dicom-orthanc.medken.com/wado",
+ "qidoRoot": "http://dicom-orthanc.medken.com/dicom-web",
+ "wadoRoot": "http://dicom-orthanc.medken.com/dicom-web",
+ "qidoSupportsIncludeField": false,
+ "imageRendering" : "wadouri",
+ "requestOptions" : {
+ "logRequests" : false,
+ "logResponses" : false,
+ "logTiming" : true
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/config/siimDCM4CHEE.json b/config/siimDCM4CHEE.json
new file mode 100644
index 000000000..30408e8f8
--- /dev/null
+++ b/config/siimDCM4CHEE.json
@@ -0,0 +1,19 @@
+{
+ "dicomWeb": {
+ "endpoints": [
+ {
+ "name": "SIIM DCM4CHEE",
+ "wadoUriRoot" : "http://vna.hackathon.siim.org/dcm4chee-arc/wado/DCM4CHEE",
+ "qidoRoot": "http://vna.hackathon.siim.org/dcm4chee-arc/qido/DCM4CHEE",
+ "wadoRoot": "http://vna.hackathon.siim.org/dcm4chee-arc/wado/DCM4CHEE",
+ "qidoSupportsIncludeField": true,
+ "imageRendering" : "wadouri",
+ "requestOptions" : {
+ "logRequests" : false,
+ "logResponses" : false,
+ "logTiming" : true
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file