diff --git a/LesionTracker/client/components/viewer.js b/LesionTracker/client/components/viewer.js
index 5a1c78445..5980eb6c4 100644
--- a/LesionTracker/client/components/viewer.js
+++ b/LesionTracker/client/components/viewer.js
@@ -3,10 +3,8 @@ Template.viewer.onCreated(function() {
$(window).on('resize', handleResize);
var self = this;
-
var firstMeasurementsActivated = false;
-
- log.info('viewer onCreated');
+ var contentId = this.data.contentId;
OHIF = OHIF || {
viewer: {}
@@ -16,7 +14,6 @@ Template.viewer.onCreated(function() {
OHIF.viewer.defaultTool = 'wwwc';
OHIF.viewer.refLinesEnabled = true;
OHIF.viewer.isPlaying = {};
- var contentId = this.data.contentId;
OHIF.viewer.functionList = {
invert: function(element) {
@@ -42,15 +39,17 @@ Template.viewer.onCreated(function() {
toggleLesionTrackerTools: toggleLesionTrackerTools,
clearTools: clearTools,
lesion: function() {
+ // Used for hotkeys
toolManager.setActiveTool('lesion');
},
nonTarget: function() {
+ // Used for hotkeys
toolManager.setActiveTool('nonTarget');
}
-
};
// The hotkey can also be an array (e.g. ["NUMPAD0", "0"])
+ OHIF.viewer.defaultHotkeys = OHIF.viewer.defaultHotkeys || {};
OHIF.viewer.defaultHotkeys.toggleLesionTrackerTools = 'O';
OHIF.viewer.defaultHotkeys.lesion = 'T'; // Target
OHIF.viewer.defaultHotkeys.nonTarget = 'N'; // Non-target
@@ -65,6 +64,10 @@ Template.viewer.onCreated(function() {
};
}
+ OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('CornerstoneNewImage', cornerstoneTools.updateImageSynchronizer);
+
+ log.info('viewer onCreated');
+
if (ViewerData[contentId].loadedSeriesData) {
log.info('Reloading previous loadedSeriesData');
OHIF.viewer.loadedSeriesData = ViewerData[contentId].loadedSeriesData;
@@ -76,8 +79,15 @@ Template.viewer.onCreated(function() {
ViewerData[contentId].loadedSeriesData = OHIF.viewer.loadedSeriesData;
// Update the viewer data object
- ViewerData[contentId].viewportColumns = 2;
- ViewerData[contentId].viewportRows = 1;
+ if (!this.data.timepointIds || this.data.timepointIds.length <= 1) {
+ // Update the viewer data object
+ ViewerData[contentId].viewportColumns = 1;
+ ViewerData[contentId].viewportRows = 1;
+ } else if (this.data.timepointIds.length > 1) {
+ ViewerData[contentId].viewportColumns = 2;
+ ViewerData[contentId].viewportRows = 1;
+ }
+
ViewerData[contentId].activeViewport = 0;
Session.set('ViewerData', ViewerData);
}
@@ -96,43 +106,29 @@ Template.viewer.onCreated(function() {
self.autorun(function() {
var patientId = Session.get('patientId');
- self.subscribe('timepoints', patientId);
- self.subscribe('measurements', patientId);
+ self.subscribe('singlePatientTimepoints', patientId);
+ self.subscribe('singlePatientMeasurements', patientId);
if (self.subscriptionsReady()) {
ViewerStudies.find().observe({
added: function(study) {
- // TODO = Replace this whole section when we have a
- // timepoint-to-study association modal
-
- // First, check if we have a timepoint related to this
- // study date already
+ // Find the relevant timepoint given the newly added study
var timepoint = Timepoints.findOne({
- timepointName: study.studyDate
+ studyInstanceUids: {
+ $in: [study.studyInstanceUid]
+ }
});
- // If we do, stop here
- if (timepoint) {
- log.warn('A timepoint with that study date already exists!');
+ if (!timepoint) {
+ log.warn('Study added to Viewer has not been associated!');
return;
}
-
- // Next, check the first timepoint in the collection
- var testTimepoint = Timepoints.findOne({});
-
- // If it relates to another subject, we need to stop here as well
- // because the tab may be changing.
- if (testTimepoint && testTimepoint.patientId !== study.patientId) {
- log.warn('Timepoints collection related to the wrong subject');
- return;
- }
-
- // Otherwise, we need to add a timepoint related to this study date
- log.info('Inserting a new timepoint');
- Timepoints.insert({
- patientId: study.patientId,
- timepointName: study.studyDate,
- timepointID: uuid.v4()
+
+ // Update the added document with its related timepointId
+ ViewerStudies.update(study._id, {
+ $set: {
+ timepointId: timepoint.timepointId
+ }
});
}
});
@@ -159,12 +155,17 @@ Template.viewer.onCreated(function() {
log.info('Measurement added');
syncMeasurementAndToolData(data);
updateRelatedElements(data.imageId);
+
+ TrialResponseCriteria.validateAll();
+ },
+ changed: function(data) {
+ TrialResponseCriteria.validateAllDelayed();
},
removed: function(data) {
log.info('Measurement removed');
// Check that this Measurement actually contains timepoint data
- if (!data.timepoints) {
+ if (!data || !data.timepoints) {
return;
}
@@ -175,9 +176,9 @@ Template.viewer.onCreated(function() {
// Remove the measurement from all the imageIds on which it exists
// as toolData
- Object.keys(data.timepoints).forEach(function(timepointID) {
+ Object.keys(data.timepoints).forEach(function(timepointId) {
// Clear the toolData for this timepoint
- var imageId = data.timepoints[timepointID].imageId;
+ var imageId = data.timepoints[timepointId].imageId;
removeToolDataWithMeasurementId(imageId, toolType, measurementId);
});
@@ -202,146 +203,29 @@ Template.viewer.onCreated(function() {
viewports.each(function(index, element) {
cornerstone.updateImage(element);
});
+
+ ValidationErrors.remove({
+ measurementId: data._id
+ });
+
+ TrialResponseCriteria.validateAll();
}
});
}
});
-
- OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('CornerstoneNewImage', cornerstoneTools.updateImageSynchronizer);
});
-function updateRelatedElements(imageId) {
- // Get all on-screen elements with this imageId
- var enabledElements = cornerstone.getEnabledElementsByImageId(imageId);
-
- // TODO=Check original event to prevent duplicate updateImage calls
-
- // Loop through these elements
- enabledElements.forEach(function(enabledElement) {
- // Update the display so the tool is removed
- var element = enabledElement.element;
- cornerstone.updateImage(element);
- });
-}
-
-function syncMeasurementAndToolData(data) {
- // Check what toolType we should be adding this to, based on the isTarget value
- // of the stored Measurement
- var toolType = data.isTarget ? 'lesion' : 'nonTarget';
- var toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.toolState;
-
- // Loop through the timepoint data for this measurement
- Object.keys(data.timepoints).forEach(function(key) {
- var storedData = data.timepoints[key];
- var imageId = storedData.imageId;
-
- if (!toolState[imageId]) {
- toolState[imageId] = {};
- }
-
- // This is probably not the best approach to prevent duplicates
- if (toolState[imageId][toolType] && toolState[imageId][toolType].data) {
- var measurementHasNoIdYet = false;
- toolState[imageId][toolType].data.forEach(function(measurement) {
- if (measurement.id === 'notready') {
- measurementHasNoIdYet = true;
- return false;
- }
- });
-
- // Stop here if it appears that we are creating this measurement right now,
- // and would not like this function to add another copy of it to the toolData
- if (measurementHasNoIdYet === true) {
- return;
- }
- }
-
- if (!toolState[imageId][toolType]) {
- toolState[imageId][toolType] = {
- data: []
- };
- } else {
- var alreadyExists = false;
- if (toolState[imageId][toolType].data.length) {
- toolState[imageId][toolType].data.forEach(function(measurement) {
- if (measurement.id === data._id) {
- alreadyExists = true;
-
- // Update the toolData lesionNumber from the Measurement
- measurement.lesionNumber = data.lesionNumber;
- return false;
- }
- });
- }
-
- if (alreadyExists === true) {
- return;
- }
- }
-
- // Create measurementData structure based on the lesion data at this timepoint
- // We will add this into the toolData for this imageId
- var measurementData = storedData;
- measurementData.isTarget = data.isTarget;
- measurementData.lesionNumber = data.lesionNumber;
- measurementData.measurementText = data.measurementText;
- measurementData.isDeleted = data.isDeleted;
- measurementData.location = data.location;
- measurementData.locationUID = data.locationUID;
- measurementData.patientId = patientId;
- measurementData.visible = data.visible;
- measurementData.active = data.active;
- measurementData.uid = data.uid;
- measurementData.id = data._id;
-
- toolState[imageId][toolType].data.push(measurementData);
- });
-}
-
Template.viewer.onRendered(function() {
// Enable hotkeys
enableHotkeys();
- // Set lesion tool buttons as disable if pixel spacing is not available for active element
- this.autorun(function(){
- if (!Session.get('ViewerData') || Session.get('activeViewport') === undefined) {
- return;
- }
-
- var activeViewportIndex = Session.get('activeViewport');
- var viewports = $(".imageViewerViewport");
- var element = viewports.get(activeViewportIndex);
-
- // Check element has .empty class
- if (element.classList.contains('empty')) {
- setLesionToolButtonsDisable(true);
- return;
- }
-
- try {
- var enabledElement = cornerstone.getEnabledElement(element);
- if (!enabledElement || !enabledElement.image ||
- !enabledElement.image.rowPixelSpacing ||
- !enabledElement.image.columnPixelSpacing) {
- // Disable Lesion Tools and Buttons
- toolManager.setActiveTool("wwwc", viewports);
- cornerstoneTools.lesion.disable(element);
- cornerstoneTools.nonTarget.disable(element);
- setLesionToolButtonsDisable(true);
- } else{
- // Enable Lesion Buttons
- setLesionToolButtonsDisable(false);
- }
- } catch(error) {
- return;
- }
- });
-
+ // Set lesion tool buttons as disabled if pixel spacing is not available for active element
+ this.autorun(pixelSpacingAutorunCheck);
});
Template.viewer.onDestroyed(function() {
log.info('onDestroyed');
- console.log("viewer destroyed!");
+ console.log('viewer destroyed!');
// Remove the Window resize listener
$(window).off('resize', handleResize);
@@ -350,6 +234,9 @@ Template.viewer.onDestroyed(function() {
});
Template.viewer.events({
+ 'CornerstoneToolsMeasurementAdded .imageViewerViewport': function(e, template, eventData) {
+ handleMeasurementAdded(e, eventData);
+ },
'CornerstoneToolsMeasurementModified .imageViewerViewport': function(e, template, eventData) {
handleMeasurementModified(e, eventData);
},
@@ -357,43 +244,3 @@ Template.viewer.events({
handleMeasurementRemoved(e, eventData);
}
});
-
-function handleMeasurementModified(e, eventData) {
- log.info('CornerstoneToolsMeasurementModified');
- var measurementData = eventData.measurementData;
-
- switch (eventData.toolType) {
- case 'nonTarget':
- case 'lesion':
- LesionManager.updateLesionData(measurementData);
- break;
- }
-}
-
-function handleMeasurementRemoved(e, eventData) {
- log.info('CornerstoneToolsMeasurementRemoved');
- var measurementData = eventData.measurementData;
-
- switch (eventData.toolType) {
- case 'nonTarget':
- case 'lesion':
- var measurement = Measurements.findOne(measurementData.id, {
- reactive: false
- });
-
- if (!measurement) {
- return;
- }
-
- clearMeasurementTimepointData(measurement._id, measurementData.timepointID);
- break;
- }
-}
-
-// Set enablement of Lesion Tools Buttons
-function setLesionToolButtonsDisable (status) {
- var buttons = [$("button#lesion"), $("button#nonTarget")];
- buttons.forEach(function(btn){
- btn.prop("disabled", status);
- });
-}
diff --git a/LesionTracker/client/mobileStyles.css b/LesionTracker/client/mobileStyles.css
deleted file mode 100644
index 08117e848..000000000
--- a/LesionTracker/client/mobileStyles.css
+++ /dev/null
@@ -1,51 +0,0 @@
-
-/* Mobile devices */
-
-@media only screen and (min-device-width : 375px) and (max-device-width : 768px){
-
- .logoContainer {
- width: 15%;
- }
-
- .navbar-brand img {
- height: 20px;
- margin-top: 10px;
- margin-bottom: 10px;
- margin-right: 5px;
- }
-
- .navbar-brand h4.name {
- padding: 1px;
- font-size: 0.3em;
- width: 50%;;
- }
-
- table#tblStudyList {
- table-layout: fixed;
- font-size: 0.7em;
- }
-
- #tablist {
- width: 85%;
- overflow-x: auto;
- }
-
- #toolbar .btn-group button {
- width: 40px;
- height: 40px;
- }
-
- .studyDateList {
- margin-top: 5px;
- }
-}
-
-
-@media (max-device-width: 667px) {
-
- #toolbar .btn-group button {
- width: 35px;
- height: 35px;
- }
-}
-
diff --git a/LesionTracker/client/routeHooks.js b/LesionTracker/client/routeHooks.js
deleted file mode 100644
index a58d7db8d..000000000
--- a/LesionTracker/client/routeHooks.js
+++ /dev/null
@@ -1,5 +0,0 @@
-Router.onBeforeAction(function() {
-
- // User is logged in, go ahead and route them
- this.next();
-});
diff --git a/LesionTracker/client/routes.js b/LesionTracker/client/routes.js
index 85846aea1..70986b89a 100644
--- a/LesionTracker/client/routes.js
+++ b/LesionTracker/client/routes.js
@@ -11,15 +11,17 @@ Object.keys(ViewerData).forEach(function(contentId) {
});
Router.configure({
- layoutTemplate: 'layoutLesionTracker',
- loadingTemplate: 'layoutLesionTracker'
+ layoutTemplate: 'lesionTrackerLayout',
+ loadingTemplate: 'lesionTrackerLayout',
+ notFoundTemplate: 'notFound'
});
Router.onBeforeAction('loading');
var data = {
additionalTemplates: [
- 'associationModal'
+ 'associationModal',
+ 'optionsModal'
]
};
@@ -35,24 +37,22 @@ Router.route('/worklist', function() {
this.render('worklist', routerOptions);
});
-Router.route('/viewer/:_id', {
- layoutTemplate: 'layoutLesionTracker',
+Router.route('/viewer/timepoints/:_id', {
+ layoutTemplate: 'lesionTrackerLayout',
name: 'viewer',
onBeforeAction: function() {
- log.info('Router GetStudyMetadata');
+ var timepointId = this.params._id;
- var studyInstanceUid = this.params._id;
-
// Check if this study is already loaded in a tab
// If it is, stop here so we don't keep adding tabs on hot-code reloads
- var tab = WorklistTabs.find({
- studyInstanceUid: studyInstanceUid
- }).fetch();
+ var tab = WorklistTabs.findOne({
+ timepointId: timepointId
+ });
if (tab) {
return;
}
this.render('worklist', routerOptions);
- openNewTab(studyInstanceUid);
+ openNewTabWithTimepoint(timepointId);
}
});
diff --git a/LesionTracker/client/stylesheets/docking-container.css b/LesionTracker/client/stylesheets/docking-container.css
deleted file mode 100644
index fa15baec2..000000000
--- a/LesionTracker/client/stylesheets/docking-container.css
+++ /dev/null
@@ -1,55 +0,0 @@
-/*****************************/
-/* Toggle */
-/*****************************/
-.dockingContainer, .collapseVertical, .collapseHorizontal {
- -o-transition: all 0.3s ease-out;
- -ms-transition: all 0.3s ease-out;
- -moz-transition: all 0.3s ease-out;
- -webkit-transition: all 0.3s ease-out;
- transition: all 0.3s ease-out;
-}
-
-.dockingContainer {
- -o-transition: all 0.3s ease-out;
- -ms-transition: all 0.3s ease-out;
- -moz-transition: all 0.3s ease-out;
- -webkit-transition: all 0.3s ease-out;
- transition: all 0.3s ease-out;
-}
-
-.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);
- -ms-transform: rotate(180deg);
- -o-transform: rotate(180deg);
- -webkit-transform: rotate(180deg);
- transform: rotate(180deg);
-}
diff --git a/LesionTracker/defaultSettings.js b/LesionTracker/defaultSettings.js
index 9c1345759..54376121a 100644
--- a/LesionTracker/defaultSettings.js
+++ b/LesionTracker/defaultSettings.js
@@ -1,30 +1,30 @@
Meteor.startup(function() {
- if (Meteor.settings.dicomWeb) {
- console.log('dicomWeb settings defined!');
- console.log(Meteor.settings);
- return;
- }
-
- console.log('Using default LesionTracker dicomWeb settings');
Meteor.settings = {
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:8043/wado',
- qidoRoot: 'http://localhost:8042/dicom-web',
- wadoRoot: 'http://localhost:8042/dicom-web',
- qidoSupportsIncludeField: false,
- imageRendering: 'wadouri',
- requestOptions: {
+ 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:8043/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
}
- }
- ]
- }
+ }]
+ },
+ dimse: {
+ host: 'localhost',
+ port: 4242,
+ hostAE: 'ORTHANC'
+ },
+ defaultServiceType: 'dicomWeb'
+ //defaultServiceType: 'dimse'
};
+
+ console.log('Using default LesionTracker settings with service: ' + Meteor.settings.defaultServiceType);
});
diff --git a/OHIFViewer/.meteor/versions b/OHIFViewer/.meteor/versions
index 1930c6f09..e52681900 100644
--- a/OHIFViewer/.meteor/versions
+++ b/OHIFViewer/.meteor/versions
@@ -77,6 +77,7 @@ reactive-var@1.0.6
reload@1.1.4
retry@1.0.4
routepolicy@1.0.6
+rwatts:uuid@0.0.2
service-configuration@1.0.5
session@1.1.1
sha@1.0.4
diff --git a/Packages/cornerstone/client/cornerstoneWADOImageLoader.js b/Packages/cornerstone/client/cornerstoneWADOImageLoader.js
index 6ede61230..29390416b 100644
--- a/Packages/cornerstone/client/cornerstoneWADOImageLoader.js
+++ b/Packages/cornerstone/client/cornerstoneWADOImageLoader.js
@@ -1,4 +1,4 @@
-/*! cornerstone-wado-image-loader - v0.7.2 - 2015-09-18 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneWADOImageLoader */
+/*! cornerstone-wado-image-loader - v0.8.1 - 2016-02-07 | (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:
@@ -243,7 +243,7 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){
"use strict";
- function createImageObject( dataSet, imageId, frame ) {
+ function createImageObject( dataSet, imageId, frame, sharedCacheKey ) {
if(frame === undefined) {
frame = 0;
}
@@ -252,9 +252,9 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){
var photometricInterpretation = dataSet.string('x00280004');
var isColor = cornerstoneWADOImageLoader.isColorImage(photometricInterpretation);
if(isColor === false) {
- return cornerstoneWADOImageLoader.makeGrayscaleImage(imageId, dataSet, frame);
+ return cornerstoneWADOImageLoader.makeGrayscaleImage(imageId, dataSet, frame, sharedCacheKey);
} else {
- return cornerstoneWADOImageLoader.makeColorImage(imageId, dataSet, frame);
+ return cornerstoneWADOImageLoader.makeColorImage(imageId, dataSet, frame, sharedCacheKey);
}
}
@@ -2213,37 +2213,37 @@ var JpegImage = (function jpegImage() {
-// 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.
+// 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
+// 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
+// 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 */
@@ -2800,13 +2800,13 @@ var JpegImage = (function jpegImage() {
-// 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.
+// 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 */
@@ -3316,118 +3316,6 @@ var JpegImage = (function jpegImage() {
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";
@@ -3561,299 +3449,116 @@ var JpegImage = (function jpegImage() {
// module exports
cornerstoneWADOImageLoader.getWindowWidthAndCenter = getWindowWidthAndCenter;
}(cornerstoneWADOImageLoader));
-(function (cornerstoneWADOImageLoader) {
+
+(function ($, cornerstone, cornerstoneWADOImageLoader) {
"use strict";
- var options = {
- // callback allowing customization of the xhr (e.g. adding custom auth headers, cors, etc)
- beforeSend : function(xhr) {}
- };
+ function loadImage(imageId) {
+ // create a deferred object
+ var deferred = $.Deferred();
- function configure(opts) {
- options = opts;
- }
+ // 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);
+ }
- 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")
+ // if multiframe and cached, use the cached data set to extract the frame
+ if(frame !== undefined &&
+ cornerstoneWADOImageLoader.internal.multiFrameCacheHack.hasOwnProperty(url))
{
- 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;
- }
-
+ var dataSet = cornerstoneWADOImageLoader.internal.multiFrameCacheHack[url];
+ var imagePromise = cornerstoneWADOImageLoader.createImageObject(dataSet, imageId, frame);
+ imagePromise.then(function(image) {
deferred.resolve(image);
- return deferred;
+ }, function(error) {
+ deferred.reject(error);
+ });
+ return deferred;
}
- // module exports
- cornerstoneWADOImageLoader.makeGrayscaleImage = makeGrayscaleImage;
+ 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.version = '0.7.2';
+ cornerstoneWADOImageLoader.fileManager = {
+ add : add,
+ get : get,
+ remove:remove,
+ purge: purge
+ };
}(cornerstoneWADOImageLoader));
(function (cornerstoneWADOImageLoader) {
@@ -3870,10 +3575,10 @@ var JpegImage = (function jpegImage() {
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);
+ //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;
}
@@ -4083,11 +3788,138 @@ var JpegImage = (function jpegImage() {
cornerstone.registerImageLoader('wadors', loadImage);
}($, cornerstone, cornerstoneWADOImageLoader));
+/**
+ * This object supports loading of DICOM P10 dataset from a uri and caching it so it can be accessed
+ * by the caller. This allows a caller to access the datasets without having to go through cornerstone's
+ * image loader mechanism. One reason a caller may need to do this is to determine the number of frames
+ * in a multiframe sop instance so it can create the imageId's correctly.
+ */
+(function (cornerstoneWADOImageLoader) {
+
+ "use strict";
+
+ var loadedDataSets = {};
+ var promises = {};
+
+ // returns true if the wadouri for the specified index has been loaded
+ function isLoaded(uri) {
+ return loadedDataSets[uri] !== undefined;
+ }
+
+ // loads the dicom dataset from the wadouri sp
+ function load(uri) {
+
+ // if already loaded return it right away
+ if(loadedDataSets[uri]) {
+ //console.log('using loaded dataset ' + uri);
+ var alreadyLoadedpromise = $.Deferred();
+ loadedDataSets[uri].cacheCount++;
+ alreadyLoadedpromise.resolve(loadedDataSets[uri].dataSet);
+ return alreadyLoadedpromise;
+ }
+
+ // if we are currently loading this uri, return its promise
+ if(promises[uri]) {
+ //console.log('returning existing load promise for ' + uri);
+ return promises[uri];
+ }
+
+ //console.log('loading ' + uri);
+
+ // This uri is not loaded or being loaded, load it via an xhrRequest
+ var promise = cornerstoneWADOImageLoader.internal.xhrRequest(uri);
+ promises[uri] = promise;
+
+ // handle success and failure of the XHR request load
+ promise.then(function(dataSet) {
+ loadedDataSets[uri] = {
+ dataSet: dataSet,
+ cacheCount: 1
+ };
+ // done loading, remove the promise
+ delete promises[uri];
+ }, function () {
+ }).always(function() {
+ // error thrown, remove the promise
+ delete promises[uri];
+ });
+ return promise;
+ }
+
+ // remove the cached/loaded dicom dataset for the specified wadouri to free up memory
+ function unload(uri) {
+ //console.log('unload for ' + uri);
+ if(loadedDataSets[uri]) {
+ loadedDataSets[uri].cacheCount--;
+ if(loadedDataSets[uri].cacheCount === 0) {
+ //console.log('removing loaded dataset for ' + uri);
+ delete loadedDataSets[uri];
+ }
+ }
+ }
+
+ // removes all cached datasets from memory
+ function purge() {
+ loadedDataSets = {};
+ promises = {};
+ }
+
+ // module exports
+ cornerstoneWADOImageLoader.dataSetCacheManager = {
+ isLoaded: isLoaded,
+ load: load,
+ unload: unload,
+ purge: purge
+ };
+
+}(cornerstoneWADOImageLoader));
(function ($, cornerstone, cornerstoneWADOImageLoader) {
"use strict";
+ function parseImageId(imageId) {
+ // 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);
+ }
+ return {
+ url : url,
+ frame: frame
+ };
+ }
+
+ // add a decache callback function to clear out our dataSetCacheManager
+ function addDecache(image) {
+ image.decache = function() {
+ //console.log('decache');
+ var parsedImageId = parseImageId(image.imageId);
+ cornerstoneWADOImageLoader.dataSetCacheManager.unload(parsedImageId.url);
+ };
+ }
+
+ function loadDataSetFromPromise(xhrRequestPromise, imageId, frame, sharedCacheKey) {
+ var deferred = $.Deferred();
+ xhrRequestPromise.then(function(dataSet) {
+ var imagePromise = cornerstoneWADOImageLoader.createImageObject(dataSet, imageId, frame, sharedCacheKey);
+ imagePromise.then(function(image) {
+ addDecache(image);
+ deferred.resolve(image);
+ }, function(error) {
+ deferred.reject(error);
+ });
+ }, function(error) {
+ deferred.reject(error);
+ });
+ return deferred;
+ }
+
// Loads an image given an imageId
// wado url example:
// http://localhost:3333/wado?requestType=WADO&studyUID=1.3.6.1.4.1.25403.166563008443.5076.20120418075541.1&seriesUID=1.3.6.1.4.1.25403.166563008443.5076.20120418075541.2&objectUID=1.3.6.1.4.1.25403.166563008443.5076.20120418075557.1&contentType=application%2Fdicom&transferSyntax=1.2.840.10008.1.2.1
@@ -4098,32 +3930,33 @@ var JpegImage = (function jpegImage() {
// create a deferred object
// 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);
+ var parsedImageId = parseImageId(imageId);
+
+ // if the dataset for this url is already loaded, use it
+ if(cornerstoneWADOImageLoader.dataSetCacheManager.isLoaded(parsedImageId.url)) {
+ return loadDataSetFromPromise(cornerstoneWADOImageLoader.dataSetCacheManager.load(parsedImageId.url), imageId, parsedImageId.frame, parsedImageId.url);
}
- // if multiframe and cached, use the cached data set to extract the frame
- if(frame !== undefined &&
- cornerstoneWADOImageLoader.internal.multiFrameCacheHack.hasOwnProperty(url))
- {
- var deferred = $.Deferred();
- var dataSet = cornerstoneWADOImageLoader.internal.multiFrameCacheHack[url];
- var imagePromise = cornerstoneWADOImageLoader.createImageObject(dataSet, imageId, frame);
+ // if multiframe, load the dataSet via the dataSetCacheManager to keep it in memory
+ if(parsedImageId.frame !== undefined) {
+ return loadDataSetFromPromise(cornerstoneWADOImageLoader.dataSetCacheManager.load(parsedImageId.url), imageId, parsedImageId.frame, parsedImageId.url);
+ }
+
+ // not multiframe, load it directly and let cornerstone cache manager its lifetime
+ var deferred = $.Deferred();
+ var xhrRequestPromise = cornerstoneWADOImageLoader.internal.xhrRequest(parsedImageId.url, imageId);
+ xhrRequestPromise.then(function(dataSet) {
+ var imagePromise = cornerstoneWADOImageLoader.createImageObject(dataSet, imageId, parsedImageId.frame);
imagePromise.then(function(image) {
+ addDecache(image);
deferred.resolve(image);
}, function(error) {
deferred.reject(error);
});
- return deferred;
- }
-
- return cornerstoneWADOImageLoader.internal.xhrRequest(imageId, frame, url);
+ }, function(error) {
+ deferred.reject(error);
+ });
+ return deferred;
}
// registery dicomweb and wadouri image loader prefixes
@@ -4131,11 +3964,310 @@ var JpegImage = (function jpegImage() {
cornerstone.registerImageLoader('wadouri', loadImage);
}($, cornerstone, 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, sharedCacheKey) {
+
+ // 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 sizeInBytes = dataSet.byteArray.length;
+ 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,
+ sharedCacheKey: sharedCacheKey
+ };
+
+ 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, sharedCacheKey) {
+ 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 sizeInBytes = dataSet.byteArray.length;
+ 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,
+ sharedCacheKey: sharedCacheKey
+ };
+
+ // 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.8.1';
+
+}(cornerstoneWADOImageLoader));
(function ($, cornerstone, cornerstoneWADOImageLoader) {
"use strict";
- function xhrRequest(imageId, frame, url) {
+ function xhrRequest(url, imageId) {
var deferred = $.Deferred();
@@ -4143,7 +4275,7 @@ var JpegImage = (function jpegImage() {
var xhr = new XMLHttpRequest();
xhr.open("get", url, true);
xhr.responseType = "arraybuffer";
- cornerstoneWADOImageLoader.internal.options.beforeSend(xhr);
+ cornerstoneWADOImageLoader.internal.options.beforeSend(xhr);
xhr.onreadystatechange = function (oEvent) {
// TODO: consider sending out progress messages here as we receive the pixel data
if (xhr.readyState === 4) {
@@ -4155,18 +4287,7 @@ var JpegImage = (function jpegImage() {
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) {
- cornerstoneWADOImageLoader.internal.multiFrameCacheHack[url] = dataSet;
- }
-
- var imagePromise = cornerstoneWADOImageLoader.createImageObject(dataSet, imageId, frame);
- imagePromise.then(function (image) {
- deferred.resolve(image);
- }, function (error) {
- deferred.reject(error);
- });
+ deferred.resolve(dataSet);
}
else {
// request failed, reject the deferred
@@ -4199,4 +4320,4 @@ var JpegImage = (function jpegImage() {
}
cornerstoneWADOImageLoader.internal.xhrRequest = xhrRequest;
-}($, cornerstone, cornerstoneWADOImageLoader));
\ No newline at end of file
+}($, cornerstone, cornerstoneWADOImageLoader));
diff --git a/Packages/cornerstone/client/cornerstoneWADORSImageLoader.js b/Packages/cornerstone/client/cornerstoneWADORSImageLoader.js
deleted file mode 100644
index b028682e9..000000000
--- a/Packages/cornerstone/client/cornerstoneWADORSImageLoader.js
+++ /dev/null
@@ -1,77 +0,0 @@
-cornerstoneWADORSImageLoader = {
- internal : {
- nextIndex: 0,
- imageIds : []
- }
-};
-
-cornerstoneWADORSImageLoader.addImage = function(image) {
- var index = cornerstoneWADORSImageLoader.internal.nextIndex++;
- cornerstoneWADORSImageLoader.internal.imageIds[index] = image;
- var imageId = 'wadors:' + index;
- return imageId;
-};
-
-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
- };
-}
-
-cornerstone.registerImageLoader('wadors', function(imageId) {
- var index = imageId.substring(7);
- var image = cornerstoneWADORSImageLoader.internal.imageIds[index];
-
- var deferred = $.Deferred();
-
- var mediaType;// = 'image/dicom+jp2';
-
- DICOMWeb.getImageFrame(image.uri, mediaType).then(function(result) {
- //console.log(result);
- // TODO: add support for retrieving compressed pixel data
- var storedPixelData;
- if(image.instance.bitsAllocated === 16) {
- if(image.instance.pixelRepresentation === 0) {
- storedPixelData = new Uint16Array(result.arrayBuffer, result.offset, result.length / 2);
- } else {
- storedPixelData = new Int16Array(result.arrayBuffer, result.offset, result.length / 2);
- }
- } else if(image.instance.bitsAllocated === 8) {
- storedPixelData = new Uint8Array(result.arrayBuffer, result.offset, result.length);
- }
-
- // TODO: handle various color space conversions
-
- var minMax = getMinMax(storedPixelData);
- image.imageId = imageId;
- image.minPixelValue = minMax.min;
- image.maxPixelValue = minMax.max;
- image.render = cornerstone.renderGrayscaleImage;
- image.getPixelData = function() {
- return storedPixelData;
- };
- //console.log(image);
- deferred.resolve(image);
- }).catch(function(reason) {
- deferred.reject(reason);
- });
-
- return deferred;
-});
-
diff --git a/Packages/cornerstone/client/dicomParser.js b/Packages/cornerstone/client/dicomParser.js
index 4cf0c07c7..1d56f7543 100644
--- a/Packages/cornerstone/client/dicomParser.js
+++ b/Packages/cornerstone/client/dicomParser.js
@@ -1,4 +1,4 @@
-/*! dicom-parser - v1.2.0 - 2015-11-02 | (c) 2014 Chris Hafey | https://github.com/chafey/dicomParser */
+/*! dicom-parser - v1.2.1 - 2016-02-07 | (c) 2014 Chris Hafey | https://github.com/chafey/dicomParser */
(function (root, factory) {
// node.js
@@ -66,6 +66,7 @@ var dicomParser = (function(dicomParser) {
function getDataSetByteStream(transferSyntax, position) {
if(transferSyntax === '1.2.840.10008.1.2.1.99')
{
+ // https://github.com/nodeca/pako
if(typeof(pako) === "undefined") {
throw 'dicomParser.parseDicom: deflated transfer syntax encountered but pako not loaded';
}
@@ -936,8 +937,9 @@ var dicomParser = (function (dicomParser)
{
throw "dicomParser.ByteStream: missing required parameter 'byteArray'";
}
- if((byteArray instanceof Uint8Array) === false) {
- throw 'dicomParser.ByteStream: parameter byteArray is not of type Uint8Array';
+ if((byteArray instanceof Uint8Array) === false &&
+ (byteArray instanceof Buffer) === false ) {
+ throw 'dicomParser.ByteStream: parameter byteArray is not of type Uint8Array or Buffer';
}
if(position < 0)
{
@@ -2253,7 +2255,7 @@ var dicomParser = (function (dicomParser)
dicomParser = {};
}
- dicomParser.version = "1.2.0";
+ dicomParser.version = "1.2.1";
return dicomParser;
}(dicomParser));
diff --git a/Packages/cornerstone/package.js b/Packages/cornerstone/package.js
index 56be57a92..1c7961310 100644
--- a/Packages/cornerstone/package.js
+++ b/Packages/cornerstone/package.js
@@ -1,30 +1,45 @@
Package.describe({
- name: "cornerstone",
- summary: "Cornerstone Web-based Medical Imaging libraries",
- version: '0.0.1'
+ name: 'cornerstone',
+ summary: 'Cornerstone Web-based Medical Imaging libraries',
+ version: '0.0.1'
});
-Package.onUse(function (api) {
+Package.onUse(function(api) {
api.versionsFrom('1.2.0.2');
api.use('jquery');
+ api.use('dicomweb');
- 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.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/dicomParser.js', 'client', {
+ bare: true
+ });
+ api.addFiles('client/hammer.js', 'client', {
+ bare: true
+ });
- api.addFiles('client/measurementManager.js', 'client', {bare: true});
- api.addFiles('client/measurementManagerExample.js', 'client', {bare: true});
+ api.addFiles('client/measurementManager.js', 'client', {
+ bare: true
+ });
+ api.addFiles('client/measurementManagerExample.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', 'server']);
+ api.export('cornerstone', 'client');
+ api.export('cornerstoneMath', 'client');
+ api.export('cornerstoneTools', 'client');
+ api.export('cornerstoneWADOImageLoader', 'client');
+ api.export('dicomParser', ['client', 'server']);
});
diff --git a/Packages/dicomweb/lib/findIndexOfString.js b/Packages/dicomweb/lib/findIndexOfString.js
deleted file mode 100644
index e76bf0adf..000000000
--- a/Packages/dicomweb/lib/findIndexOfString.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Converts a String to a UInt8 array of character codes
- * @param {String} str Input string
- * @returns {Uint8Array} Uint8 Array of character codes
- */
-function stringToUint8Array(str) {
- var uint = new Uint8Array(str.length);
- for (var i = 0,j = str.length;i< j;i++){
- uint[i] = str.charCodeAt(i);
- }
-
- return uint;
-}
-
-function checkToken(token, data, dataOffset) {
- if (dataOffset + token.length > data.length) {
- //console.log('dataOffset >> ', dataOffset);
- return false;
- }
-
- for (var i = 0; i < token.length; i++) {
- if (token[i] !== data[endIndex++]) {
- if (endIndex > 520000) {
- console.log('token=',uint8ArrayToString(token));
- console.log('data=', uint8ArrayToString(data, dataOffset, endIndex - dataOffset));
- console.log('miss at %d %s dataOffset=%d', i, String.fromCharCode(data[endIndex]), endIndex);
- console.log('miss at %d %s dataOffset=%d', i, String.fromCharCode(token[endIndex]), endIndex);
- }
-
- return false;
- }
- }
-
- return true;
-}
-
-findIndexOfString = function(data, str, offset) {
- offset = offset || 0;
-
- var token = stringToUint8Array(str);
-
- for (var i = offset; i < data.length; i++) {
- if (data[i] === token[0]) {
- //console.log('match @', i);
- if (checkToken(token, data, i)) {
- return i;
- }
- }
- }
-
- return -1;
-};
diff --git a/Packages/dicomweb/lib/uint8ArrayToString.js b/Packages/dicomweb/lib/uint8ArrayToString.js
deleted file mode 100644
index cb8e8f4d4..000000000
--- a/Packages/dicomweb/lib/uint8ArrayToString.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Converts a Uint8 array to a string
- * @param data
- * @param offset
- * @param length
- * @returns {string}
- */
-uint8ArrayToString = function(data, offset, length) {
- offset = offset || 0;
- length = length || data.length - offset;
- var str = '';
-
- for (var i = offset; i < offset + length; i++) {
- str += String.fromCharCode(data[i]);
- }
-
- return str;
-};
diff --git a/Packages/dicomweb/package.js b/Packages/dicomweb/package.js
index 427f3aabb..b52ab9943 100644
--- a/Packages/dicomweb/package.js
+++ b/Packages/dicomweb/package.js
@@ -9,16 +9,11 @@ Package.onUse(function(api) {
// DICOMWeb API functions
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');
- // Helper functions
- api.addFiles('lib/findIndexOfString.js', 'server');
- api.addFiles('lib/uint8ArrayToString.js', 'server');
-
- api.export('DICOMWeb', 'server');
+ api.export('DICOMWeb', ['client', 'server']);
});
diff --git a/Packages/dicomweb/server/getImageFrame.js b/Packages/dicomweb/server/getImageFrame.js
deleted file mode 100644
index 0124e6a66..000000000
--- a/Packages/dicomweb/server/getImageFrame.js
+++ /dev/null
@@ -1,74 +0,0 @@
-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();
- });
-};
diff --git a/Packages/dicomweb/server/getJSON.js b/Packages/dicomweb/server/getJSON.js
index c08412d7d..f02c38418 100644
--- a/Packages/dicomweb/server/getJSON.js
+++ b/Packages/dicomweb/server/getJSON.js
@@ -1,8 +1,8 @@
DICOMWeb.getJSON = function(url, options) {
var getOptions = {
headers: {
- Accept: 'application/json'
- },
+ Accept: 'application/json'
+ }
};
if (options.auth) {
diff --git a/Packages/dimse-service/etc/sampleOrthancConfig.json b/Packages/dimse-service/etc/sampleOrthancConfig.json
deleted file mode 100644
index 46c4db930..000000000
--- a/Packages/dimse-service/etc/sampleOrthancConfig.json
+++ /dev/null
@@ -1,6 +0,0 @@
-// The list of the known DICOM modalities
-// If you are running Orthanc with Docker, you need to put the contents of this file in
-// /etc/orthanc/orthanc.json
-"DicomModalities" : {
- "OHIFDCM" : [ "OHIFDCM", "localhost", 3000 ]
-},
\ No newline at end of file
diff --git a/Packages/dimseservice/etc/sampleOrthancConfig.json b/Packages/dimseservice/etc/sampleOrthancConfig.json
new file mode 100644
index 000000000..9c2b84002
--- /dev/null
+++ b/Packages/dimseservice/etc/sampleOrthancConfig.json
@@ -0,0 +1,5 @@
+// The list of the known DICOM modalities
+// If you are running Orthanc , you need to put the contents of this file in its Configuration file
+"DicomModalities" : {
+ "OHIFDCM" : [ "OHIFDCM", "localhost", 3000 ]
+},
\ No newline at end of file
diff --git a/Packages/dimse-service/package.js b/Packages/dimseservice/package.js
similarity index 100%
rename from Packages/dimse-service/package.js
rename to Packages/dimseservice/package.js
diff --git a/Packages/dimse-service/server/Connection.js b/Packages/dimseservice/server/Connection.js
similarity index 100%
rename from Packages/dimse-service/server/Connection.js
rename to Packages/dimseservice/server/Connection.js
diff --git a/Packages/dimse-service/server/DIMSE.js b/Packages/dimseservice/server/DIMSE.js
similarity index 84%
rename from Packages/dimse-service/server/DIMSE.js
rename to Packages/dimseservice/server/DIMSE.js
index 988514b7e..bd266d117 100755
--- a/Packages/dimse-service/server/DIMSE.js
+++ b/Packages/dimseservice/server/DIMSE.js
@@ -8,27 +8,38 @@ DIMSE.associate = function(contexts, callback) {
port = Meteor.settings.dimse.port,
ae = Meteor.settings.dimse.hostAE;
- var client = net.connect({
- host: host,
- port: port
- },
- function() { //'connect' listener
- console.log('==Connected');
+ console.log("Associating via DIMSE");
+ console.log(Meteor.settings.dimse);
- var conn = new Connection(client, {
- vr: {
- split: false
- }
- });
- conn.associate({
- contexts: contexts,
- hostAE: ae
- }, function(pdu) {
- // associated
- console.log('==Associated');
- callback.call(conn, pdu);
- });
+ var client = net.connect({
+ host: host,
+ port: port
+ });
+
+ client.on('connect', function() {
+ //'connect' listener
+ console.log('==Connected');
+
+ var conn = new Connection(client, {
+ vr: {
+ split: false
+ }
});
+
+ conn.associate({
+ contexts: contexts,
+ hostAE: ae
+ }, function(pdu) {
+ // associated
+ console.log('==Associated');
+
+ callback.call(conn, pdu);
+ });
+ });
+
+ client.on('error', function(error) {
+ throw error;
+ });
};
DIMSE.retrievePatients = function(params) {
diff --git a/Packages/dimse-service/server/Data.js b/Packages/dimseservice/server/Data.js
similarity index 100%
rename from Packages/dimse-service/server/Data.js
rename to Packages/dimseservice/server/Data.js
diff --git a/Packages/dimse-service/server/Field.js b/Packages/dimseservice/server/Field.js
similarity index 100%
rename from Packages/dimse-service/server/Field.js
rename to Packages/dimseservice/server/Field.js
diff --git a/Packages/dimse-service/server/Message.js b/Packages/dimseservice/server/Message.js
similarity index 100%
rename from Packages/dimse-service/server/Message.js
rename to Packages/dimseservice/server/Message.js
diff --git a/Packages/dimse-service/server/PDU.js b/Packages/dimseservice/server/PDU.js
similarity index 100%
rename from Packages/dimse-service/server/PDU.js
rename to Packages/dimseservice/server/PDU.js
diff --git a/Packages/dimse-service/server/RWStream.js b/Packages/dimseservice/server/RWStream.js
similarity index 100%
rename from Packages/dimse-service/server/RWStream.js
rename to Packages/dimseservice/server/RWStream.js
diff --git a/Packages/dimse-service/server/constants.js b/Packages/dimseservice/server/constants.js
similarity index 100%
rename from Packages/dimse-service/server/constants.js
rename to Packages/dimseservice/server/constants.js
diff --git a/Packages/dimse-service/server/elements_data.js b/Packages/dimseservice/server/elements_data.js
similarity index 100%
rename from Packages/dimse-service/server/elements_data.js
rename to Packages/dimseservice/server/elements_data.js
diff --git a/Packages/dimse-service/server/methods.js b/Packages/dimseservice/server/methods.js
similarity index 100%
rename from Packages/dimse-service/server/methods.js
rename to Packages/dimseservice/server/methods.js
diff --git a/Packages/dimse-service/server/require.js b/Packages/dimseservice/server/require.js
similarity index 100%
rename from Packages/dimse-service/server/require.js
rename to Packages/dimseservice/server/require.js
diff --git a/Packages/lesiontracker/both/collections.js b/Packages/lesiontracker/both/collections.js
index 65b6f9da0..6dab1a18d 100644
--- a/Packages/lesiontracker/both/collections.js
+++ b/Packages/lesiontracker/both/collections.js
@@ -1,2 +1,5 @@
Timepoints = new Meteor.Collection('timepoints');
+Studies = new Meteor.Collection('studies');
Measurements = new Meteor.Collection('measurements');
+
+WorklistSubscriptions = ['studies', 'timepoints'];
\ No newline at end of file
diff --git a/Packages/lesiontracker/client/collections/LesionLocations.js b/Packages/lesiontracker/client/collections/LesionLocations.js
index 5d9e0d655..893e9d8d8 100644
--- a/Packages/lesiontracker/client/collections/LesionLocations.js
+++ b/Packages/lesiontracker/client/collections/LesionLocations.js
@@ -6,7 +6,8 @@ LesionLocations.insert({
location: 'Liver Left',
hasDescription: false,
description: '',
- selected: false
+ selected: false,
+ isNodal: false
});
LesionLocations.insert({
@@ -15,7 +16,8 @@ LesionLocations.insert({
location: 'Liver Right',
hasDescription: false,
description: '',
- selected: false
+ selected: false,
+ isNodal: false
});
LesionLocations.insert({
@@ -24,7 +26,8 @@ LesionLocations.insert({
location: 'Liver Caudate',
hasDescription: false,
description: '',
- selected: false
+ selected: false,
+ isNodal: false
});
LesionLocations.insert({
@@ -33,7 +36,8 @@ LesionLocations.insert({
location: 'Lung LLL',
hasDescription: false,
description: '',
- selected: false
+ selected: false,
+ isNodal: false
});
LesionLocations.insert({
@@ -42,7 +46,8 @@ LesionLocations.insert({
location: 'Lung LUL',
hasDescription: false,
description: '',
- selected: false
+ selected: false,
+ isNodal: false
});
LesionLocations.insert({
@@ -51,7 +56,8 @@ LesionLocations.insert({
location: 'Lung RLL',
hasDescription: false,
description: '',
- selected: false
+ selected: false,
+ isNodal: false
});
LesionLocations.insert({
@@ -60,7 +66,8 @@ LesionLocations.insert({
location: 'Lung RML',
hasDescription: false,
description: '',
- selected: false
+ selected: false,
+ isNodal: false
});
LesionLocations.insert({
@@ -69,7 +76,8 @@ LesionLocations.insert({
location: 'Lung RUL',
hasDescription: false,
description: '',
- selected: false
+ selected: false,
+ isNodal: false
});
LesionLocations.insert({
@@ -78,7 +86,8 @@ LesionLocations.insert({
location: 'Pleura Left',
hasDescription: false,
description: '',
- selected: false
+ selected: false,
+ isNodal: false
});
LesionLocations.insert({
@@ -87,7 +96,8 @@ LesionLocations.insert({
location: 'Pleura Right',
hasDescription: false,
description: '',
- selected: false
+ selected: false,
+ isNodal: false
});
LesionLocations.insert({
@@ -96,7 +106,8 @@ LesionLocations.insert({
location: 'Kidney Left',
hasDescription: false,
description: '',
- selected: false
+ selected: false,
+ isNodal: false
});
LesionLocations.insert({
@@ -104,5 +115,7 @@ LesionLocations.insert({
group: 'kidney',
location: 'Kidney Right',
hasDescription: false,
- description: ''
+ description: '',
+ selected: false,
+ isNodal: false
});
diff --git a/Packages/lesiontracker/client/collections/PatientLocations.js b/Packages/lesiontracker/client/collections/PatientLocations.js
new file mode 100644
index 000000000..49283024c
--- /dev/null
+++ b/Packages/lesiontracker/client/collections/PatientLocations.js
@@ -0,0 +1 @@
+PatientLocations = new Meteor.Collection(null);
\ No newline at end of file
diff --git a/Packages/lesiontracker/client/compatibility/LesionManager.js b/Packages/lesiontracker/client/compatibility/LesionManager.js
deleted file mode 100644
index 28c4df051..000000000
--- a/Packages/lesiontracker/client/compatibility/LesionManager.js
+++ /dev/null
@@ -1,190 +0,0 @@
-var LesionManager = (function() {
- PatientLocations = new Meteor.Collection(null);
-
- /**
- * Retrieve a location name (e.g. Liver Right) from the
- * PatientLocations Collection by id, if it exists. Otherwise,
- * return an empty string.
- *
- * @param id
- * @returns {*|string}
- */
- function getLocationName(id) {
- var locationObject = PatientLocations.findOne(id);
- if (!locationObject || !locationObject.location) {
- return '';
- }
-
- return locationObject.location;
- }
-
- /**
- * Update the Timepoint object for a specific Measurement.
- * If no measurement exists yet, one will be created.
- *
- * Input is toolData from the lesion or nonTarget tool
- *
- * @param lesionData
- */
- function updateLesionData(lesionData) {
- // Find the related Timepoint from the Timepoints Collection
- var timepointID = lesionData.timepointID;
- var timepoint = Timepoints.findOne({
- timepointID: timepointID
- });
- if (!timepoint) {
- log.warn('Timepoint in an image is not present in the Timepoints Collection?');
- return;
- }
-
- // Find the specific lesion to be updated
- var existingMeasurement;
- if (lesionData.id && lesionData.id !== 'notready') {
- existingMeasurement = Measurements.findOne(lesionData.id);
- } else {
- existingMeasurement = Measurements.findOne({
- lesionNumber: lesionData.lesionNumber,
- isTarget: lesionData.isTarget
- });
- }
-
- // Create a structure for the timepointData based
- // on this Lesion's toolData
- var timepointData = {
- seriesInstanceUid: lesionData.seriesInstanceUid,
- studyInstanceUid: lesionData.studyInstanceUid,
- handles: lesionData.handles,
- imageId: lesionData.imageId
- };
-
- if (lesionData.isTarget === true) {
- timepointData.shortestDiameter = lesionData.widthMeasurement;
- timepointData.longestDiameter = lesionData.measurementText;
- } else {
- timepointData.response = lesionData.response;
- }
-
- // If no such lesion exists, we need to add one
- if (!existingMeasurement) {
- // Create a data structure for the Measurement
- // based on the current tool data
- var measurement = {
- lesionNumber: lesionData.lesionNumber,
- isTarget: lesionData.isTarget,
- patientId: lesionData.patientId,
- id: lesionData.id
- };
-
- // Retrieve the location name given the locationUID
- if (lesionData.locationUID !== undefined) {
- measurement.location = getLocationName(lesionData.locationUID);
- }
-
- // Add toolData parameters to the Measurement at this Timepoint
- measurement.timepoints = {};
- measurement.timepoints[timepointID] = timepointData;
-
- // Set a flag to prevent duplication of toolData
- measurement.toolDataInsertedManually = true;
-
- // Increment and store the absolute Lesion Number for this Measurement
- measurement.lesionNumberAbsolute = Measurements.find().count() + 1;
-
- // Insert this into the Measurements Collection
- // Save the ID into the toolData (not sure if this works?)
- measurement.id = Measurements.insert(measurement);
-
- // Update the database entry so it can be readded next time the study is loaded
- Measurements.update(measurement.id, {
- $set: {
- toolDataInsertedManually: false
- }
- });
- } else {
- lesionData.id = existingMeasurement._id;
-
- // Update timepoints from lesion data
- existingMeasurement.timepoints[timepointID] = timepointData;
-
- Measurements.update(existingMeasurement._id, {
- $set: {
- timepoints: existingMeasurement.timepoints
- }
- });
- }
- }
-
- /**
- * Returns new lesion number according to timepointID
- * @param timepointID
- * @param isTarget
- * @returns {*}
- */
- function getNewLesionNumber(timepointID, isTarget) {
- // Get all current lesion measurements
- var numMeasurements = Measurements.find({
- isTarget: isTarget
- }).count();
-
- // If no measurements exist yet, start at 1
- if (!numMeasurements) {
- return 1;
- }
-
- // Find related measurements (i.e. target or non-target)
- var measurements = Measurements.find({
- isTarget: isTarget
- }, {
- sort: {
- lesionNumber: 1
- }
- }).fetch();
-
- // If measurements exist, find the last lesion number
- // from the given timepoint
- var lesionNumberCounter = 1;
-
- // Search through every Measurement to see which ones
- // already have data for this Timepoint, if we find one that
- // doesn't have data, we will stop there and use that as the
- // current Measurement
- measurements.every(function(measurement) {
- // If this measurement has no data for this Timepoint,
- // use this as the current Measurement
- if (!measurement.timepoints[timepointID]) {
- lesionNumberCounter = measurement.lesionNumber;
- return false;
- }
-
- lesionNumberCounter++;
- return true;
- });
- return lesionNumberCounter;
- }
-
- /**
- * If the current Lesion Number already exists
- * for any other timepoint, returns lesion locationUID
- * @param lesionData
- * @returns {*}
- */
- function lesionNumberExists(lesionData) {
- var measurement = Measurements.findOne({
- lesionNumber: lesionData.lesionNumber,
- isTarget: lesionData.isTarget
- });
-
- if (!measurement) {
- return;
- }
-
- return measurement.locationUID;
- }
-
- return {
- updateLesionData: updateLesionData,
- getNewLesionNumber: getNewLesionNumber,
- lesionNumberExists: lesionNumberExists,
- getLocationName: getLocationName
- };
-})();
diff --git a/Packages/lesiontracker/client/compatibility/lesionTool.js b/Packages/lesiontracker/client/compatibility/lesionTool.js
index a1a9a364b..d2846cf4f 100644
--- a/Packages/lesiontracker/client/compatibility/lesionTool.js
+++ b/Packages/lesiontracker/client/compatibility/lesionTool.js
@@ -50,6 +50,11 @@
};
var config = cornerstoneTools.lesion.getConfiguration();
+ // Set lesion number and lesion name
+ if (measurementData.lesionNumber === undefined) {
+ config.setLesionNumberCallback(measurementData, mouseEventData, doneCallback);
+ }
+
// associate this data with this imageId so we can render it and manipulate it
cornerstoneTools.addToolState(element, toolType, measurementData);
@@ -76,11 +81,6 @@
// Bind a one-time event listener for the Esc key
$(element).one('keydown', cancelCallback);
- // Set lesion number and lesion name
- if (measurementData.lesionNumber === undefined) {
- config.setLesionNumberCallback(measurementData, mouseEventData, doneCallback);
- }
-
cornerstone.updateImage(element);
cornerstoneTools.moveNewHandle(mouseEventData, toolType, measurementData, measurementData.handles.end, function() {
@@ -217,8 +217,7 @@
widthMeasurement: 0,
perpendicularMeasurement: 0,
isDeleted: false,
- isTarget: true,
- uid: uuid.v4()
+ isTarget: true
};
return measurementData;
}
diff --git a/Packages/lesiontracker/client/compatibility/nonTargetTool.js b/Packages/lesiontracker/client/compatibility/nonTargetTool.js
index d42e8b33c..939c340c6 100644
--- a/Packages/lesiontracker/client/compatibility/nonTargetTool.js
+++ b/Packages/lesiontracker/client/compatibility/nonTargetTool.js
@@ -51,6 +51,13 @@
mouseButtonMask: mouseEventData.which
};
+ var config = cornerstoneTools.nonTarget.getConfiguration();
+
+ // Set lesion number and lesion name
+ if (measurementData.lesionNumber === undefined) {
+ config.setLesionNumberCallback(measurementData, mouseEventData, doneCallback);
+ }
+
// associate this data with this imageId so we can render it and manipulate it
cornerstoneTools.addToolState(mouseEventData.element, toolType, measurementData);
@@ -77,13 +84,6 @@
// Bind a one-time event listener for the Esc key
$(element).one('keydown', cancelCallback);
- var config = cornerstoneTools.nonTarget.getConfiguration();
-
- // Set lesion number and lesion name
- if (measurementData.lesionNumber === undefined) {
- config.setLesionNumberCallback(measurementData, mouseEventData, doneCallback);
- }
-
cornerstone.updateImage(element);
cornerstoneTools.moveNewHandle(mouseEventData, toolType, measurementData, measurementData.handles.end, function() {
@@ -101,6 +101,8 @@
$(element).on('CornerstoneToolsMouseDown', eventData, cornerstoneTools.nonTarget.mouseDownCallback);
$(element).on('CornerstoneToolsMouseDownActivate', eventData, cornerstoneTools.nonTarget.mouseDownActivateCallback);
+ $(element).off('keydown', cancelCallback);
+
cornerstone.updateImage(mouseEventData.element);
});
}
diff --git a/Packages/lesiontracker/client/compatibility/scaleOverlayTool.js b/Packages/lesiontracker/client/compatibility/scaleOverlayTool.js
index fbedca2aa..16990bdc6 100644
--- a/Packages/lesiontracker/client/compatibility/scaleOverlayTool.js
+++ b/Packages/lesiontracker/client/compatibility/scaleOverlayTool.js
@@ -50,11 +50,10 @@
x: config.horizontalLine.start.x + i * config.horizontalMinorTick,
y: 0
};
- if (i% 5 === 0) {
+ if (i% 5 === 0) {
endPoint.y = config.horizontalLine.start.y - config.majorTickLength;
} else {
-
endPoint.y = config.horizontalLine.start.y - config.minorTickLength;
}
diff --git a/Packages/lesiontracker/client/compatibility/validate.js b/Packages/lesiontracker/client/compatibility/validate.js
new file mode 100644
index 000000000..ec942ac7a
--- /dev/null
+++ b/Packages/lesiontracker/client/compatibility/validate.js
@@ -0,0 +1,1086 @@
+/*!
+ * validate.js 0.9.0
+ *
+ * (c) 2013-2015 Nicklas Ansman, 2013 Wrapp
+ * Validate.js may be freely distributed under the MIT license.
+ * For all details and documentation:
+ * http://validatejs.org/
+ */
+
+(function(exports, module, define) {
+ "use strict";
+
+ // The main function that calls the validators specified by the constraints.
+ // The options are the following:
+ // - format (string) - An option that controls how the returned value is formatted
+ // * flat - Returns a flat array of just the error messages
+ // * grouped - Returns the messages grouped by attribute (default)
+ // * detailed - Returns an array of the raw validation data
+ // - fullMessages (boolean) - If `true` (default) the attribute name is prepended to the error.
+ //
+ // Please note that the options are also passed to each validator.
+ var validate = function(attributes, constraints, options) {
+ options = v.extend({}, v.options, options);
+
+ var results = v.runValidations(attributes, constraints, options)
+ , attr
+ , validator;
+
+ for (attr in results) {
+ for (validator in results[attr]) {
+ if (v.isPromise(results[attr][validator])) {
+ throw new Error("Use validate.async if you want support for promises");
+ }
+ }
+ }
+ return validate.processValidationResults(results, options);
+ };
+
+ var v = validate;
+
+ // Copies over attributes from one or more sources to a single destination.
+ // Very much similar to underscore's extend.
+ // The first argument is the target object and the remaining arguments will be
+ // used as sources.
+ v.extend = function(obj) {
+ [].slice.call(arguments, 1).forEach(function(source) {
+ for (var attr in source) {
+ obj[attr] = source[attr];
+ }
+ });
+ return obj;
+ };
+
+ v.extend(validate, {
+ // This is the version of the library as a semver.
+ // The toString function will allow it to be coerced into a string
+ version: {
+ major: 0,
+ minor: 9,
+ patch: 0,
+ metadata: "development",
+ toString: function() {
+ var version = v.format("%{major}.%{minor}.%{patch}", v.version);
+ if (!v.isEmpty(v.version.metadata)) {
+ version += "+" + v.version.metadata;
+ }
+ return version;
+ }
+ },
+
+ // Below is the dependencies that are used in validate.js
+
+ // The constructor of the Promise implementation.
+ // If you are using Q.js, RSVP or any other A+ compatible implementation
+ // override this attribute to be the constructor of that promise.
+ // Since jQuery promises aren't A+ compatible they won't work.
+ Promise: typeof Promise !== "undefined" ? Promise : /* istanbul ignore next */ null,
+
+ EMPTY_STRING_REGEXP: /^\s*$/,
+
+ // Runs the validators specified by the constraints object.
+ // Will return an array of the format:
+ // [{attribute: "", error: ""}, ...]
+ runValidations: function(attributes, constraints, options) {
+ var results = []
+ , attr
+ , validatorName
+ , value
+ , validators
+ , validator
+ , validatorOptions
+ , error;
+
+ if (v.isDomElement(attributes) || v.isJqueryElement(attributes)) {
+ attributes = v.collectFormValues(attributes);
+ }
+
+ // Loops through each constraints, finds the correct validator and run it.
+ for (attr in constraints) {
+ value = v.getDeepObjectValue(attributes, attr);
+ // This allows the constraints for an attribute to be a function.
+ // The function will be called with the value, attribute name, the complete dict of
+ // attributes as well as the options and constraints passed in.
+ // This is useful when you want to have different
+ // validations depending on the attribute value.
+ validators = v.result(constraints[attr], value, attributes, attr, options, constraints);
+
+ for (validatorName in validators) {
+ validator = v.validators[validatorName];
+
+ if (!validator) {
+ error = v.format("Unknown validator %{name}", {name: validatorName});
+ throw new Error(error);
+ }
+
+ validatorOptions = validators[validatorName];
+ // This allows the options to be a function. The function will be
+ // called with the value, attribute name, the complete dict of
+ // attributes as well as the options and constraints passed in.
+ // This is useful when you want to have different
+ // validations depending on the attribute value.
+ validatorOptions = v.result(validatorOptions, value, attributes, attr, options, constraints);
+ if (!validatorOptions) {
+ continue;
+ }
+ results.push({
+ attribute: attr,
+ value: value,
+ validator: validatorName,
+ globalOptions: options,
+ attributes: attributes,
+ options: validatorOptions,
+ error: validator.call(validator,
+ value,
+ validatorOptions,
+ attr,
+ attributes,
+ options)
+ });
+ }
+ }
+
+ return results;
+ },
+
+ // Takes the output from runValidations and converts it to the correct
+ // output format.
+ processValidationResults: function(errors, options) {
+ var attr;
+
+ errors = v.pruneEmptyErrors(errors, options);
+ errors = v.expandMultipleErrors(errors, options);
+ errors = v.convertErrorMessages(errors, options);
+
+ switch (options.format || "grouped") {
+ case "detailed":
+ // Do nothing more to the errors
+ break;
+
+ case "flat":
+ errors = v.flattenErrorsToArray(errors);
+ break;
+
+ case "grouped":
+ errors = v.groupErrorsByAttribute(errors);
+ for (attr in errors) {
+ errors[attr] = v.flattenErrorsToArray(errors[attr]);
+ }
+ break;
+
+ default:
+ throw new Error(v.format("Unknown format %{format}", options));
+ }
+
+ return v.isEmpty(errors) ? undefined : errors;
+ },
+
+ // Runs the validations with support for promises.
+ // This function will return a promise that is settled when all the
+ // validation promises have been completed.
+ // It can be called even if no validations returned a promise.
+ async: function(attributes, constraints, options) {
+ options = v.extend({}, v.async.options, options);
+
+ var WrapErrors = options.wrapErrors || function(errors) {
+ return errors;
+ };
+
+ // Removes unknown attributes
+ if (options.cleanAttributes !== false) {
+ attributes = v.cleanAttributes(attributes, constraints);
+ }
+
+ var results = v.runValidations(attributes, constraints, options);
+
+ return new v.Promise(function(resolve, reject) {
+ v.waitForResults(results).then(function() {
+ var errors = v.processValidationResults(results, options);
+ if (errors) {
+ reject(new WrapErrors(errors, options, attributes, constraints));
+ } else {
+ resolve(attributes);
+ }
+ }, function(err) {
+ reject(err);
+ });
+ });
+ },
+
+ single: function(value, constraints, options) {
+ options = v.extend({}, v.single.options, options, {
+ format: "flat",
+ fullMessages: false
+ });
+ return v({single: value}, {single: constraints}, options);
+ },
+
+ // Returns a promise that is resolved when all promises in the results array
+ // are settled. The promise returned from this function is always resolved,
+ // never rejected.
+ // This function modifies the input argument, it replaces the promises
+ // with the value returned from the promise.
+ waitForResults: function(results) {
+ // Create a sequence of all the results starting with a resolved promise.
+ return results.reduce(function(memo, result) {
+ // If this result isn't a promise skip it in the sequence.
+ if (!v.isPromise(result.error)) {
+ return memo;
+ }
+
+ return memo.then(function() {
+ return result.error.then(
+ function(error) {
+ result.error = error || null;
+ },
+ function(error) {
+ if (error instanceof Error) {
+ throw error;
+ }
+ v.error("Rejecting promises with the result is deprecated. Please use the resolve callback instead.");
+ result.error = error;
+ }
+ );
+ });
+ }, new v.Promise(function(r) { r(); })); // A resolved promise
+ },
+
+ // If the given argument is a call: function the and: function return the value
+ // otherwise just return the value. Additional arguments will be passed as
+ // arguments to the function.
+ // Example:
+ // ```
+ // result('foo') // 'foo'
+ // result(Math.max, 1, 2) // 2
+ // ```
+ result: function(value) {
+ var args = [].slice.call(arguments, 1);
+ if (typeof value === 'function') {
+ value = value.apply(null, args);
+ }
+ return value;
+ },
+
+ // Checks if the value is a number. This function does not consider NaN a
+ // number like many other `isNumber` functions do.
+ isNumber: function(value) {
+ return typeof value === 'number' && !isNaN(value);
+ },
+
+ // Returns false if the object is not a function
+ isFunction: function(value) {
+ return typeof value === 'function';
+ },
+
+ // A simple check to verify that the value is an integer. Uses `isNumber`
+ // and a simple modulo check.
+ isInteger: function(value) {
+ return v.isNumber(value) && value % 1 === 0;
+ },
+
+ // Uses the `Object` function to check if the given argument is an object.
+ isObject: function(obj) {
+ return obj === Object(obj);
+ },
+
+ // Simply checks if the object is an instance of a date
+ isDate: function(obj) {
+ return obj instanceof Date;
+ },
+
+ // Returns false if the object is `null` of `undefined`
+ isDefined: function(obj) {
+ return obj !== null && obj !== undefined;
+ },
+
+ // Checks if the given argument is a promise. Anything with a `then`
+ // function is considered a promise.
+ isPromise: function(p) {
+ return !!p && v.isFunction(p.then);
+ },
+
+ isJqueryElement: function(o) {
+ return o && v.isString(o.jquery);
+ },
+
+ isDomElement: function(o) {
+ if (!o) {
+ return false;
+ }
+
+ if (!v.isFunction(o.querySelectorAll) || !v.isFunction(o.querySelector)) {
+ return false;
+ }
+
+ if (v.isObject(document) && o === document) {
+ return true;
+ }
+
+ // http://stackoverflow.com/a/384380/699304
+ /* istanbul ignore else */
+ if (typeof HTMLElement === "object") {
+ return o instanceof HTMLElement;
+ } else {
+ return o &&
+ typeof o === "object" &&
+ o !== null &&
+ o.nodeType === 1 &&
+ typeof o.nodeName === "string";
+ }
+ },
+
+ isEmpty: function(value) {
+ var attr;
+
+ // Null and undefined are empty
+ if (!v.isDefined(value)) {
+ return true;
+ }
+
+ // functions are non empty
+ if (v.isFunction(value)) {
+ return false;
+ }
+
+ // Whitespace only strings are empty
+ if (v.isString(value)) {
+ return v.EMPTY_STRING_REGEXP.test(value);
+ }
+
+ // For arrays we use the length property
+ if (v.isArray(value)) {
+ return value.length === 0;
+ }
+
+ // Dates have no attributes but aren't empty
+ if (v.isDate(value)) {
+ return false;
+ }
+
+ // If we find at least one property we consider it non empty
+ if (v.isObject(value)) {
+ for (attr in value) {
+ return false;
+ }
+ return true;
+ }
+
+ return false;
+ },
+
+ // Formats the specified strings with the given values like so:
+ // ```
+ // format("Foo: %{foo}", {foo: "bar"}) // "Foo bar"
+ // ```
+ // If you want to write %{...} without having it replaced simply
+ // prefix it with % like this `Foo: %%{foo}` and it will be returned
+ // as `"Foo: %{foo}"`
+ format: v.extend(function(str, vals) {
+ if (!v.isString(str)) {
+ return str;
+ }
+ return str.replace(v.format.FORMAT_REGEXP, function(m0, m1, m2) {
+ if (m1 === '%') {
+ return "%{" + m2 + "}";
+ } else {
+ return String(vals[m2]);
+ }
+ });
+ }, {
+ // Finds %{key} style patterns in the given string
+ FORMAT_REGEXP: /(%?)%\{([^\}]+)\}/g
+ }),
+
+ // "Prettifies" the given string.
+ // Prettifying means replacing [.\_-] with spaces as well as splitting
+ // camel case words.
+ prettify: function(str) {
+ if (v.isNumber(str)) {
+ // If there are more than 2 decimals round it to two
+ if ((str * 100) % 1 === 0) {
+ return "" + str;
+ } else {
+ return parseFloat(Math.round(str * 100) / 100).toFixed(2);
+ }
+ }
+
+ if (v.isArray(str)) {
+ return str.map(function(s) { return v.prettify(s); }).join(", ");
+ }
+
+ if (v.isObject(str)) {
+ return str.toString();
+ }
+
+ // Ensure the string is actually a string
+ str = "" + str;
+
+ return str
+ // Splits keys separated by periods
+ .replace(/([^\s])\.([^\s])/g, '$1 $2')
+ // Removes backslashes
+ .replace(/\\+/g, '')
+ // Replaces - and - with space
+ .replace(/[_-]/g, ' ')
+ // Splits camel cased words
+ .replace(/([a-z])([A-Z])/g, function(m0, m1, m2) {
+ return "" + m1 + " " + m2.toLowerCase();
+ })
+ .toLowerCase();
+ },
+
+ stringifyValue: function(value) {
+ return v.prettify(value);
+ },
+
+ isString: function(value) {
+ return typeof value === 'string';
+ },
+
+ isArray: function(value) {
+ return {}.toString.call(value) === '[object Array]';
+ },
+
+ contains: function(obj, value) {
+ if (!v.isDefined(obj)) {
+ return false;
+ }
+ if (v.isArray(obj)) {
+ return obj.indexOf(value) !== -1;
+ }
+ return value in obj;
+ },
+
+ forEachKeyInKeypath: function(object, keypath, callback) {
+ if (!v.isString(keypath)) {
+ return undefined;
+ }
+
+ var key = ""
+ , i
+ , escape = false;
+
+ for (i = 0; i < keypath.length; ++i) {
+ switch (keypath[i]) {
+ case '.':
+ if (escape) {
+ escape = false;
+ key += '.';
+ } else {
+ object = callback(object, key, false);
+ key = "";
+ }
+ break;
+
+ case '\\':
+ if (escape) {
+ escape = false;
+ key += '\\';
+ } else {
+ escape = true;
+ }
+ break;
+
+ default:
+ escape = false;
+ key += keypath[i];
+ break;
+ }
+ }
+
+ return callback(object, key, true);
+ },
+
+ getDeepObjectValue: function(obj, keypath) {
+ if (!v.isObject(obj)) {
+ return undefined;
+ }
+
+ return v.forEachKeyInKeypath(obj, keypath, function(obj, key) {
+ if (v.isObject(obj)) {
+ return obj[key];
+ }
+ });
+ },
+
+ // This returns an object with all the values of the form.
+ // It uses the input name as key and the value as value
+ // So for example this:
+ //
+ // would return:
+ // {email: "foo@bar.com"}
+ collectFormValues: function(form, options) {
+ var values = {}
+ , i
+ , input
+ , inputs
+ , value;
+
+ if (v.isJqueryElement(form)) {
+ form = form[0];
+ }
+
+ if (!form) {
+ return values;
+ }
+
+ options = options || {};
+
+ inputs = form.querySelectorAll("input[name], textarea[name]");
+ for (i = 0; i < inputs.length; ++i) {
+ input = inputs.item(i);
+
+ if (v.isDefined(input.getAttribute("data-ignored"))) {
+ continue;
+ }
+
+ value = v.sanitizeFormValue(input.value, options);
+ if (input.type === "number") {
+ value = value ? +value : null;
+ } else if (input.type === "checkbox") {
+ if (input.attributes.value) {
+ if (!input.checked) {
+ value = values[input.name] || null;
+ }
+ } else {
+ value = input.checked;
+ }
+ } else if (input.type === "radio") {
+ if (!input.checked) {
+ value = values[input.name] || null;
+ }
+ }
+ values[input.name] = value;
+ }
+
+ inputs = form.querySelectorAll("select[name]");
+ for (i = 0; i < inputs.length; ++i) {
+ input = inputs.item(i);
+ value = v.sanitizeFormValue(input.options[input.selectedIndex].value, options);
+ values[input.name] = value;
+ }
+
+ return values;
+ },
+
+ sanitizeFormValue: function(value, options) {
+ if (options.trim && v.isString(value)) {
+ value = value.trim();
+ }
+
+ if (options.nullify !== false && value === "") {
+ return null;
+ }
+ return value;
+ },
+
+ capitalize: function(str) {
+ if (!v.isString(str)) {
+ return str;
+ }
+ return str[0].toUpperCase() + str.slice(1);
+ },
+
+ // Remove all errors who's error attribute is empty (null or undefined)
+ pruneEmptyErrors: function(errors) {
+ return errors.filter(function(error) {
+ return !v.isEmpty(error.error);
+ });
+ },
+
+ // In
+ // [{error: ["err1", "err2"], ...}]
+ // Out
+ // [{error: "err1", ...}, {error: "err2", ...}]
+ //
+ // All attributes in an error with multiple messages are duplicated
+ // when expanding the errors.
+ expandMultipleErrors: function(errors) {
+ var ret = [];
+ errors.forEach(function(error) {
+ // Removes errors without a message
+ if (v.isArray(error.error)) {
+ error.error.forEach(function(msg) {
+ ret.push(v.extend({}, error, {error: msg}));
+ });
+ } else {
+ ret.push(error);
+ }
+ });
+ return ret;
+ },
+
+ // Converts the error mesages by prepending the attribute name unless the
+ // message is prefixed by ^
+ convertErrorMessages: function(errors, options) {
+ options = options || {};
+
+ var ret = [];
+ errors.forEach(function(errorInfo) {
+ var error = v.result(errorInfo.error,
+ errorInfo.value,
+ errorInfo.attribute,
+ errorInfo.options,
+ errorInfo.attributes,
+ errorInfo.globalOptions);
+
+ if (!v.isString(error)) {
+ ret.push(errorInfo);
+ return;
+ }
+
+ if (error[0] === '^') {
+ error = error.slice(1);
+ } else if (options.fullMessages !== false) {
+ error = v.capitalize(v.prettify(errorInfo.attribute)) + " " + error;
+ }
+ error = error.replace(/\\\^/g, "^");
+ error = v.format(error, {value: v.stringifyValue(errorInfo.value)});
+ ret.push(v.extend({}, errorInfo, {error: error}));
+ });
+ return ret;
+ },
+
+ // In:
+ // [{attribute: "", ...}]
+ // Out:
+ // {"": [{attribute: "", ...}]}
+ groupErrorsByAttribute: function(errors) {
+ var ret = {};
+ errors.forEach(function(error) {
+ var list = ret[error.attribute];
+ if (list) {
+ list.push(error);
+ } else {
+ ret[error.attribute] = [error];
+ }
+ });
+ return ret;
+ },
+
+ // In:
+ // [{error: "", ...}, {error: "", ...}]
+ // Out:
+ // ["", ""]
+ flattenErrorsToArray: function(errors) {
+ return errors.map(function(error) { return error.error; });
+ },
+
+ cleanAttributes: function(attributes, whitelist) {
+ function whitelistCreator(obj, key, last) {
+ if (v.isObject(obj[key])) {
+ return obj[key];
+ }
+ return (obj[key] = last ? true : {});
+ }
+
+ function buildObjectWhitelist(whitelist) {
+ var ow = {}
+ , lastObject
+ , attr;
+ for (attr in whitelist) {
+ if (!whitelist[attr]) {
+ continue;
+ }
+ v.forEachKeyInKeypath(ow, attr, whitelistCreator);
+ }
+ return ow;
+ }
+
+ function cleanRecursive(attributes, whitelist) {
+ if (!v.isObject(attributes)) {
+ return attributes;
+ }
+
+ var ret = v.extend({}, attributes)
+ , w
+ , attribute;
+
+ for (attribute in attributes) {
+ w = whitelist[attribute];
+
+ if (v.isObject(w)) {
+ ret[attribute] = cleanRecursive(ret[attribute], w);
+ } else if (!w) {
+ delete ret[attribute];
+ }
+ }
+ return ret;
+ }
+
+ if (!v.isObject(whitelist) || !v.isObject(attributes)) {
+ return {};
+ }
+
+ whitelist = buildObjectWhitelist(whitelist);
+ return cleanRecursive(attributes, whitelist);
+ },
+
+ exposeModule: function(validate, root, exports, module, define) {
+ if (exports) {
+ if (module && module.exports) {
+ exports = module.exports = validate;
+ }
+ exports.validate = validate;
+ } else {
+ root.validate = validate;
+ if (validate.isFunction(define) && define.amd) {
+ define([], function () { return validate; });
+ }
+ }
+ },
+
+ warn: function(msg) {
+ if (typeof console !== "undefined" && console.warn) {
+ console.warn("[validate.js] " + msg);
+ }
+ },
+
+ error: function(msg) {
+ if (typeof console !== "undefined" && console.error) {
+ console.error("[validate.js] " + msg);
+ }
+ }
+ });
+
+ validate.validators = {
+ // Presence validates that the value isn't empty
+ presence: function(value, options) {
+ options = v.extend({}, this.options, options);
+ if (v.isEmpty(value)) {
+ return options.message || this.message || "can't be blank";
+ }
+ },
+ length: function(value, options, attribute) {
+ // Empty values are allowed
+ if (v.isEmpty(value)) {
+ return;
+ }
+
+ options = v.extend({}, this.options, options);
+
+ var is = options.is
+ , maximum = options.maximum
+ , minimum = options.minimum
+ , tokenizer = options.tokenizer || function(val) { return val; }
+ , err
+ , errors = [];
+
+ value = tokenizer(value);
+ var length = value.length;
+ if(!v.isNumber(length)) {
+ v.error(v.format("Attribute %{attr} has a non numeric value for `length`", {attr: attribute}));
+ return options.message || this.notValid || "has an incorrect length";
+ }
+
+ // Is checks
+ if (v.isNumber(is) && length !== is) {
+ err = options.wrongLength ||
+ this.wrongLength ||
+ "is the wrong length (should be %{count} characters)";
+ errors.push(v.format(err, {count: is}));
+ }
+
+ if (v.isNumber(minimum) && length < minimum) {
+ err = options.tooShort ||
+ this.tooShort ||
+ "is too short (minimum is %{count} characters)";
+ errors.push(v.format(err, {count: minimum}));
+ }
+
+ if (v.isNumber(maximum) && length > maximum) {
+ err = options.tooLong ||
+ this.tooLong ||
+ "is too long (maximum is %{count} characters)";
+ errors.push(v.format(err, {count: maximum}));
+ }
+
+ if (errors.length > 0) {
+ return options.message || errors;
+ }
+ },
+ numericality: function(value, options) {
+ // Empty values are fine
+ if (v.isEmpty(value)) {
+ return;
+ }
+
+ options = v.extend({}, this.options, options);
+
+ var errors = []
+ , name
+ , count
+ , checks = {
+ greaterThan: function(v, c) { return v > c; },
+ greaterThanOrEqualTo: function(v, c) { return v >= c; },
+ equalTo: function(v, c) { return v === c; },
+ lessThan: function(v, c) { return v < c; },
+ lessThanOrEqualTo: function(v, c) { return v <= c; }
+ };
+
+ // Coerce the value to a number unless we're being strict.
+ if (options.noStrings !== true && v.isString(value)) {
+ value = +value;
+ }
+
+ // If it's not a number we shouldn't continue since it will compare it.
+ if (!v.isNumber(value)) {
+ return options.message || options.notValid || this.notValid || "is not a number";
+ }
+
+ // Same logic as above, sort of. Don't bother with comparisons if this
+ // doesn't pass.
+ if (options.onlyInteger && !v.isInteger(value)) {
+ return options.message || options.notInteger || this.notInteger || "must be an integer";
+ }
+
+ for (name in checks) {
+ count = options[name];
+ if (v.isNumber(count) && !checks[name](value, count)) {
+ // This picks the default message if specified
+ // For example the greaterThan check uses the message from
+ // this.notGreaterThan so we capitalize the name and prepend "not"
+ var key = "not" + v.capitalize(name);
+ var msg = options[key] || this[key] || "must be %{type} %{count}";
+
+ errors.push(v.format(msg, {
+ count: count,
+ type: v.prettify(name)
+ }));
+ }
+ }
+
+ if (options.odd && value % 2 !== 1) {
+ errors.push(options.notOdd || this.notOdd || "must be odd");
+ }
+ if (options.even && value % 2 !== 0) {
+ errors.push(options.notEven || this.notEven || "must be even");
+ }
+
+ if (errors.length) {
+ return options.message || errors;
+ }
+ },
+ datetime: v.extend(function(value, options) {
+ if (!v.isFunction(this.parse) || !v.isFunction(this.format)) {
+ throw new Error("Both the parse and format functions needs to be set to use the datetime/date validator");
+ }
+
+ // Empty values are fine
+ if (v.isEmpty(value)) {
+ return;
+ }
+
+ options = v.extend({}, this.options, options);
+
+ var err
+ , errors = []
+ , earliest = options.earliest ? this.parse(options.earliest, options) : NaN
+ , latest = options.latest ? this.parse(options.latest, options) : NaN;
+
+ value = this.parse(value, options);
+
+ // 86400000 is the number of seconds in a day, this is used to remove
+ // the time from the date
+ if (isNaN(value) || options.dateOnly && value % 86400000 !== 0) {
+ return options.message || this.notValid || "must be a valid date";
+ }
+
+ if (!isNaN(earliest) && value < earliest) {
+ err = this.tooEarly || "must be no earlier than %{date}";
+ err = v.format(err, {date: this.format(earliest, options)});
+ errors.push(err);
+ }
+
+ if (!isNaN(latest) && value > latest) {
+ err = this.tooLate || "must be no later than %{date}";
+ err = v.format(err, {date: this.format(latest, options)});
+ errors.push(err);
+ }
+
+ if (errors.length) {
+ return options.message || errors;
+ }
+ }, {
+ parse: null,
+ format: null
+ }),
+ date: function(value, options) {
+ options = v.extend({}, options, {dateOnly: true});
+ return v.validators.datetime.call(v.validators.datetime, value, options);
+ },
+ format: function(value, options) {
+ if (v.isString(options) || (options instanceof RegExp)) {
+ options = {pattern: options};
+ }
+
+ options = v.extend({}, this.options, options);
+
+ var message = options.message || this.message || "is invalid"
+ , pattern = options.pattern
+ , match;
+
+ // Empty values are allowed
+ if (v.isEmpty(value)) {
+ return;
+ }
+ if (!v.isString(value)) {
+ return message;
+ }
+
+ if (v.isString(pattern)) {
+ pattern = new RegExp(options.pattern, options.flags);
+ }
+ match = pattern.exec(value);
+ if (!match || match[0].length != value.length) {
+ return message;
+ }
+ },
+ inclusion: function(value, options) {
+ // Empty values are fine
+ if (v.isEmpty(value)) {
+ return;
+ }
+ if (v.isArray(options)) {
+ options = {within: options};
+ }
+ options = v.extend({}, this.options, options);
+ if (v.contains(options.within, value)) {
+ return;
+ }
+ var message = options.message ||
+ this.message ||
+ "^%{value} is not included in the list";
+ return v.format(message, {value: value});
+ },
+ exclusion: function(value, options) {
+ // Empty values are fine
+ if (v.isEmpty(value)) {
+ return;
+ }
+ if (v.isArray(options)) {
+ options = {within: options};
+ }
+ options = v.extend({}, this.options, options);
+ if (!v.contains(options.within, value)) {
+ return;
+ }
+ var message = options.message || this.message || "^%{value} is restricted";
+ return v.format(message, {value: value});
+ },
+ email: v.extend(function(value, options) {
+ options = v.extend({}, this.options, options);
+ var message = options.message || this.message || "is not a valid email";
+ // Empty values are fine
+ if (v.isEmpty(value)) {
+ return;
+ }
+ if (!v.isString(value)) {
+ return message;
+ }
+ if (!this.PATTERN.exec(value)) {
+ return message;
+ }
+ }, {
+ PATTERN: /^[a-z0-9\u007F-\uffff!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9\u007F-\uffff!#$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/i
+ }),
+ equality: function(value, options, attribute, attributes) {
+ if (v.isEmpty(value)) {
+ return;
+ }
+
+ if (v.isString(options)) {
+ options = {attribute: options};
+ }
+ options = v.extend({}, this.options, options);
+ var message = options.message ||
+ this.message ||
+ "is not equal to %{attribute}";
+
+ if (v.isEmpty(options.attribute) || !v.isString(options.attribute)) {
+ throw new Error("The attribute must be a non empty string");
+ }
+
+ var otherValue = v.getDeepObjectValue(attributes, options.attribute)
+ , comparator = options.comparator || function(v1, v2) {
+ return v1 === v2;
+ };
+
+ if (!comparator(value, otherValue, options, attribute, attributes)) {
+ return v.format(message, {attribute: v.prettify(options.attribute)});
+ }
+ },
+
+ // A URL validator that is used to validate URLs with the ability to
+ // restrict schemes and some domains.
+ url: function(value, options) {
+ if (v.isEmpty(value)) {
+ return;
+ }
+
+ options = v.extend({}, this.options, options);
+
+ var message = options.message || this.message || "is not a valid url"
+ , schemes = options.schemes || this.schemes || ['http', 'https']
+ , allowLocal = options.allowLocal || this.allowLocal || false;
+
+ if (!v.isString(value)) {
+ return message;
+ }
+
+ // https://gist.github.com/dperini/729294
+ var regex =
+ "^" +
+ // schemes
+ "(?:(?:" + schemes.join("|") + "):\\/\\/)" +
+ // credentials
+ "(?:\\S+(?::\\S*)?@)?";
+
+ regex += "(?:";
+
+ var hostname =
+ "(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)" +
+ "(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*" +
+ "(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))";
+
+ // This ia a special case for the localhost hostname
+ if (allowLocal) {
+ hostname = "(?:localhost|" + hostname + ")";
+ } else {
+ // private & local addresses
+ regex +=
+ "(?!10(?:\\.\\d{1,3}){3})" +
+ "(?!127(?:\\.\\d{1,3}){3})" +
+ "(?!169\\.254(?:\\.\\d{1,3}){2})" +
+ "(?!192\\.168(?:\\.\\d{1,3}){2})" +
+ "(?!172" +
+ "\\.(?:1[6-9]|2\\d|3[0-1])" +
+ "(?:\\.\\d{1,3})" +
+ "{2})";
+ }
+
+ // reserved addresses
+ regex +=
+ "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
+ "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
+ "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
+ "|" +
+ hostname +
+ // port number
+ "(?::\\d{2,5})?" +
+ // path
+ "(?:\\/[^\\s]*)?" +
+ "$";
+
+ var PATTERN = new RegExp(regex, 'i');
+ if (!PATTERN.exec(value)) {
+ return message;
+ }
+ }
+ };
+
+ validate.exposeModule(validate, this, exports, module, define);
+}).call(this,
+ typeof exports !== 'undefined' ? /* istanbul ignore next */ exports : null,
+ typeof module !== 'undefined' ? /* istanbul ignore next */ module : null,
+ typeof define !== 'undefined' ? /* istanbul ignore next */ define : null);
diff --git a/Packages/lesiontracker/client/components/associationModal/associationModal.html b/Packages/lesiontracker/client/components/associationModal/associationModal.html
index 78fd6761f..ba3125479 100644
--- a/Packages/lesiontracker/client/components/associationModal/associationModal.html
+++ b/Packages/lesiontracker/client/components/associationModal/associationModal.html
@@ -10,8 +10,8 @@
{{ >studyAssociationTable }}
diff --git a/Packages/lesiontracker/client/components/associationModal/associationModal.js b/Packages/lesiontracker/client/components/associationModal/associationModal.js
index bbeea1ae7..62d522934 100644
--- a/Packages/lesiontracker/client/components/associationModal/associationModal.js
+++ b/Packages/lesiontracker/client/components/associationModal/associationModal.js
@@ -1,5 +1,116 @@
Template.associationModal.events({
'click #saveAssociations': function(e) {
- log.info("Saving associations");
+ log.info('Saving associations');
+
+ // Close the modal
+ var saveButton = $(e.currentTarget);
+ saveButton.attr('disabled', true);
+ saveButton.addClass('btn-success').removeClass('btn-primary');
+
+ // Find the rows of the study association table
+ var tableRows = $('#studyAssociationTable table tbody tr');
+
+ // Create an empty object to group studies into
+ var studies = {};
+
+ // Loop through each row to parse the data
+ tableRows.each(function() {
+ // Get a selector for this row
+ var row = $(this);
+
+ // Check the includeStudy checkbox to see if we should parse this row
+ var includeStudy = row.find('input.includeStudy[type="checkbox"]').eq(0).prop('checked');
+ if (!includeStudy) {
+ return;
+ }
+
+ // Find the selected timepoint option for this study
+ var timepointInput = row.find('input.timepointOption[type="radio"]:checked');
+
+ // Find the related label and trim it down to actual label (TODO: do this another way)
+ var timepointType = timepointInput.val();
+
+ // Get the study metaData by checking the row with the template engine Blaze
+ var data = Blaze.getData(this);
+
+ // Concatenate the study data to an array, depending on whether is was marked as baseline
+ // or follow-up
+ if (!studies.hasOwnProperty(timepointType)) {
+ studies[timepointType] = [];
+ }
+
+ studies[timepointType].push(data);
+ });
+
+ Object.keys(studies).forEach(function(timepointType) {
+ // Get the studies associated with this timepoint
+ var relatedStudies = studies[timepointType];
+
+ // Create an array of all the studyInstanceUids for storage in the Timepoint
+ var studyInstanceUids = relatedStudies.map(function(study) {
+ return study.studyInstanceUid;
+ });
+
+ // Create an array of all the studyDates for storage in the Timepoint
+ var studyDates = relatedStudies.map(function(study) {
+ return moment(study.studyDate, 'YYYYMMDD');
+ });
+
+ // Sort the study dates, so we can get a range for these values
+ studyDates = studyDates.sort();
+
+ // Create a new timepoint to represent the (baseline or follow-up) studies
+ var timepoint = {
+ timepointType: timepointType,
+ timepointId: uuid.new(),
+ studyInstanceUids: studyInstanceUids,
+ patientId: relatedStudies[0].patientId, // TODO: Revisit this (Should timepoints be related to patientId?)
+ earliestDate: studyDates[0].format('YYYYMMDD'),
+ latestDate: studyDates[studyDates.length - 1].format('YYYYMMDD')
+ };
+
+ // Insert this timepoint into the Timepoints Collection
+ Timepoints.insert(timepoint);
+
+ // Loop through these studies to associate them with the newly created timepoint
+ relatedStudies.forEach(function(study) {
+ // Check if a study already exists in the Studies collection
+ var existingStudy = Studies.findOne({
+ studyInstanceUid: study.studyInstanceUid
+ });
+
+ if (existingStudy) {
+ // If a study already exists, update the entry with the new timepointId
+ Studies.update(existingStudy._id, {
+ $set: {
+ timepointId: timepoint.timepointId
+ }
+ });
+ } else {
+ // If no such study exists, update the entry with the new timepointId
+
+ // Clear the ID from the document
+ delete study._id;
+
+ // Attach the timepointId and insert it into the Studies Collection
+ study.timepointId = timepoint.timepointId;
+ Studies.insert(study);
+ }
+ });
+ });
+
+ // Hide the modal
+ $('#associationModal').modal('hide');
+
+ // Reset the save button to its normal state
+ saveButton.removeClass('btn-success').addClass('btn-primary');
+ saveButton.attr('disabled', false);
+ },
+ 'click #cancelAssociation': function() {
+ // When the modal is closed, we should reset
+ // the save button to its normal state
+ var saveButton = $('#saveAssociations');
+ saveButton.removeClass('btn-success').addClass('btn-primary');
+ saveButton.attr('disabled', false);
}
-});
\ No newline at end of file
+});
diff --git a/Packages/lesiontracker/client/components/conformanceCheckFeedback/conformanceCheckFeedback.html b/Packages/lesiontracker/client/components/conformanceCheckFeedback/conformanceCheckFeedback.html
new file mode 100644
index 000000000..74a152193
--- /dev/null
+++ b/Packages/lesiontracker/client/components/conformanceCheckFeedback/conformanceCheckFeedback.html
@@ -0,0 +1,7 @@
+
+
+ {{ #each validationErrors }}
+
{{prefix}}{{error}}
+ {{ /each }}
+
+
diff --git a/Packages/lesiontracker/client/components/conformanceCheckFeedback/conformanceCheckFeedback.js b/Packages/lesiontracker/client/components/conformanceCheckFeedback/conformanceCheckFeedback.js
new file mode 100644
index 000000000..9ea592471
--- /dev/null
+++ b/Packages/lesiontracker/client/components/conformanceCheckFeedback/conformanceCheckFeedback.js
@@ -0,0 +1,11 @@
+Template.conformanceCheckFeedback.helpers({
+ validationErrors: function() {
+ // Return validation errors sorted by last added Target
+ return ValidationErrors.find({}, {
+ sort: {
+ prefix: -1,
+ type: 1
+ }
+ });
+ }
+});
\ No newline at end of file
diff --git a/Packages/lesiontracker/client/components/conformanceCheckFeedback/conformanceCheckFeedback.styl b/Packages/lesiontracker/client/components/conformanceCheckFeedback/conformanceCheckFeedback.styl
new file mode 100644
index 000000000..68fdf2d51
--- /dev/null
+++ b/Packages/lesiontracker/client/components/conformanceCheckFeedback/conformanceCheckFeedback.styl
@@ -0,0 +1,19 @@
+#conformanceCheckFeedback
+ position: absolute
+ right: 10px
+ color: darkorange
+ cursor: pointer
+ background: black
+
+ width: 30%
+ height: 30px
+ overflow: hidden
+
+ p
+ margin: 5px
+ text-align: center
+
+ &:hover
+ overflow: auto
+ height: 200px
+ z-index: 200
\ No newline at end of file
diff --git a/Packages/lesiontracker/client/components/lesionLocationDialog/lesionLocationDialog.js b/Packages/lesiontracker/client/components/lesionLocationDialog/lesionLocationDialog.js
index adbcef064..972fd6dc3 100644
--- a/Packages/lesiontracker/client/components/lesionLocationDialog/lesionLocationDialog.js
+++ b/Packages/lesiontracker/client/components/lesionLocationDialog/lesionLocationDialog.js
@@ -11,24 +11,29 @@ function closeHandler(dialog) {
// This event sets lesion number for new lesion
function setLesionNumberCallback(measurementData, eventData, doneCallback) {
- // Get the current element's timepointID from the study date metadata
+ // Get the current element's timepointId from the study date metadata
var element = eventData.element;
var enabledElement = cornerstone.getEnabledElement(element);
var imageId = enabledElement.image.imageId;
var study = cornerstoneTools.metaData.get('study', imageId);
+
+ // Find the relevant timepoint given the current study
var timepoint = Timepoints.findOne({
- timepointName: study.studyDate
+ studyInstanceUids: {
+ $in: [study.studyInstanceUid]
+ }
});
+
if (!timepoint) {
return;
}
- measurementData.timepointID = timepoint.timepointID;
+ measurementData.timepointId = timepoint.timepointId;
// Get a lesion number for this lesion, depending on whether or not the same lesion previously
// exists at a different timepoint
- var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointID, isTarget = true);
+ var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointId, isTarget = true);
measurementData.lesionNumber = lesionNumber;
// Set lesion number
@@ -226,6 +231,9 @@ Template.lesionLocationDialog.events({
/// Set the isTarget value to true, since this is the target-lesion dialog callback
measurementData.isTarget = true;
+ // Set the isNodal value based on the Location's properties
+ measurementData.isNodal = locationObj.isNodal;
+
// Adds lesion data to timepoints array
LesionManager.updateLesionData(measurementData);
} else {
@@ -233,6 +241,7 @@ Template.lesionLocationDialog.events({
$set: {
location: locationObj.location,
locationId: locationObj.id,
+ isNodal: locationObj.isNodal,
locationUID: id
}
});
diff --git a/Packages/lesiontracker/client/components/lesionTableTimepointCell/lesionTableTimepointCell.js b/Packages/lesiontracker/client/components/lesionTableTimepointCell/lesionTableTimepointCell.js
index 590ddec22..05d9f5a45 100644
--- a/Packages/lesiontracker/client/components/lesionTableTimepointCell/lesionTableTimepointCell.js
+++ b/Packages/lesiontracker/client/components/lesionTableTimepointCell/lesionTableTimepointCell.js
@@ -5,18 +5,18 @@ Template.lesionTableTimepointCell.helpers({
var lesionData = Template.parentData(1);
return (lesionData &&
lesionData.timepoints &&
- lesionData.timepoints[this.timepointID]);
+ lesionData.timepoints[this.timepointId]);
},
displayData: function() {
// Search Measurements by lesion and timepoint
var lesionData = Template.parentData(1);
if (!lesionData ||
!lesionData.timepoints ||
- !lesionData.timepoints[this.timepointID]) {
+ !lesionData.timepoints[this.timepointId]) {
return;
}
- var data = lesionData.timepoints[this.timepointID];
+ var data = lesionData.timepoints[this.timepointId];
if (lesionData.isTarget === true) {
if (data.shortestDiameter) {
@@ -40,7 +40,7 @@ function doneCallback(measurementData, deleteTool) {
// the specified Timepoint Cell
if (deleteTool === true) {
log.info('Confirm clicked!');
- clearMeasurementTimepointData(measurementData.id, measurementData.timepointID);
+ clearMeasurementTimepointData(measurementData.id, measurementData.timepointId);
}
}
@@ -57,12 +57,12 @@ Template.lesionTableTimepointCell.events({
var currentMeasurement = Template.parentData(1);
// Create some fake measurement data
- var currentTimepointID = this.timepointID;
+ var currentTimepointID = this.timepointId;
var timepointData = currentMeasurement.timepoints[currentTimepointID];
var measurementData = {
id: currentMeasurement._id,
- timepointID: currentTimepointID,
+ timepointId: currentTimepointID,
response: timepointData.response,
imageId: timepointData.imageId,
handles: timepointData.handles,
@@ -83,7 +83,7 @@ Template.lesionTableTimepointCell.events({
if (keyCode === keys.DELETE ||
(keyCode === keys.D && e.ctrlKey === true)) {
var currentMeasurement = Template.parentData(1);
- var currentTimepointID = this.timepointID;
+ var currentTimepointID = this.timepointId;
showConfirmDialog(function() {
clearMeasurementTimepointData(currentMeasurement._id, currentTimepointID);
diff --git a/Packages/lesiontracker/client/components/lesionTableTimepointHeader/lesionTableTimepointHeader.html b/Packages/lesiontracker/client/components/lesionTableTimepointHeader/lesionTableTimepointHeader.html
index b97ba8894..62f4bbd52 100644
--- a/Packages/lesiontracker/client/components/lesionTableTimepointHeader/lesionTableTimepointHeader.html
+++ b/Packages/lesiontracker/client/components/lesionTableTimepointHeader/lesionTableTimepointHeader.html
@@ -1,9 +1,6 @@
-
+
\ No newline at end of file
diff --git a/Packages/lesiontracker/client/components/lesionTrackerViewportOverlay/lesionTrackerViewportOverlay.js b/Packages/lesiontracker/client/components/lesionTrackerViewportOverlay/lesionTrackerViewportOverlay.js
new file mode 100644
index 000000000..9b8dbedad
--- /dev/null
+++ b/Packages/lesiontracker/client/components/lesionTrackerViewportOverlay/lesionTrackerViewportOverlay.js
@@ -0,0 +1,30 @@
+// Use Aldeed's meteor-template-extension package to replace the
+// default viewportOverlay template.
+// See https://github.com/aldeed/meteor-template-extension
+var defaultTemplate = 'viewportOverlay';
+Template.lesionTrackerViewportOverlay.replaces(defaultTemplate);
+
+// Add the TimepointName helper to the default template. The
+// HTML of this template is replaced with that of lesionTrackerViewportOverlay
+Template[defaultTemplate].helpers({
+ timepointName: function() {
+ var data = this;
+ var study = Studies.findOne({
+ studyInstanceUid: data.studyInstanceUid
+ });
+
+ if (!study) {
+ return;
+ }
+
+ var timepoint = Timepoints.findOne({
+ timepointId: study.timepointId
+ });
+
+ if (!timepoint) {
+ return;
+ }
+
+ return getTimepointName(timepoint);
+ }
+});
\ No newline at end of file
diff --git a/Packages/lesiontracker/client/components/lesionTrackerViewportOverlay/lesionTrackerViewportOverlay.styl b/Packages/lesiontracker/client/components/lesionTrackerViewportOverlay/lesionTrackerViewportOverlay.styl
new file mode 100644
index 000000000..5b317c2be
--- /dev/null
+++ b/Packages/lesiontracker/client/components/lesionTrackerViewportOverlay/lesionTrackerViewportOverlay.styl
@@ -0,0 +1,3 @@
+.imageViewerViewportOverlay
+ .timepointName
+ color: #32BFFF
\ No newline at end of file
diff --git a/Packages/worklist/components/studyContextMenu/studyContextMenu.html b/Packages/lesiontracker/client/components/lesionTrackerWorklistContextMenu/lesionTrackerWorklistContextMenu.html
similarity index 52%
rename from Packages/worklist/components/studyContextMenu/studyContextMenu.html
rename to Packages/lesiontracker/client/components/lesionTrackerWorklistContextMenu/lesionTrackerWorklistContextMenu.html
index ebf19fb34..a94b076bc 100644
--- a/Packages/worklist/components/studyContextMenu/studyContextMenu.html
+++ b/Packages/lesiontracker/client/components/lesionTrackerWorklistContextMenu/lesionTrackerWorklistContextMenu.html
@@ -1,11 +1,10 @@
-
+
diff --git a/Packages/lesiontracker/client/components/lesionTrackerWorklistContextMenu/lesionTrackerWorklistContextMenu.js b/Packages/lesiontracker/client/components/lesionTrackerWorklistContextMenu/lesionTrackerWorklistContextMenu.js
new file mode 100644
index 000000000..a7a6d7b65
--- /dev/null
+++ b/Packages/lesiontracker/client/components/lesionTrackerWorklistContextMenu/lesionTrackerWorklistContextMenu.js
@@ -0,0 +1,75 @@
+// Use Aldeed's meteor-template-extension package to replace the
+// default WorklistStudy template.
+// See https://github.com/aldeed/meteor-template-extension
+var defaultTemplate = 'studyContextMenu';
+Template.lesionTrackerWorklistContextMenu.replaces(defaultTemplate);
+
+Worklist.functions['removeTimepointAssociations'] = removeTimepointAssociations;
+
+/**
+ * Removes all present study / timepoint associations from the Clinical Trial
+ */
+function removeTimepointAssociations() {
+ // Get a Cursor pointing to the selected Studies from the Worklist
+ var selectedStudies = WorklistSelectedStudies.find({}, {
+ sort: {
+ studyDate: 1
+ }
+ });
+
+ // Loop through the Cursor of Selected Studies
+ selectedStudies.forEach(function(selectedStudy) {
+ // Find the selected study in question in the Collection of
+ // Timepoint/Study associated Studies
+ var study = Studies.findOne({
+ studyInstanceUid: selectedStudy.studyInstanceUid
+ });
+
+ // If the studies that were selected are not already associated
+ // with a Timepoint, stop here
+ if (!study) {
+ return;
+ }
+
+ // Update the Studies Collection to remove the link to this Timepoint
+ Studies.update(study._id, {
+ unset: {
+ timepointId: ''
+ }
+ });
+
+ // Find the Timepoint that was previously referenced
+ var timepoint = Timepoints.findOne({
+ timepointId: study.timepointId
+ });
+
+ // Find the index of the current studyInstanceUid in the array
+ // of reference studyInstanceUids
+ var index = timepoint.studyInstanceUids.indexOf(study.studyInstanceUid);
+ if (index < 0) {
+ return;
+ }
+
+ // Remove the specified studyInstanceUid from the array of associated studyInstanceUids
+ timepoint.studyInstanceUids.splice(index, 1);
+
+ // Check if there are still one or more Studies associated with this Timepoint
+ if (timepoint.studyInstanceUids.length) {
+ // Update the Timepoints Collection with this modified array for the
+ // studyInstanceUids attribute
+ Timepoints.update(timepoint._id, {
+ $set: {
+ studyInstanceUids: timepoint.studyInstanceUids
+ }
+ });
+ } else {
+ // If no more Studies are associated with this Timepoint, we should remove it
+ // from the Timepoints Collection via a server call
+ Meteor.call('removeTimepoint', timepoint._id, function(error) {
+ if (error) {
+ log.warn(error);
+ }
+ });
+ }
+ });
+}
\ No newline at end of file
diff --git a/Packages/lesiontracker/client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.html b/Packages/lesiontracker/client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.html
new file mode 100644
index 000000000..d3c8f8744
--- /dev/null
+++ b/Packages/lesiontracker/client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.html
@@ -0,0 +1,31 @@
+
+
+
\ No newline at end of file
diff --git a/Packages/lesiontracker/client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.js b/Packages/lesiontracker/client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.js
new file mode 100644
index 000000000..0f3eae242
--- /dev/null
+++ b/Packages/lesiontracker/client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.js
@@ -0,0 +1,30 @@
+// Use Aldeed's meteor-template-extension package to replace the
+// default WorklistStudy template.
+// See https://github.com/aldeed/meteor-template-extension
+var defaultTemplate = 'worklistStudy';
+Template.lesionTrackerWorklistStudy.replaces(defaultTemplate);
+
+// Add the TimepointName helper to the default template. The
+// HTML of this template is replaced with that of lesionTrackerWorklistStudy
+Template[defaultTemplate].helpers({
+ timepointName: function() {
+ var data = this;
+ var study = Studies.findOne({
+ studyInstanceUid: data.studyInstanceUid
+ });
+
+ if (!study) {
+ return;
+ }
+
+ var timepoint = Timepoints.findOne({
+ timepointId: study.timepointId
+ });
+
+ if (!timepoint) {
+ return;
+ }
+
+ return getTimepointName(timepoint);
+ }
+});
\ No newline at end of file
diff --git a/Packages/worklist/components/worklistStudy/worklistStudy.styl b/Packages/lesiontracker/client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.styl
similarity index 100%
rename from Packages/worklist/components/worklistStudy/worklistStudy.styl
rename to Packages/lesiontracker/client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.styl
diff --git a/Packages/lesiontracker/client/components/nonTargetLesionDialog/nonTargetLesionDialog.js b/Packages/lesiontracker/client/components/nonTargetLesionDialog/nonTargetLesionDialog.js
index 8b81bb739..6de536fd1 100644
--- a/Packages/lesiontracker/client/components/nonTargetLesionDialog/nonTargetLesionDialog.js
+++ b/Packages/lesiontracker/client/components/nonTargetLesionDialog/nonTargetLesionDialog.js
@@ -11,24 +11,29 @@ function closeHandler(dialog) {
// This event sets lesion number for new lesion
function setLesionNumberCallback(measurementData, eventData, doneCallback) {
- // Get the current element's timepointID from the study date metadata
+ // Get the current element's timepointId from the study date metadata
var element = eventData.element;
var enabledElement = cornerstone.getEnabledElement(element);
var imageId = enabledElement.image.imageId;
var study = cornerstoneTools.metaData.get('study', imageId);
+
+ // Find the relevant timepoint given the current study
var timepoint = Timepoints.findOne({
- timepointName: study.studyDate
+ studyInstanceUids: {
+ $in: [study.studyInstanceUid]
+ }
});
+
if (!timepoint) {
return;
}
- measurementData.timepointID = timepoint.timepointID;
+ measurementData.timepointId = timepoint.timepointId;
// Get a lesion number for this lesion, depending on whether or not the same lesion previously
// exists at a different timepoint
- var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointID, isTarget = false);
+ var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointId, isTarget = false);
measurementData.lesionNumber = lesionNumber;
// Set lesion number
@@ -211,7 +216,7 @@ changeNonTargetLocationCallback = function(measurementData, eventData, doneCallb
multi: true
});
- var response = measurement.timepoints[measurementData.timepointID].response;
+ var response = measurement.timepoints[measurementData.timepointId].response;
// TODO = Standardize this. Searching by code probably isn't the best, we should use
// some sort of UID
diff --git a/Packages/lesiontracker/client/components/nonTargetResponseDialog/nonTargetResponseDialog.js b/Packages/lesiontracker/client/components/nonTargetResponseDialog/nonTargetResponseDialog.js
index ca2615641..6559eabf4 100644
--- a/Packages/lesiontracker/client/components/nonTargetResponseDialog/nonTargetResponseDialog.js
+++ b/Packages/lesiontracker/client/components/nonTargetResponseDialog/nonTargetResponseDialog.js
@@ -65,7 +65,7 @@ changeNonTargetResponse = function(measurementData, eventData, doneCallback) {
return;
}
- var response = measurement.timepoints[measurementData.timepointID].response;
+ var response = measurement.timepoints[measurementData.timepointId].response;
// TODO = Standardize this. Searching by code probably isn't the best, we should use
// some sort of UID
diff --git a/Packages/lesiontracker/client/components/optionsButton/optionsButton.html b/Packages/lesiontracker/client/components/optionsButton/optionsButton.html
new file mode 100644
index 000000000..5227dac2f
--- /dev/null
+++ b/Packages/lesiontracker/client/components/optionsButton/optionsButton.html
@@ -0,0 +1,12 @@
+
+
+
\ No newline at end of file
diff --git a/Packages/lesiontracker/client/components/optionsModal/optionsModal.js b/Packages/lesiontracker/client/components/optionsModal/optionsModal.js
new file mode 100644
index 000000000..0cc2bf0b8
--- /dev/null
+++ b/Packages/lesiontracker/client/components/optionsModal/optionsModal.js
@@ -0,0 +1,33 @@
+Session.setDefault('TrialResponseAssessmentCriteria', 'RECIST');
+
+Template.optionsModal.events({
+ /**
+ * When the trial criteria radio buttons are changed, change the
+ * trial assessment criteria for the Lesion Tracker
+ *
+ * @param e The 'change' event on the selected radio button
+ */
+ 'change input.trialCriteria': function(e) {
+ // Get the Trial Criteria type that the selected radio button represents
+ var radioButton = $(e.currentTarget);
+ var criteriaType = radioButton.val();
+
+ // Set this as the current Trial Response Assessment Criteria
+ // TODO: Update when we have more trial-level support
+ // (Currently this information is stored in Session, later this will change)
+ Session.set('TrialResponseAssessmentCriteria', criteriaType);
+
+ log.info('Trial Criteria changed to: ' + criteriaType);
+ },
+ /**
+ * When the Clear Study/Timepoint Associations button is clicked, we
+ * send a call to the server to erase all entries in the Timepoints Collection.
+ */
+ 'click a.clearAllStudyTimepointAssociations': function() {
+ Meteor.call('clearAllTimepoints', function(error) {
+ if (error) {
+ log.warn(error);
+ }
+ });
+ }
+});
diff --git a/Packages/lesiontracker/client/components/optionsModal/optionsModal.styl b/Packages/lesiontracker/client/components/optionsModal/optionsModal.styl
new file mode 100644
index 000000000..9803d9d1f
--- /dev/null
+++ b/Packages/lesiontracker/client/components/optionsModal/optionsModal.styl
@@ -0,0 +1,2 @@
+.trialCriteriaLabel
+ margin: 0 5px
\ No newline at end of file
diff --git a/Packages/lesiontracker/client/components/studyAssociationTable/studyAssociationTable.html b/Packages/lesiontracker/client/components/studyAssociationTable/studyAssociationTable.html
index fadcaa8b2..f9b8aedf0 100644
--- a/Packages/lesiontracker/client/components/studyAssociationTable/studyAssociationTable.html
+++ b/Packages/lesiontracker/client/components/studyAssociationTable/studyAssociationTable.html
@@ -43,13 +43,15 @@
{{studyDescription}}
-
+
{{ #each timepointOptions }}
-
{{#unless isTouchDevice}}
-
- {{numberOfStudyRelatedInstances}}
-
+
+ {{numberOfStudyRelatedInstances}}
+
{{/unless}}
\ No newline at end of file
diff --git a/Packages/worklist/components/worklistStudy/worklistStudy.js b/Packages/worklist/client/components/worklistStudy/worklistStudy.js
similarity index 84%
rename from Packages/worklist/components/worklistStudy/worklistStudy.js
rename to Packages/worklist/client/components/worklistStudy/worklistStudy.js
index b0d5aa263..37c0d7a91 100644
--- a/Packages/worklist/components/worklistStudy/worklistStudy.js
+++ b/Packages/worklist/client/components/worklistStudy/worklistStudy.js
@@ -1,6 +1,3 @@
-Worklist = {};
-Worklist.previouslySelected = undefined;
-
// Maybe we should use regular Worklist collection?
WorklistSelectedStudies = new Meteor.Collection(null);
@@ -52,11 +49,7 @@ function handleShiftClick(studyRow, data) {
row.addClass('active');
// When we reach the currently clicked-on row, stop the loop
- if (row.is(studyRow)) {
- return false;
- }
-
- return true;
+ return !row.is(studyRow);
});
} else {
// Set the current study as selected
@@ -117,12 +110,29 @@ Template.worklistStudy.events({
studyRow.addClass('active');
}
},
- 'dblclick tr.worklistStudy': function() {
- // Use the formatPN template helper to clean up the patient name
- var title = Blaze._globalHelpers['formatPN'](this.patientName);
+ 'mousedown tr.worklistStudy': function(e) {
+ // This event handler is meant to handle middle-click on a study
+ if (e.which !== 2) {
+ return;
+ }
- // Open a new tab with this study
- openNewTab(this.studyInstanceUid, title);
+ var data = this;
+ var middleClickOnStudy = Worklist.callbacks.middleClickOnStudy;
+ if (middleClickOnStudy && typeof middleClickOnStudy === 'function') {
+ middleClickOnStudy(data);
+ }
+ },
+ 'dblclick tr.worklistStudy': function(e) {
+ if (e.which !== 1) {
+ return;
+ }
+
+ var data = this;
+ var dblClickOnStudy = Worklist.callbacks.dblClickOnStudy;
+
+ if (dblClickOnStudy && typeof dblClickOnStudy === 'function') {
+ dblClickOnStudy(data);
+ }
},
'contextmenu tr.worklistStudy': function(e, template) {
$(e.currentTarget).addClass('active');
@@ -134,10 +144,5 @@ Template.worklistStudy.events({
return false;
}
}
-});
-Template.worklistStudy.helpers({
- isTouchDevice: function() {
- return isTouchDevice();
- }
-});
+});
\ No newline at end of file
diff --git a/Packages/worklist/client/components/worklistStudy/worklistStudy.styl b/Packages/worklist/client/components/worklistStudy/worklistStudy.styl
new file mode 100644
index 000000000..024cbeb81
--- /dev/null
+++ b/Packages/worklist/client/components/worklistStudy/worklistStudy.styl
@@ -0,0 +1,2 @@
+.worklistStudy
+ cursor: pointer
\ No newline at end of file
diff --git a/Packages/worklist/components/tabTitle/tabTitle.js b/Packages/worklist/components/tabTitle/tabTitle.js
deleted file mode 100644
index 1751db6dd..000000000
--- a/Packages/worklist/components/tabTitle/tabTitle.js
+++ /dev/null
@@ -1,87 +0,0 @@
-Template.tabTitle.events({
- /**
- * Closes a tab when the close button is pressed in the title
- * The next tab to the left is loaded if the current tab is closed
- *
- * @param e The click event used to close the tab
- */
- 'click .close': function(e) {
- // Identify the tab title DOM node
- var tab = $(e.currentTarget).parents('a[data-toggle="tab"]').eq(0);
-
- // Get the relevent contentId that this tab title represents
- // Replace any hash marks (#) that were required by Bootstrap's tab switching
- var contentId = tab.data("target").replace("#", "");
-
- // Check if we are closing the active tab. If we are, prepare to switch
- // to the next tab to the left.
- var activeContentId = Session.get('activeContentId');
- if (activeContentId === contentId) {
- // Find the index of the tab that is being closed
- var tabIndex = tab.parent('li').index();
-
- // Find the index the tab to its left
- var newActiveTabIndex = Math.max(tabIndex - 1, 0);
-
- // Find the DOM node of the tab that will be activated
- var newActiveTab = $(".tabTitle").eq(newActiveTabIndex);
-
- // Find the content ID of the tab that will be switched to
- var newActiveTabLink = newActiveTab.find("a[data-toggle=tab]");
- var newContentId = newActiveTabLink.data("target").replace("#", "");
-
- // Switch to this tab
- switchToTab(newContentId);
- }
-
- // Find the tab to be closed in the Tabs collection
- var tabObjectId = WorklistTabs.findOne({contentid: contentId})._id;
-
- // Remove this tab from the Tabs collection so it is no longer rendered
- WorklistTabs.remove(tabObjectId);
-
- // Remove any stored data related to this tab from the global ViewerData structure
- delete ViewerData[contentId];
-
- }
-});
-
-// Set tab width when a tab is added or removed
-function setTabWidth (){
- var allTabTitles = $(".tabTitle");
- var widthTabList = $("#tablist").width();
- var totalTitleWidths = 0;
-
- var tabCount = 0;
- allTabTitles.each( function( index, tabItem ) {
- totalTitleWidths += $(tabItem).width();
- tabCount ++;
- });
-
- if(totalTitleWidths > widthTabList) {
- var newTabWidth = widthTabList / tabCount;
- allTabTitles.each( function( index, tabItem ) {
- $(tabItem).css("width",newTabWidth+"px");
- });
- } else {
- var newTabWidth = widthTabList / tabCount;
- allTabTitles.each( function( index, tabItem ) {
- if (index === 0) {
- if(newTabWidth > 95) {
- $(tabItem).css("width","95px");
- } else {
- $(tabItem).css("width",newTabWidth+"px");
- }
- } else {
- if(newTabWidth > 130) {
- $(tabItem).css("width","130px");
- } else {
- $(tabItem).css("width",newTabWidth+"px");
- }
- }
-
- });
- }
-}
-Template.tabTitle.onRendered(function(){
-});
diff --git a/Packages/worklist/components/worklist.js b/Packages/worklist/components/worklist.js
deleted file mode 100644
index be47cf775..000000000
--- a/Packages/worklist/components/worklist.js
+++ /dev/null
@@ -1,218 +0,0 @@
-/**
- * Template: Worklist
- *
- * This is the main component of the Worklist package
- */
-
-// Define the ViewerData global object
-// If there is currently any Session data for this object,
-// use this to repopulate the variable
-ViewerData = Session.get('ViewerData') || {};
-
-// Define the StudyMetaData object. This is used as a cache
-// to store study meta data information to prevent unnecessary
-// calls to the server
-var StudyMetaData = {};
-
-// Create the WorklistTabs collection
-WorklistTabs = new Meteor.Collection(null);
-
-// Create the WorklistStudies collection
-WorklistStudies = new Meteor.Collection(null);
-
-/**
-* Retrieves study metadata using a server call, and fires a callback
-* when completed.
-*
-* @params {string} studyInstanceUid The UID of the Study to be retrieved
-* @params {function} doneCallback The callback function to be executed when the study retrieval has finished
-*/
-getStudyMetadata = function(studyInstanceUid, doneCallback) {
- log.info('worklistStudy getStudyMetadata');
-
- // If the StudyMetaData cache already has data related to this
- // studyInstanceUid, then we should fire the doneCallback with this data
- // and stop here.
- var study = StudyMetaData[studyInstanceUid];
- if (study) {
- doneCallback(study);
- return;
- }
-
- // If no study metadata is in the cache variable, we need to retrieve it from
- // the server with a call.
- Meteor.call('GetStudyMetadata', studyInstanceUid, function(error, study) {
- if (error) {
- log.warn(error);
- return;
- }
- // Once we have retrieved the data, we sort the series' by series
- // and instance number in ascending order
- sortStudy(study);
-
- // Then we store this data in the cache variable
- StudyMetaData[studyInstanceUid] = study;
-
- // Finally, we fire the doneCallback with this study meta data
- doneCallback(study);
-
- // Temporary: Testing out the Clinical Meteor HIPAA Audit log
- if (Meteor.user()) {
- log.info('Adding access to HIPAA Log');
- var hipaaEvent = {
- eventType: "access",
- userId: Meteor.userId(),
- userName: Meteor.user().profile.fullName,
- collectionName: "Studies",
- recordId: studyInstanceUid,
- patientId: null,
- patientName: null
- };
- HipaaLogger.logEvent(hipaaEvent);
- }
- });
-};
-
-/**
- * Switches to a new tab in the tabbed worklist container
- * This function renders either the Worklist or the Viewer template with new data.
- *
- * @param contentId The unique ID of the tab to be switched to
- */
-switchToTab = function(contentId) {
- log.info('Switching to tab: ' + contentId);
-
- // Use Bootstrap's Tab JavaScript to show the contents of the current tab
- // Unless it is the worklist, it is currently an empty div
- $('.tabTitle a[data-target="#' + contentId + '"]').tab('show');
-
- // Remove any previous Viewers from the DOM
- $('#viewer').remove();
-
- // Update the 'activeContentId' variable in Session
- Session.set('activeContentId', contentId);
-
- // If we are switching to the Worklist tab, reset any CSS styles
- // that have been applied to prevent scrolling in the Viewer.
- // Then stop here, since nothing needs to be re-rendered.
- if (contentId === 'worklistTab') {
- document.body.style.overflow = null;
- document.body.style.height = null;
- document.body.style.minWidth = null;
- document.body.style.position = null;
- return;
- }
-
- // Get tab content container given the contentId string
- // If no such container exists, stop here because something is wrong
- var container = $('.tab-content').find('#' + contentId).get(0);
- if (!container) {
- log.warn('No container present with the contentId: ' + contentId);
- return;
- }
-
- // Use the stored ViewerData global object to retrieve the studyInstanceUid
- // related to this tab
- var studyInstanceUid = ViewerData[contentId].studyInstanceUid;
-
- // Attempt to retrieve the meta data (it might be cached)
- getStudyMetadata(studyInstanceUid, function(study) {
-
- // Once we have the study data, store it in a structure with
- // any other saved data about this tab (e.g. layout structure)
- var data = {
- viewportRows: ViewerData[contentId].viewportRows,
- viewportColumns: ViewerData[contentId].viewportColumns,
- contentId: contentId,
- studies: [ study ]
- };
-
- if (ViewerData[contentId].studies && ViewerData[contentId].studies.length) {
- data.studies = ViewerData[contentId].studies;
- }
-
- // Remove the loading text template that is inside the tab container by default
- container.innerHTML = '';
-
- // Use Blaze to render the Viewer Template into the container
- UI.renderWithData(Template.viewer, data, container);
-
- // Retrieve the DOM element of the viewer
- var imageViewer = $('#viewer');
-
- // If it is present in the DOM (it should be), then apply
- // styles to prevent page scrolling and overscrolling on mobile devices
- if (imageViewer) {
- 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
- }
- });
-};
-
-/**
- * Opens a new tab in the tabbed worklist environment using
- * a given study and new tab title.
- *
- * @param studyInstanceUid The UID of the Study to be opened
- * @param title The title to be used for the tab heading
- */
-openNewTab = function(studyInstanceUid, title) {
- // Generate a unique ID to represent this tab
- // We can't just use the Mongo entry ID because
- // then it will change after hot-reloading.
- var contentid = generateUUID();
-
- // Create a new entry in the WorklistTabs Collection
- WorklistTabs.insert({
- title: title,
- contentid: contentid,
- active: false
- });
-
- // Update the ViewerData global object
- ViewerData[contentid] = {
- title: title,
- contentid: contentid,
- studyInstanceUid: studyInstanceUid
- };
-
- // Switch to the new tab
- switchToTab(contentid);
-};
-
-Template.worklist.onRendered(function() {
- // If there is a tab set as active in the Session,
- // switch to that now.
- var contentId = Session.get('activeContentId');
- if (contentId) {
- switchToTab(contentId);
- }
-});
-
-Template.worklist.helpers({
- /**
- * Returns the current set of Worklist Tabs
- * @returns Meteor.Collection The current state of the WorklistTabs Collection
- */
- worklistTabs: function() {
- return WorklistTabs.find();
- },
-});
-
-Template.worklist.events({
- 'click #tablist a[data-toggle="tab"]': function(e) {
- // If this tab is already active, do nothing
- var tabButton = $(e.currentTarget);
- var tabTitle = tabButton.parents('.tabTitle');
- if (tabTitle.hasClass('active')) {
- return;
- }
-
- // Otherwise, switch to the tab
- var contentId = tabButton.data('target').replace('#', '');
- switchToTab(contentId);
- }
-});
diff --git a/Packages/worklist/components/worklistResult/worklistResult.html b/Packages/worklist/components/worklistResult/worklistResult.html
deleted file mode 100644
index f0d9248e9..000000000
--- a/Packages/worklist/components/worklistResult/worklistResult.html
+++ /dev/null
@@ -1,68 +0,0 @@
-
-
- {{ #unless studies.count }}
- {{ >loadingText }}
- {{ /unless }}
-
\ No newline at end of file
diff --git a/Packages/worklist/lib/generateUUID.js b/Packages/worklist/lib/generateUUID.js
deleted file mode 100644
index 7bc60c1b5..000000000
--- a/Packages/worklist/lib/generateUUID.js
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- *
- * @returns {string}
- */
-generateUUID = function() {
- 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;
-};
diff --git a/Packages/worklist/lib/getStudiesMetadata.js b/Packages/worklist/lib/getStudiesMetadata.js
new file mode 100644
index 000000000..7942b9ca6
--- /dev/null
+++ b/Packages/worklist/lib/getStudiesMetadata.js
@@ -0,0 +1,62 @@
+/**
+ * Retrieves metaData for multiple studies at once.
+ *
+ * This function calls getStudyMetadata several times, asynchronously,
+ * and waits for all of the results to be returned.
+ *
+ * @param studyInstanceUids The UIDs of the Studies to be retrieved
+ * @param doneCallback The callback function to be executed when the study retrieval has finished
+ * @param failCallback The callback function to be executed when the study retrieval has failed
+ */
+getStudiesMetadata = function(studyInstanceUids, doneCallback, failCallback) {
+ // Check to make sure studyInstanceUids were actually input
+ if (!studyInstanceUids || !studyInstanceUids.length) {
+ if (failCallback && typeof failCallback === 'function') {
+ failCallback('No studyInstanceUids were input');
+ }
+
+ return;
+ }
+
+ // Create an empty array to store the Promises for each metaData
+ // retrieval call
+ var promises = [];
+
+ // Create an empty array to hold all the results of the promises
+ var studies = [];
+
+
+ // Loop through the array of studyInstanceUids
+ studyInstanceUids.forEach(function(studyInstanceUid) {
+ // Create a new Deferred to monitor the progress of the asynchronous
+ // metaData retrieval
+ var deferred = new $.Deferred();
+
+ // Send the call, and attach doneCallbacks and failCallbacks
+ // which can resolve or reject the related promise based on its outcome
+ getStudyMetadata(studyInstanceUid, function(study) {
+ deferred.resolve(study);
+ }, function(error) {
+ deferred.reject(error);
+ });
+
+ // Add the current promise to the array of promises
+ promises.push(deferred.promise());
+ });
+
+ // When all of the promises are complete, this callback runs
+ $.when.apply($, promises).done(function() {
+ // Convert the Arguments Array-like Object to an actual array
+ var studies = $.makeArray(arguments);
+
+ // Pass the studies array to the doneCallback, if one exists
+ if (doneCallback && typeof doneCallback === 'function') {
+ doneCallback(studies);
+ }
+ }).fail(function(error) {
+ log.warn(error);
+ if (failCallback && typeof failCallback === 'function') {
+ failCallback(error);
+ }
+ });
+};
\ No newline at end of file
diff --git a/Packages/worklist/lib/getStudyMetadata.js b/Packages/worklist/lib/getStudyMetadata.js
new file mode 100644
index 000000000..51ba83645
--- /dev/null
+++ b/Packages/worklist/lib/getStudyMetadata.js
@@ -0,0 +1,45 @@
+// Define the StudyMetaData object. This is used as a cache
+// to store study meta data information to prevent unnecessary
+// calls to the server
+var StudyMetaData = {};
+
+/**
+ * Retrieves study metadata using a server call, and fires a callback
+ * when completed.
+ *
+ * @params {string} studyInstanceUid The UID of the Study to be retrieved
+ * @params {function} doneCallback The callback function to be executed when the study retrieval has finished
+ * @param failCallback The callback function to be executed when the study retrieval has failed
+ */
+getStudyMetadata = function(studyInstanceUid, doneCallback, failCallback) {
+ log.info('worklistStudy getStudyMetadata');
+
+ // If the StudyMetaData cache already has data related to this
+ // studyInstanceUid, then we should fire the doneCallback with this data
+ // and stop here.
+ var study = StudyMetaData[studyInstanceUid];
+ if (study) {
+ doneCallback(study);
+ return;
+ }
+
+ // If no study metadata is in the cache variable, we need to retrieve it from
+ // the server with a call.
+ Meteor.call('GetStudyMetadata', studyInstanceUid, function(error, study) {
+ if (error) {
+ log.warn(error);
+ failCallback(error);
+ return;
+ }
+
+ // Once we have retrieved the data, we sort the series' by series
+ // and instance number in ascending order
+ sortStudy(study);
+
+ // Then we store this data in the cache variable
+ StudyMetaData[studyInstanceUid] = study;
+
+ // Finally, we fire the doneCallback with this study meta data
+ doneCallback(study);
+ });
+};
\ No newline at end of file
diff --git a/Packages/worklist/lib/openNewTab.js b/Packages/worklist/lib/openNewTab.js
new file mode 100644
index 000000000..b77591bfb
--- /dev/null
+++ b/Packages/worklist/lib/openNewTab.js
@@ -0,0 +1,30 @@
+/**
+ * Opens a new tab in the tabbed worklist environment using
+ * a given study and new tab title.
+ *
+ * @param studyInstanceUid The UID of the Study to be opened
+ * @param title The title to be used for the tab heading
+ */
+openNewTab = function(studyInstanceUid, title) {
+ // Generate a unique ID to represent this tab
+ // We can't just use the Mongo entry ID because
+ // then it will change after hot-reloading.
+ var contentid = uuid.new();
+
+ // Create a new entry in the WorklistTabs Collection
+ WorklistTabs.insert({
+ title: title,
+ contentid: contentid,
+ active: false
+ });
+
+ // Update the ViewerData global object
+ ViewerData[contentid] = {
+ title: title,
+ contentid: contentid,
+ studyInstanceUids: [studyInstanceUid]
+ };
+
+ // Switch to the new tab
+ switchToTab(contentid);
+};
diff --git a/Packages/worklist/lib/switchToTab.js b/Packages/worklist/lib/switchToTab.js
new file mode 100644
index 000000000..8c53d5b6a
--- /dev/null
+++ b/Packages/worklist/lib/switchToTab.js
@@ -0,0 +1,90 @@
+/**
+ * Switches to a new tab in the tabbed worklist container
+ * This function renders either the Worklist or the Viewer template with new data.
+ *
+ * @param contentId The unique ID of the tab to be switched to
+ */
+switchToTab = function(contentId) {
+ if (!contentId) {
+ return;
+ }
+ log.info('Switching to tab: ' + contentId);
+
+ // Use Bootstrap's Tab JavaScript to show the contents of the current tab
+ // Unless it is the worklist, it is currently an empty div
+ $('.tabTitle a[data-target="#' + contentId + '"]').tab('show');
+
+ // Remove any previous Viewers from the DOM
+ $('#viewer').remove();
+
+ // Update the 'activeContentId' variable in Session
+ Session.set('activeContentId', contentId);
+
+ // If we are switching to the Worklist tab, reset any CSS styles
+ // that have been applied to prevent scrolling in the Viewer.
+ // Then stop here, since nothing needs to be re-rendered.
+ if (contentId === 'worklistTab') {
+ document.body.style.overflow = null;
+ document.body.style.height = null;
+ document.body.style.minWidth = null;
+ document.body.style.position = null;
+ return;
+ }
+
+ // Tab was closed at some point, stop here
+ if (!ViewerData[contentId]) {
+ return;
+ }
+
+ // Use the stored ViewerData global object to retrieve the studyInstanceUid
+ // related to this tab
+ var studyInstanceUids = ViewerData[contentId].studyInstanceUids;
+
+ // Attempt to retrieve the meta data (it might be cached)
+ getStudiesMetadata(studyInstanceUids, function(studies) {
+ // Tab closed while study data was being retrieved, stop here
+ if (!ViewerData[contentId]) {
+ log.warn('Tab closed while study data was being retrieved');
+ return;
+ }
+
+ // Once we have the study data, store it in a structure with
+ // any other saved data about this tab (e.g. layout structure)
+ var data = jQuery.extend({}, ViewerData[contentId]);
+ data.studies = studies;
+ data.contentId = contentId;
+
+ if (ViewerData[contentId].studies && ViewerData[contentId].studies.length) {
+ data.studies = ViewerData[contentId].studies;
+ }
+
+ // Get tab content container given the contentId string
+ // If no such container exists, stop here because something is wrong
+ var container = $('.tab-content').find('#' + contentId).get(0);
+ if (!container) {
+ log.warn('No container present with the contentId: ' + contentId);
+ return;
+ }
+
+ // Remove the loading text template that is inside the tab container by default
+ container.innerHTML = '';
+
+ // Use Blaze to render the Viewer Template into the container
+ UI.renderWithData(Template.viewer, data, container);
+
+ // Retrieve the DOM element of the viewer
+ var imageViewer = $('#viewer');
+
+ // If it is present in the DOM (it should be), then apply
+ // styles to prevent page scrolling and overscrolling on mobile devices
+ if (imageViewer) {
+ document.body.style.overflow = 'hidden';
+ document.body.style.height = '100%';
+ document.body.style.width = '100%';
+ document.body.style.minWidth = 0;
+
+ // Prevent overscroll on mobile devices
+ document.body.style.position = 'fixed';
+ }
+ });
+};
\ No newline at end of file
diff --git a/Packages/worklist/lib/worklist.js b/Packages/worklist/lib/worklist.js
new file mode 100644
index 000000000..eee59b45b
--- /dev/null
+++ b/Packages/worklist/lib/worklist.js
@@ -0,0 +1,13 @@
+Worklist = {
+ functions: {},
+ callbacks: {}
+};
+
+Worklist.callbacks.dblClickOnStudy = dblClickOnStudy;
+Worklist.callbacks.middleClickOnStudy = dblClickOnStudy;
+
+function dblClickOnStudy(data) {
+ // Use the formatPN template helper to clean up the patient name
+ var title = formatPN(data.patientName);
+ openNewTab(data.studyInstanceUid, title);
+}
\ No newline at end of file
diff --git a/Packages/worklist/package.js b/Packages/worklist/package.js
index 799224e4c..aec98651b 100644
--- a/Packages/worklist/package.js
+++ b/Packages/worklist/package.js
@@ -11,8 +11,8 @@ Package.onUse(function (api) {
api.use('jquery');
api.use('stylus');
api.use('http');
-
api.use('practicalmeteor:loglevel');
+ api.use('rwatts:uuid');
// Our custom packages
api.use('dicomweb');
@@ -23,36 +23,44 @@ Package.onUse(function (api) {
api.addFiles('log.js', 'client');
// Components
- api.addFiles('components/worklist.html', 'client');
- api.addFiles('components/worklist.js', 'client');
- api.addFiles('components/worklist.styl', 'client');
+ api.addFiles('client/components/worklist.html', 'client');
+ api.addFiles('client/components/worklist.js', 'client');
+ api.addFiles('client/components/worklist.styl', 'client');
- api.addFiles('components/tabTitle/tabTitle.html', 'client');
- api.addFiles('components/tabTitle/tabTitle.js', 'client');
- api.addFiles('components/tabTitle/tabTitle.styl', 'client');
+ api.addFiles('client/components/tabTitle/tabTitle.html', 'client');
+ api.addFiles('client/components/tabTitle/tabTitle.js', 'client');
+ api.addFiles('client/components/tabTitle/tabTitle.styl', 'client');
- api.addFiles('components/tabContent/tabContent.html', 'client');
- api.addFiles('components/tabContent/tabContent.styl', 'client');
+ api.addFiles('client/components/tabContent/tabContent.html', 'client');
+ api.addFiles('client/components/tabContent/tabContent.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('client/components/worklistStudy/worklistStudy.html', 'client');
+ api.addFiles('client/components/worklistStudy/worklistStudy.js', 'client');
+ api.addFiles('client/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('client/components/worklistResult/worklistResult.html', 'client');
+ api.addFiles('client/components/worklistResult/worklistResult.js', 'client');
+ api.addFiles('client/components/worklistResult/worklistResult.styl', 'client');
- api.addFiles('components/studyContextMenu/studyContextMenu.html', 'client');
- api.addFiles('components/studyContextMenu/studyContextMenu.js', 'client');
- api.addFiles('components/studyContextMenu/studyContextMenu.styl', 'client');
+ api.addFiles('client/components/studyContextMenu/studyContextMenu.html', 'client');
+ api.addFiles('client/components/studyContextMenu/studyContextMenu.js', 'client');
+ api.addFiles('client/components/studyContextMenu/studyContextMenu.styl', 'client');
- api.addFiles('lib/generateUUID.js', 'client');
- api.export('generateUUID', 'client');
+ // Library functions
+ api.addFiles('lib/getStudyMetadata.js', 'client');
+ api.addFiles('lib/getStudiesMetadata.js', 'client');
+ api.addFiles('lib/openNewTab.js', 'client');
+ api.addFiles('lib/switchToTab.js', 'client');
+ api.addFiles('lib/worklist.js', 'client');
// Export Worklist helper functions for usage in Routes
+ api.export('getTimepointName', 'client');
api.export('getStudyMetadata', 'client');
+ api.export('getStudiesMetadata', 'client');
api.export('openNewTab', 'client');
+ api.export('setWorklistSubscriptions', 'client');
api.export('switchToTab', 'client');
+ api.export('Worklist');
// Export the global ViewerData object
api.export('ViewerData', 'client');
@@ -61,5 +69,4 @@ Package.onUse(function (api) {
api.export('WorklistTabs', 'client');
api.export('WorklistStudies', 'client');
api.export('WorklistSelectedStudies', 'client');
-});
-
+});
\ No newline at end of file
diff --git a/config/localhostOrthanc.json b/config/localhostOrthanc.json
index 0097b2f56..9b31cd918 100644
--- a/config/localhostOrthanc.json
+++ b/config/localhostOrthanc.json
@@ -4,7 +4,7 @@
{
"name": "Orthanc",
"wadoUriRootNOTE" : "either this uri is not correct for wado-uri or wado-uri is not configured on orthanc currently",
- "wadoUriRoot" : "http://localhost:8043/wado",
+ "wadoUriRoot" : "http://localhost:8042/wado",
"qidoRoot": "http://localhost:8042/dicom-web",
"wadoRoot": "http://localhost:8042/dicom-web",
"qidoSupportsIncludeField": false,
@@ -17,5 +17,11 @@
}
}
]
- }
+ },
+ "dimse" : {
+ "host" : "localhost",
+ "port" : 4242,
+ "hostAE" : "ORTHANC"
+ },
+ "defaultServiceType": "dicomWeb"
}
diff --git a/config/orthancDIMSE.json b/config/orthancDIMSE.json
new file mode 100644
index 000000000..c83f47c5c
--- /dev/null
+++ b/config/orthancDIMSE.json
@@ -0,0 +1,27 @@
+{
+ "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
+ }
+ }
+ ]
+ },
+ "dimse" : {
+ "host" : "localhost",
+ "port" : 4242,
+ "hostAE" : "ORTHANC"
+ },
+ "defaultServiceType": "dimse"
+}