From beaa56c0b3ccc96480b50106a70e388c5c5c0797 Mon Sep 17 00:00:00 2001 From: Erik Ziegler Date: Wed, 13 Jan 2016 16:17:32 +0100 Subject: [PATCH] Updates for Timepoint assocation (LT-60) and Conformance Checks (LT-92, LT-93, LT-94) --- .gitignore | 4 +- .jscsrc | 6 +- LesionTracker/.meteor/packages | 1 + LesionTracker/.meteor/versions | 3 + LesionTracker/client/body.styl | 70 +- LesionTracker/client/components/viewer.html | 1 + LesionTracker/client/components/viewer.js | 255 +--- LesionTracker/client/mobileStyles.css | 51 - LesionTracker/client/routeHooks.js | 5 - LesionTracker/client/routes.js | 24 +- .../client/stylesheets/docking-container.css | 55 - LesionTracker/defaultSettings.js | 40 +- OHIFViewer/.meteor/versions | 1 + .../client/cornerstoneWADOImageLoader.js | 1057 +++++++++------- .../client/cornerstoneWADORSImageLoader.js | 77 -- Packages/cornerstone/client/dicomParser.js | 10 +- Packages/cornerstone/package.js | 53 +- Packages/dicomweb/lib/findIndexOfString.js | 52 - Packages/dicomweb/lib/uint8ArrayToString.js | 18 - Packages/dicomweb/package.js | 7 +- Packages/dicomweb/server/getImageFrame.js | 74 -- Packages/dicomweb/server/getJSON.js | 4 +- .../etc/sampleOrthancConfig.json | 6 - .../dimseservice/etc/sampleOrthancConfig.json | 5 + .../package.js | 0 .../server/Connection.js | 0 .../server/DIMSE.js | 49 +- .../server/Data.js | 0 .../server/Field.js | 0 .../server/Message.js | 0 .../server/PDU.js | 0 .../server/RWStream.js | 0 .../server/constants.js | 0 .../server/elements_data.js | 0 .../server/methods.js | 0 .../server/require.js | 0 Packages/lesiontracker/both/collections.js | 3 + .../client/collections/LesionLocations.js | 37 +- .../client/collections/PatientLocations.js | 1 + .../client/compatibility/LesionManager.js | 190 --- .../client/compatibility/lesionTool.js | 13 +- .../client/compatibility/nonTargetTool.js | 16 +- .../client/compatibility/scaleOverlayTool.js | 3 +- .../client/compatibility/validate.js | 1086 +++++++++++++++++ .../associationModal/associationModal.html | 4 +- .../associationModal/associationModal.js | 115 +- .../conformanceCheckFeedback.html | 7 + .../conformanceCheckFeedback.js | 11 + .../conformanceCheckFeedback.styl | 19 + .../lesionLocationDialog.js | 17 +- .../lesionTableTimepointCell.js | 14 +- .../lesionTableTimepointHeader.html | 9 +- .../lesionTableTimepointHeader.js | 107 +- .../lesionTrackerLayout.html | 5 +- .../lesionTrackerLayout.styl | 0 .../lesionTrackerViewportOverlay.html | 26 + .../lesionTrackerViewportOverlay.js | 30 + .../lesionTrackerViewportOverlay.styl | 3 + .../lesionTrackerWorklistContextMenu.html} | 19 +- .../lesionTrackerWorklistContextMenu.js | 75 ++ .../lesionTrackerWorklistStudy.html | 31 + .../lesionTrackerWorklistStudy.js | 30 + .../lesionTrackerWorklistStudy.styl} | 0 .../nonTargetLesionDialog.js | 15 +- .../nonTargetResponseDialog.js | 2 +- .../optionsButton/optionsButton.html | 12 + .../optionsButton/optionsButton.styl | 6 + .../components/optionsModal/optionsModal.html | 83 ++ .../components/optionsModal/optionsModal.js | 33 + .../components/optionsModal/optionsModal.styl | 2 + .../studyAssociationTable.html | 12 +- .../studyAssociationTable.js | 56 +- .../studyDateList/studyDateList.html | 2 +- .../components/studyDateList/studyDateList.js | 83 +- Packages/lesiontracker/lib/LesionManager.js | 202 +++ .../lib/TrialCriteriaConstraints.js | 388 ++++++ .../lib/TrialResponseCriteria.js | 343 ++++++ .../lesiontracker/lib/activateMeasurements.js | 12 +- .../lib/calculateTotalLesionBurden.js | 59 + .../lib/clearMeasurementTimepointData.js | 3 +- Packages/lesiontracker/lib/clearTools.js | 3 + .../lesiontracker/lib/getTimepointName.js | 44 + .../lesiontracker/lib/getTimepointObject.js | 4 +- .../lib/handleMeasurementAdded.js | 12 + .../lib/handleMeasurementModified.js | 12 + .../lib/handleMeasurementRemoved.js | 19 + .../lib/pixelSpacingAutorunCheck.js | 30 + .../lib/syncMeasurementAndToolData.js | 75 ++ .../lib/updateRelatedElements.js | 13 + Packages/lesiontracker/lib/uuid.js | 250 ---- .../lib/worklist/openNewTabWithTimepoint.js | 108 ++ .../lib/worklist/worklistModification.js | 37 + Packages/lesiontracker/package.js | 87 +- Packages/lesiontracker/server/collections.js | 12 +- .../lesiontracker/server/removeCollections.js | 6 + .../viewer/viewportOverlay/viewportOverlay.js | 53 +- .../viewportOverlay/viewportOverlay.styl | 3 +- Packages/viewerbase/lib/enableHotkeys.js | 128 +- Packages/viewerbase/lib/getWADORSImageId.js | 16 +- Packages/viewerbase/lib/helpers/formatPN.js | 17 +- Packages/viewerbase/lib/helpers/inlineIf.js | 2 +- .../viewerbase/lib/helpers/isTouchDevice.js | 20 + Packages/viewerbase/lib/isTouchDevice.js | 13 - Packages/viewerbase/lib/metaDataProvider.js | 1 + Packages/viewerbase/lib/stringToUint8Array.js | 1 - Packages/viewerbase/package.js | 22 +- Packages/viewerbase/server/lib/namespace.js | 3 +- .../server/methods/getStudyMetadata.js | 8 +- .../server/methods/worklistSearch.js | 23 +- .../server/services/dimse/instances.js | 70 ++ .../server/services/dimse/retrieveMetadata.js | 162 +++ .../server/services/dimse/studies.js | 48 + .../server/services/qido/instances.js | 14 +- .../server/services/qido/studies.js | 1 + .../server/services/wado/retrieveMetadata.js | 3 +- .../studyContextMenu/studyContextMenu.html | 21 + .../studyContextMenu/studyContextMenu.js | 20 +- .../studyContextMenu/studyContextMenu.styl | 7 +- .../components/tabContent/tabContent.html | 0 .../components/tabContent/tabContent.styl | 0 .../components/tabTitle/tabTitle.html | 0 .../client/components/tabTitle/tabTitle.js | 48 + .../components/tabTitle/tabTitle.styl | 0 .../{ => client}/components/worklist.html | 0 .../worklist/client/components/worklist.js | 59 + .../{ => client}/components/worklist.styl | 0 .../worklistResult/worklistResult.html | 54 + .../worklistResult/worklistResult.js | 50 +- .../worklistResult/worklistResult.styl | 10 +- .../worklistStudy/worklistStudy.html | 6 +- .../components/worklistStudy/worklistStudy.js | 43 +- .../worklistStudy/worklistStudy.styl | 2 + .../worklist/components/tabTitle/tabTitle.js | 87 -- Packages/worklist/components/worklist.js | 218 ---- .../worklistResult/worklistResult.html | 68 -- Packages/worklist/lib/generateUUID.js | 13 - Packages/worklist/lib/getStudiesMetadata.js | 62 + Packages/worklist/lib/getStudyMetadata.js | 45 + Packages/worklist/lib/openNewTab.js | 30 + Packages/worklist/lib/switchToTab.js | 90 ++ Packages/worklist/lib/worklist.js | 13 + Packages/worklist/package.js | 51 +- config/localhostOrthanc.json | 10 +- config/orthancDIMSE.json | 27 + 144 files changed, 5050 insertions(+), 2416 deletions(-) delete mode 100644 LesionTracker/client/mobileStyles.css delete mode 100644 LesionTracker/client/routeHooks.js delete mode 100644 LesionTracker/client/stylesheets/docking-container.css delete mode 100644 Packages/cornerstone/client/cornerstoneWADORSImageLoader.js delete mode 100644 Packages/dicomweb/lib/findIndexOfString.js delete mode 100644 Packages/dicomweb/lib/uint8ArrayToString.js delete mode 100644 Packages/dicomweb/server/getImageFrame.js delete mode 100644 Packages/dimse-service/etc/sampleOrthancConfig.json create mode 100644 Packages/dimseservice/etc/sampleOrthancConfig.json rename Packages/{dimse-service => dimseservice}/package.js (100%) rename Packages/{dimse-service => dimseservice}/server/Connection.js (100%) rename Packages/{dimse-service => dimseservice}/server/DIMSE.js (84%) rename Packages/{dimse-service => dimseservice}/server/Data.js (100%) rename Packages/{dimse-service => dimseservice}/server/Field.js (100%) rename Packages/{dimse-service => dimseservice}/server/Message.js (100%) rename Packages/{dimse-service => dimseservice}/server/PDU.js (100%) rename Packages/{dimse-service => dimseservice}/server/RWStream.js (100%) rename Packages/{dimse-service => dimseservice}/server/constants.js (100%) rename Packages/{dimse-service => dimseservice}/server/elements_data.js (100%) rename Packages/{dimse-service => dimseservice}/server/methods.js (100%) rename Packages/{dimse-service => dimseservice}/server/require.js (100%) create mode 100644 Packages/lesiontracker/client/collections/PatientLocations.js delete mode 100644 Packages/lesiontracker/client/compatibility/LesionManager.js create mode 100644 Packages/lesiontracker/client/compatibility/validate.js create mode 100644 Packages/lesiontracker/client/components/conformanceCheckFeedback/conformanceCheckFeedback.html create mode 100644 Packages/lesiontracker/client/components/conformanceCheckFeedback/conformanceCheckFeedback.js create mode 100644 Packages/lesiontracker/client/components/conformanceCheckFeedback/conformanceCheckFeedback.styl rename {LesionTracker => Packages/lesiontracker}/client/components/lesionTrackerLayout/lesionTrackerLayout.html (74%) rename {LesionTracker => Packages/lesiontracker}/client/components/lesionTrackerLayout/lesionTrackerLayout.styl (100%) create mode 100644 Packages/lesiontracker/client/components/lesionTrackerViewportOverlay/lesionTrackerViewportOverlay.html create mode 100644 Packages/lesiontracker/client/components/lesionTrackerViewportOverlay/lesionTrackerViewportOverlay.js create mode 100644 Packages/lesiontracker/client/components/lesionTrackerViewportOverlay/lesionTrackerViewportOverlay.styl rename Packages/{worklist/components/studyContextMenu/studyContextMenu.html => lesiontracker/client/components/lesionTrackerWorklistContextMenu/lesionTrackerWorklistContextMenu.html} (52%) create mode 100644 Packages/lesiontracker/client/components/lesionTrackerWorklistContextMenu/lesionTrackerWorklistContextMenu.js create mode 100644 Packages/lesiontracker/client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.html create mode 100644 Packages/lesiontracker/client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.js rename Packages/{worklist/components/worklistStudy/worklistStudy.styl => lesiontracker/client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.styl} (100%) create mode 100644 Packages/lesiontracker/client/components/optionsButton/optionsButton.html create mode 100644 Packages/lesiontracker/client/components/optionsButton/optionsButton.styl create mode 100644 Packages/lesiontracker/client/components/optionsModal/optionsModal.html create mode 100644 Packages/lesiontracker/client/components/optionsModal/optionsModal.js create mode 100644 Packages/lesiontracker/client/components/optionsModal/optionsModal.styl create mode 100644 Packages/lesiontracker/lib/LesionManager.js create mode 100644 Packages/lesiontracker/lib/TrialCriteriaConstraints.js create mode 100644 Packages/lesiontracker/lib/TrialResponseCriteria.js create mode 100644 Packages/lesiontracker/lib/calculateTotalLesionBurden.js create mode 100644 Packages/lesiontracker/lib/getTimepointName.js create mode 100644 Packages/lesiontracker/lib/handleMeasurementAdded.js create mode 100644 Packages/lesiontracker/lib/handleMeasurementModified.js create mode 100644 Packages/lesiontracker/lib/handleMeasurementRemoved.js create mode 100644 Packages/lesiontracker/lib/pixelSpacingAutorunCheck.js create mode 100644 Packages/lesiontracker/lib/syncMeasurementAndToolData.js create mode 100644 Packages/lesiontracker/lib/updateRelatedElements.js delete mode 100755 Packages/lesiontracker/lib/uuid.js create mode 100644 Packages/lesiontracker/lib/worklist/openNewTabWithTimepoint.js create mode 100644 Packages/lesiontracker/lib/worklist/worklistModification.js create mode 100644 Packages/viewerbase/lib/helpers/isTouchDevice.js delete mode 100644 Packages/viewerbase/lib/isTouchDevice.js delete mode 100644 Packages/viewerbase/lib/stringToUint8Array.js create mode 100755 Packages/viewerbase/server/services/dimse/instances.js create mode 100755 Packages/viewerbase/server/services/dimse/retrieveMetadata.js create mode 100755 Packages/viewerbase/server/services/dimse/studies.js create mode 100644 Packages/worklist/client/components/studyContextMenu/studyContextMenu.html rename Packages/worklist/{ => client}/components/studyContextMenu/studyContextMenu.js (81%) rename Packages/worklist/{ => client}/components/studyContextMenu/studyContextMenu.styl (88%) rename Packages/worklist/{ => client}/components/tabContent/tabContent.html (100%) rename Packages/worklist/{ => client}/components/tabContent/tabContent.styl (100%) rename Packages/worklist/{ => client}/components/tabTitle/tabTitle.html (100%) create mode 100644 Packages/worklist/client/components/tabTitle/tabTitle.js rename Packages/worklist/{ => client}/components/tabTitle/tabTitle.styl (100%) rename Packages/worklist/{ => client}/components/worklist.html (100%) create mode 100644 Packages/worklist/client/components/worklist.js rename Packages/worklist/{ => client}/components/worklist.styl (100%) create mode 100644 Packages/worklist/client/components/worklistResult/worklistResult.html rename Packages/worklist/{ => client}/components/worklistResult/worklistResult.js (64%) rename Packages/worklist/{ => client}/components/worklistResult/worklistResult.styl (73%) rename Packages/worklist/{ => client}/components/worklistStudy/worklistStudy.html (87%) rename Packages/worklist/{ => client}/components/worklistStudy/worklistStudy.js (84%) create mode 100644 Packages/worklist/client/components/worklistStudy/worklistStudy.styl delete mode 100644 Packages/worklist/components/tabTitle/tabTitle.js delete mode 100644 Packages/worklist/components/worklist.js delete mode 100644 Packages/worklist/components/worklistResult/worklistResult.html delete mode 100644 Packages/worklist/lib/generateUUID.js create mode 100644 Packages/worklist/lib/getStudiesMetadata.js create mode 100644 Packages/worklist/lib/getStudyMetadata.js create mode 100644 Packages/worklist/lib/openNewTab.js create mode 100644 Packages/worklist/lib/switchToTab.js create mode 100644 Packages/worklist/lib/worklist.js create mode 100644 config/orthancDIMSE.json diff --git a/.gitignore b/.gitignore index efc739489..ba6626150 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ docs/ .meteor/local .meteor/meteorite -node_modules \ No newline at end of file +node_modules +Packages/active-entry/helloworld/ +LesionTracker/tests/nightwatch/reports/ \ No newline at end of file diff --git a/.jscsrc b/.jscsrc index 716cd1cf6..b5c406d8e 100644 --- a/.jscsrc +++ b/.jscsrc @@ -7,6 +7,7 @@ "disallowSpacesInFunctionExpression": { "beforeOpeningRoundBrace": true }, + "disallowSpacesInsideParentheses": true, "disallowKeywordsOnNewLine": ["else"], "disallowNewlineBeforeBlockStatements": true, "requirePaddingNewLinesAfterUseStrict": true, @@ -17,9 +18,12 @@ "requireObjectKeysOnNewLine": true, "requireSemicolons": true, "requireSpaceAfterBinaryOperators": true, + "requireSpaceAfterComma": true, + "requireSpacesInFunctionExpression": { + "beforeOpeningCurlyBrace": true + }, "requireSpaceBeforeObjectValues": true, "requireSpacesInsideObjectBrackets": "all", - "requireSpacesInsideArrayBrackets": "all", "requireLineBreakAfterVariableAssignment": true, "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-"], diff --git a/LesionTracker/.meteor/packages b/LesionTracker/.meteor/packages index 58bfd688d..298479847 100644 --- a/LesionTracker/.meteor/packages +++ b/LesionTracker/.meteor/packages @@ -48,3 +48,4 @@ clinical:hipaa-audit-log clinical:hipaa-logger anti:gagarin@=0.4.11 check +aldeed:template-extension@4.0.0 diff --git a/LesionTracker/.meteor/versions b/LesionTracker/.meteor/versions index 3aae6e1e5..752136502 100644 --- a/LesionTracker/.meteor/versions +++ b/LesionTracker/.meteor/versions @@ -1,5 +1,6 @@ accounts-base@1.2.2 accounts-password@1.1.4 +aldeed:template-extension@4.0.0 anti:gagarin@0.4.11 anti:i18n@0.4.3 arsnebula:reactive-promise@0.9.1 @@ -38,6 +39,7 @@ ddp-server@1.2.2 deps@1.0.9 dicomweb@0.0.1 diff-sequence@1.0.1 +dimseservice@0.0.1 ecmascript@0.1.6 ecmascript-runtime@0.2.6 ejson@1.0.7 @@ -90,6 +92,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/LesionTracker/client/body.styl b/LesionTracker/client/body.styl index e5e6350dd..79a0ebbae 100644 --- a/LesionTracker/client/body.styl +++ b/LesionTracker/client/body.styl @@ -1,30 +1,62 @@ +// TODO: Move all of this into the LesionTracker package body background-color: #202020 -.tab-content { - height: calc(100% - 40px); -} +.tab-content + height: calc(100% - 40px) -table#tblStudyList thead > tr { - background-color: #424242; - color: white; -} -table#tblStudyList tbody > tr:nth-child(even) { - background-color: #888; - color: black; - } -table#tblStudyList tbody > tr:nth-child(odd) { - background-color: #BBB; - color: black; -} -table#tblStudyList tbody > tr:hover{ +table#tblStudyList thead > tr + background-color: #424242 + color: white + +table#tblStudyList tbody > tr:nth-child(even) + background-color: #888 + color: black + +table#tblStudyList tbody > tr:nth-child(odd) + background-color: #BBB + color: black + +table#tblStudyList tbody > tr:hover background-color: #009BD2 color: white -} -table#tblStudyList tbody > tr > td{ +table#tblStudyList tbody > tr > td word-wrap: break-word -} +/* 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 + 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 \ No newline at end of file diff --git a/LesionTracker/client/components/viewer.html b/LesionTracker/client/components/viewer.html index 80296c329..2d42d2548 100644 --- a/LesionTracker/client/components/viewer.html +++ b/LesionTracker/client/components/viewer.html @@ -5,6 +5,7 @@ {{>nonTargetLesionDialog}} {{>nonTargetResponseDialog}} {{>timepointTextDialog}} + {{ >conformanceCheckFeedback }} {{>hidingPanel}}
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 @@ + 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/lesionTableTimepointHeader/lesionTableTimepointHeader.js b/Packages/lesiontracker/client/components/lesionTableTimepointHeader/lesionTableTimepointHeader.js index 9b0ed0dd1..26c4891e2 100644 --- a/Packages/lesiontracker/client/components/lesionTableTimepointHeader/lesionTableTimepointHeader.js +++ b/Packages/lesiontracker/client/components/lesionTableTimepointHeader/lesionTableTimepointHeader.js @@ -1,105 +1,6 @@ -Template.lesionTableTimepointHeader.events({ - 'click th.lesionTableTimepointCell': function(e, template) { - var parentPosition = getPosition(e.currentTarget); - var cellText = e.currentTarget.innerText; - - // Remove spaces in string - cellText = cellText.replace(/\s/g, ''); // Remove spaces - cellText = cellText.replace('<', ''); // Remove < - var splitCellText = cellText.split('/'); - var dateStr = splitCellText[2].replace(/\D/g,'') + '' + splitCellText[0].replace(/\D/g,'') + '' + splitCellText[1].replace(/\D/g,''); // Remove non-digit chars - - // Check patient has a timepointText as Baseline - var patientId = template.data.patientId; - - // Open popup - var timepointTextDialog = $('#timepointTextDialog'); - var dialogDisplay = timepointTextDialog.css('display'); - if (dialogDisplay === 'none') { - var isBaselineInCollection = Timepoints.findOne({ - patientId: patientId, - timepointName: dateStr, - timepointText: 'Baseline' - }); - - // If isBaselineInCollection is true, "Baseline" is found in collection for patient and set checkbox as checked - if (isBaselineInCollection) { - // Set checkbox as checked - $('#checkBoxBaseline').prop('checked', true); - } else { - // Set checkbox as unchecked - $('#checkBoxBaseline').prop('checked', false); - } - - // Open dialog - var dialogProperty = { - top: parentPosition.y - 30, - left: parentPosition.x, - display: 'block' - }; - - timepointTextDialog.css(dialogProperty); - } else { - // Get timepoints of patient - // Set timepointText as Baseline for selected timepoint - var timepoints = Timepoints.find({ - patientId: patientId - }).fetch(); - - // Check checkbox is selected - var checkboxBaselineChecked = $('#checkBoxBaseline').is(':checked'); - - timepoints.forEach(function(timepoint) { - // timepointText defines a custom text for timepoint such as Baseline, Nadir, Current - if (timepoint.timepointName === dateStr) { - // If checkbox is selected, set timepointText as Baseline - // Else set timepointText as "" - var timepointText = 'Baseline'; - if (!checkboxBaselineChecked) { - timepointText = ''; - } - - Timepoints.update(timepoint._id,{ - $set: { - timepointText: timepointText - } - }); - } else { - // Set timepointText as empty - Timepoints.update(timepoint._id,{ - $set: { - timepointText: '' - } - }); - } - }); - - // Close dialog - timepointTextDialog.css('display', 'none'); - } - } -}); - Template.lesionTableTimepointHeader.helpers({ - timepointTextFound: function() { - var timepointText = this.timepointText; - return (timepointText && timepointText === 'Baseline'); + 'timepointName': function() { + var timepoint = this; + return getTimepointName(timepoint); } -}); - -// Gets parent's position of element which mouse pointer is clicked in -function getPosition(element) { - var xPosition = 0; - var yPosition = 0; - - while (element) { - xPosition += (element.offsetLeft - element.scrollLeft + element.clientLeft); - yPosition += (element.offsetTop - element.scrollTop + element.clientTop); - element = element.offsetParent; - } - - return { - x: xPosition, - y: yPosition - }; -} +}); \ No newline at end of file diff --git a/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.html b/Packages/lesiontracker/client/components/lesionTrackerLayout/lesionTrackerLayout.html similarity index 74% rename from LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.html rename to Packages/lesiontracker/client/components/lesionTrackerLayout/lesionTrackerLayout.html index a7116b58c..b03c6d454 100644 --- a/LesionTracker/client/components/lesionTrackerLayout/lesionTrackerLayout.html +++ b/Packages/lesiontracker/client/components/lesionTrackerLayout/lesionTrackerLayout.html @@ -1,9 +1,10 @@ -