From b624d127629e44fcf140747457e5d2bfae295826 Mon Sep 17 00:00:00 2001 From: Erik Ziegler Date: Sat, 13 Feb 2016 17:42:10 +0100 Subject: [PATCH] Fix syncing bug LT-177, update Cornerstone package libraries --- LesionTracker/client/components/viewer.js | 32 +- Packages/cornerstone/client/cornerstone.js | 131 +- .../cornerstone/client/cornerstoneMath.js | 40 +- .../cornerstone/client/cornerstoneTools.js | 1249 +++++++++++------ .../client/cornerstoneWADOImageLoader.js | 4 +- Packages/cornerstone/client/hammer.js | 141 +- .../client/compatibility/lesionTool.js | 183 ++- .../client/compatibility/nonTargetTool.js | 16 +- .../components/lesionTable/lesionTable.js | 2 +- .../nonTargetLesionDialog.js | 3 +- .../nonTargetResponseDialog.js | 3 +- Packages/lesiontracker/lib/LesionManager.js | 30 +- .../lib/handleMeasurementAdded.js | 1 - .../lib/handleMeasurementModified.js | 1 - .../lib/removeToolDataWithMeasurementId.js | 4 +- .../lib/syncMeasurementAndToolData.js | 77 +- Packages/viewerbase/client/collections.js | 4 + .../imageViewerViewport.js | 4 +- Packages/viewerbase/lib/WLPresets.js | 2 +- Packages/viewerbase/lib/getWADORSImageId.js | 2 +- Packages/viewerbase/package.js | 10 +- .../server/services/dimse/retrieveMetadata.js | 2 +- .../server/services/dimse/studies.js | 2 +- .../worklistResult/worklistResult.js | 5 - 24 files changed, 1240 insertions(+), 708 deletions(-) create mode 100644 Packages/viewerbase/client/collections.js diff --git a/LesionTracker/client/components/viewer.js b/LesionTracker/client/components/viewer.js index 84525f608..712a3188a 100644 --- a/LesionTracker/client/components/viewer.js +++ b/LesionTracker/client/components/viewer.js @@ -1,8 +1,5 @@ Session.setDefault('activeViewport', false); -ViewerStudies = new Meteor.Collection(null); -ViewerStudies._debugName = 'ViewerStudies'; - Template.viewer.onCreated(function() { // Attach the Window resize listener $(window).on('resize', handleResize); @@ -73,8 +70,6 @@ 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; @@ -122,7 +117,7 @@ Template.viewer.onCreated(function() { self.subscribe('singlePatientMeasurements', dataContext.studies[0].patientId); var subscriptionsReady = self.subscriptionsReady(); - console.log('autorun viewer.js. Ready: ' + subscriptionsReady); + log.info('autorun viewer.js. Ready: ' + subscriptionsReady); if (subscriptionsReady) { TrialResponseCriteria.validateAllDelayed(); @@ -150,16 +145,19 @@ Template.viewer.onCreated(function() { } }); - // This is used to re-add tools from the database into the - // Cornerstone ToolData structure - var syncTimeout, - syncDelay = 50; Measurements.find().observe({ added: function(data) { - if (data.toolDataInsertedManually === true) { + if (data.clientId === ClientId) { + TrialResponseCriteria.validateAllDelayed(); return; } + log.info('Measurement added'); + + // This is used to re-add tools from the database into the + // Cornerstone ToolData structure + syncMeasurementAndToolData(data); + // Activate first measurements in image box as default if exists if (!firstMeasurementsActivated) { var templateData = { @@ -171,9 +169,6 @@ Template.viewer.onCreated(function() { firstMeasurementsActivated = true; } - log.info('Measurement added'); - syncMeasurementAndToolData(data); - // Update each displayed viewport var viewports = $('.imageViewerViewport').not('.empty'); viewports.each(function(index, element) { @@ -181,11 +176,15 @@ Template.viewer.onCreated(function() { }); }, changed: function(data) { - if (OHIF.viewer.manuallyModifyingMeasurement === true) { + if (data.clientId === ClientId) { + TrialResponseCriteria.validateAllDelayed(); return; } log.info('Measurement changed'); + + // This is used to update changed tools from the database + // in the Cornerstone ToolData structure syncMeasurementAndToolData(data); // Update each displayed viewport @@ -258,9 +257,6 @@ Template.viewer.onRendered(function() { }); Template.viewer.onDestroyed(function() { - log.info('onDestroyed'); - console.log('viewer destroyed!'); - // Remove the Window resize listener $(window).off('resize', handleResize); diff --git a/Packages/cornerstone/client/cornerstone.js b/Packages/cornerstone/client/cornerstone.js index f2cca6c2d..cd8943abc 100644 --- a/Packages/cornerstone/client/cornerstone.js +++ b/Packages/cornerstone/client/cornerstone.js @@ -1,4 +1,4 @@ -/*! cornerstone - v0.8.4 - 2015-10-09 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstone */ +/*! cornerstone - v0.9.0 - 2016-02-04 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstone */ if(typeof cornerstone === 'undefined'){ cornerstone = { internal : {}, @@ -482,8 +482,11 @@ if(typeof cornerstone === 'undefined'){ "use strict"; + // dictionary of imageId to cachedImage objects var imageCache = {}; - + // dictionary of sharedCacheKeys to number of imageId's in cache with this shared cache key + var sharedCacheKeys = {}; + // array of cachedImage objects var cachedImages = []; var maximumSizeInBytes = 1024 * 1024 * 1024; // 1 GB @@ -549,6 +552,7 @@ if(typeof cornerstone === 'undefined'){ var cachedImage = { loaded : false, imageId : imageId, + sharedCacheKey: undefined, // the sharedCacheKey for this imageId. undefined by default imagePromise : imagePromise, timeStamp : new Date(), sizeInBytes: 0 @@ -566,8 +570,23 @@ if(typeof cornerstone === 'undefined'){ if (image.sizeInBytes.toFixed === undefined) { throw "putImagePromise: image.sizeInBytes is not a number"; } - cachedImage.sizeInBytes = image.sizeInBytes; - cacheSizeInBytes += cachedImage.sizeInBytes; + + // If this image has a shared cache key, reference count it and only + // count the image size for the first one added with this sharedCacheKey + if(image.sharedCacheKey) { + cachedImage.sizeInBytes = image.sizeInBytes; + cachedImage.sharedCacheKey = image.sharedCacheKey; + if(sharedCacheKeys[image.sharedCacheKey]) { + sharedCacheKeys[image.sharedCacheKey]++; + } else { + sharedCacheKeys[image.sharedCacheKey] = 1; + cacheSizeInBytes += cachedImage.sizeInBytes; + } + } + else { + cachedImage.sizeInBytes = image.sizeInBytes; + cacheSizeInBytes += cachedImage.sizeInBytes; + } purgeCacheIfNecessary(); }); } @@ -595,9 +614,23 @@ if(typeof cornerstone === 'undefined'){ throw "removeImagePromise: imageId must not be undefined"; } cachedImages.splice( cachedImages.indexOf(cachedImage), 1); - cacheSizeInBytes -= cachedImage.sizeInBytes; + + // If this is using a sharedCacheKey, decrement the cache size only + // if it is the last imageId in the cache with this sharedCacheKey + if(cachedImages.sharedCacheKey) { + if(sharedCacheKeys[cachedImages.sharedCacheKey] === 1) { + cacheSizeInBytes -= cachedImage.sizeInBytes; + delete sharedCacheKeys[cachedImages.sharedCacheKey]; + } else { + sharedCacheKeys[cachedImages.sharedCacheKey]--; + } + } else { + cacheSizeInBytes -= cachedImage.sizeInBytes; + } delete imageCache[imageId]; + decache(cachedImage.imagePromise, cachedImage.imageId); + return cachedImage.imagePromise; } @@ -609,15 +642,37 @@ if(typeof cornerstone === 'undefined'){ }; } + function decache(imagePromise, imageId) { + imagePromise.then(function(image) { + if(image.decache) { + image.decache(); + } + imagePromise.reject(); + delete imageCache[imageId]; + }).always(function() { + delete imageCache[imageId]; + }); + } + function purgeCache() { while (cachedImages.length > 0) { - var removedCachedImage = cachedImages.pop(); - delete imageCache[removedCachedImage.imageId]; - removedCachedImage.imagePromise.reject(); + var removedCachedImage = cachedImages.pop(); + decache(removedCachedImage.imagePromise, removedCachedImage.imageId); } cacheSizeInBytes = 0; } + function changeImageIdCacheSize(imageId, newCacheSize) { + var cacheEntry = imageCache[imageId]; + if(cacheEntry) { + cacheEntry.imagePromise.then(function(image) { + var cacheSizeDifference = newCacheSize - image.sizeInBytes; + image.sizeInBytes = newCacheSize; + cacheSizeInBytes += cacheSizeDifference; + }); + } + } + // module exports cornerstone.imageCache = { putImagePromise : putImagePromise, @@ -626,7 +681,8 @@ if(typeof cornerstone === 'undefined'){ setMaximumSizeBytes: setMaximumSizeBytes, getCacheInfo : getCacheInfo, purgeCache: purgeCache, - cachedImages: cachedImages + cachedImages: cachedImages, + changeImageIdCacheSize: changeImageIdCacheSize }; }(cornerstone)); @@ -1522,38 +1578,39 @@ if(typeof cornerstone === 'undefined'){ function getRenderCanvas(enabledElement, image, invalidated) { - // apply the lut to the stored pixel data onto the render canvas - if(enabledElement.viewport.voi.windowWidth === enabledElement.image.windowWidth && - enabledElement.viewport.voi.windowCenter === enabledElement.image.windowCenter && - enabledElement.viewport.invert === false) + // The ww/wc is identity and not inverted - get a canvas with the image rendered into it for + // fast drawing + if(enabledElement.viewport.voi.windowWidth === 255 && + enabledElement.viewport.voi.windowCenter === 128 && + enabledElement.viewport.invert === false && + image.getCanvas && + image.getCanvas() + ) { - // the color image voi/invert has not been modified, request the canvas that contains - // it so we can draw it directly to the display canvas return image.getCanvas(); } - else - { - if(doesImageNeedToBeRendered(enabledElement, image) === false && invalidated !== true) { - return colorRenderCanvas; - } - // If our render canvas does not match the size of this image reset it - // NOTE: This might be inefficient if we are updating multiple images of different - // sizes frequently. - if(colorRenderCanvas.width !== image.width || colorRenderCanvas.height != image.height) { - initializeColorRenderCanvas(image); - } - - // get the lut to use - var colorLut = getLut(image, enabledElement.viewport); - - // the color image voi/invert has been modified - apply the lut to the underlying - // pixel data and put it into the renderCanvas - cornerstone.storedColorPixelDataToCanvasImageData(image, colorLut, colorRenderCanvasData.data); - colorRenderCanvasContext.putImageData(colorRenderCanvasData, 0, 0); + // apply the lut to the stored pixel data onto the render canvas + if(doesImageNeedToBeRendered(enabledElement, image) === false && invalidated !== true) { return colorRenderCanvas; } + + // If our render canvas does not match the size of this image reset it + // NOTE: This might be inefficient if we are updating multiple images of different + // sizes frequently. + if(colorRenderCanvas.width !== image.width || colorRenderCanvas.height != image.height) { + initializeColorRenderCanvas(image); + } + + // get the lut to use + var colorLut = getLut(image, enabledElement.viewport); + + // the color image voi/invert has been modified - apply the lut to the underlying + // pixel data and put it into the renderCanvas + cornerstone.storedColorPixelDataToCanvasImageData(image, colorLut, colorRenderCanvasData.data); + colorRenderCanvasContext.putImageData(colorRenderCanvasData, 0, 0); + return colorRenderCanvas; } /** @@ -2148,7 +2205,7 @@ if(typeof cornerstone === 'undefined'){ function initShaders() { for (var id in cornerstone.webGL.shaders) { - //console.log("WEBGL: Loading shader", id); + console.log("WEBGL: Loading shader", id); var shader = cornerstone.webGL.shaders[ id ]; shader.attributes = {}; shader.uniforms = {}; @@ -2168,13 +2225,13 @@ if(typeof cornerstone === 'undefined'){ function initRenderer() { if (cornerstone.webGL.isWebGLInitialized === true) { - //console.log("WEBGL Renderer already initialized"); + console.log("WEBGL Renderer already initialized"); return; } if ( initWebGL( renderCanvas ) ) { initBuffers(); initShaders(); - //console.log("WEBGL Renderer initialized!"); + console.log("WEBGL Renderer initialized!"); cornerstone.webGL.isWebGLInitialized = true; } } diff --git a/Packages/cornerstone/client/cornerstoneMath.js b/Packages/cornerstone/client/cornerstoneMath.js index 71b32da5b..58120cdc6 100644 --- a/Packages/cornerstone/client/cornerstoneMath.js +++ b/Packages/cornerstone/client/cornerstoneMath.js @@ -1,4 +1,4 @@ -/*! cornerstoneMath - v0.1.2 - 2015-08-31 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneMath */ +/*! cornerstoneMath - v0.1.3 - 2016-02-04 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneMath */ // Begin Source: src/vector3.js // Based on THREE.JS @@ -1651,6 +1651,31 @@ var cornerstoneMath = (function (cornerstoneMath) { return true; } + /** + * Returns the closest source point to a target point + * given an array of source points. + * + * @param sources An Array of source Points + * @param target The target Point + * @returns Point The closest point from the points array + */ + function findClosestPoint(sources, target) { + var distances = []; + var minDistance; + sources.forEach(function(source, index) { + var distance = cornerstoneMath.point.distance(source, target); + distances.push(distance); + + if (index === 0) { + minDistance = distance; + } else { + minDistance = Math.min(distance, minDistance); + } + }); + + var index = distances.indexOf(minDistance); + return sources[index]; + } // module exports cornerstoneMath.point = @@ -1660,7 +1685,8 @@ var cornerstoneMath = (function (cornerstoneMath) { pageToPoint: pageToPoint, distance: distance, distanceSquared: distanceSquared, - insideRect: insideRect + insideRect: insideRect, + findClosestPoint: findClosestPoint }; @@ -1836,7 +1862,6 @@ var cornerstoneMath = (function (cornerstoneMath) { return (distance < maxDistance); } - function distanceToPoint(rect, point) { var minDistance = 655535; @@ -1850,7 +1875,7 @@ var cornerstoneMath = (function (cornerstoneMath) { return minDistance; } - // Returns top-left and bottom-right of rectangle + // Returns top-left and bottom-right points of the rectangle function rectToPoints (rect) { var rectPoints = { topLeft: { @@ -1959,11 +1984,12 @@ var cornerstoneMath = (function (cornerstoneMath) { // module exports cornerstoneMath.rect = { - rectToLineSegments : distanceToPoint, + distanceToPoint : distanceToPoint, getIntersectionRect : getIntersectionRect + }; return cornerstoneMath; -}(cornerstoneMath)); -// End Source; src/rect.js \ No newline at end of file +}(cornerstoneMath)); +// End Source; src/rect.js diff --git a/Packages/cornerstone/client/cornerstoneTools.js b/Packages/cornerstone/client/cornerstoneTools.js index 9cefd8e12..3dafa86ff 100644 --- a/Packages/cornerstone/client/cornerstoneTools.js +++ b/Packages/cornerstone/client/cornerstoneTools.js @@ -1,4 +1,4 @@ -/*! cornerstoneTools - v0.7.7 - 2016-02-02 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneTools */ +/*! cornerstoneTools - v0.7.8 - 2016-02-13 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneTools */ // Begin Source: src/header.js if (typeof cornerstone === 'undefined') { cornerstone = {}; @@ -380,7 +380,7 @@ if (typeof cornerstoneTools === 'undefined') { preventNextPinch = false; function onTouch(e) { - ///console.log(e.type); + console.log(e.type); var element = e.target.parentNode, event, eventType; @@ -985,7 +985,6 @@ if (typeof cornerstoneTools === 'undefined') { var element = eventData.element; function handleDoneMove() { - data.active = false; data.invalidated = true; if (cornerstoneTools.anyHandlesOutsideImage(eventData, data.handles)) { // delete the measurement @@ -996,53 +995,60 @@ if (typeof cornerstoneTools === 'undefined') { $(element).on('CornerstoneToolsMouseMove', eventData, mouseToolInterface.mouseMoveCallback || mouseMoveCallback); } - if (cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) { - var coords = eventData.startPoints.canvas; - var toolData = cornerstoneTools.getToolState(e.currentTarget, mouseToolInterface.toolType); + if (!cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) { + return; + } - var i; + var coords = eventData.startPoints.canvas; + var toolData = cornerstoneTools.getToolState(e.currentTarget, mouseToolInterface.toolType); + if (!toolData) { + return; + } - // now check to see if there is a handle we can move - if (toolData) { + var i; - var preventHandleOutsideImage; - if (mouseToolInterface.options && mouseToolInterface.options.preventHandleOutsideImage !== undefined) { - preventHandleOutsideImage = mouseToolInterface.options.preventHandleOutsideImage; - } else { - preventHandleOutsideImage = false; - } + // now check to see if there is a handle we can move + + var preventHandleOutsideImage; + if (mouseToolInterface.options && mouseToolInterface.options.preventHandleOutsideImage !== undefined) { + preventHandleOutsideImage = mouseToolInterface.options.preventHandleOutsideImage; + } else { + preventHandleOutsideImage = false; + } - for (i = 0; i < toolData.data.length; i++) { - data = toolData.data[i]; - var distanceSq = 25; - var handle = cornerstoneTools.getHandleNearImagePoint(element, data.handles, coords, distanceSq); - if (handle) { - $(element).off('CornerstoneToolsMouseMove', mouseToolInterface.mouseMoveCallback || mouseMoveCallback); - data.active = true; - cornerstoneTools.moveHandle(eventData, mouseToolInterface.toolType, data, handle, handleDoneMove, preventHandleOutsideImage); - e.stopImmediatePropagation(); - return false; - } - } + for (i = 0; i < toolData.data.length; i++) { + data = toolData.data[i]; + var distance = 6; + var handle = cornerstoneTools.getHandleNearImagePoint(element, data.handles, coords, distance); + if (handle) { + $(element).off('CornerstoneToolsMouseMove', mouseToolInterface.mouseMoveCallback || mouseMoveCallback); + data.active = true; + cornerstoneTools.moveHandle(eventData, mouseToolInterface.toolType, data, handle, handleDoneMove, preventHandleOutsideImage); + e.stopImmediatePropagation(); + return false; } + } - // Now check to see if there is a line we can move - // now check to see if we have a tool that we can move - if (toolData && mouseToolInterface.pointNearTool) { - var options = mouseToolInterface.options || { - deleteIfHandleOutsideImage: true, - preventHandleOutsideImage: false - }; + // Now check to see if there is a line we can move + // now check to see if we have a tool that we can move + if (!mouseToolInterface.pointNearTool) { + return; + } - for (i = 0; i < toolData.data.length; i++) { - data = toolData.data[i]; - if (mouseToolInterface.pointNearTool(element, data, coords)) { - $(element).off('CornerstoneToolsMouseMove', mouseToolInterface.mouseMoveCallback || mouseMoveCallback); - cornerstoneTools.moveAllHandles(e, data, toolData, mouseToolInterface.toolType, options, handleDoneMove); - e.stopImmediatePropagation(); - return false; - } - } + var options = mouseToolInterface.options || { + deleteIfHandleOutsideImage: true, + preventHandleOutsideImage: false + }; + + for (i = 0; i < toolData.data.length; i++) { + data = toolData.data[i]; + data.active = false; + if (mouseToolInterface.pointNearTool(element, data, coords)) { + data.active = true; + $(element).off('CornerstoneToolsMouseMove', mouseToolInterface.mouseMoveCallback || mouseMoveCallback); + cornerstoneTools.moveAllHandles(e, data, toolData, mouseToolInterface.toolType, options, handleDoneMove); + e.stopImmediatePropagation(); + return false; } } } @@ -1592,7 +1598,7 @@ if (typeof cornerstoneTools === 'undefined') { deactivateAllToolInstances(toolData); function doneMovingCallback() { - console.log('touchTool tapCallback doneMovingCallback'); + //console.log('touchTool tapCallback doneMovingCallback'); deactivateAllToolInstances(toolData); if (cornerstoneTools.anyHandlesOutsideImage(eventData, data.handles)) { // delete the measurement @@ -1658,7 +1664,7 @@ if (typeof cornerstoneTools === 'undefined') { var toolData = cornerstoneTools.getToolState(e.currentTarget, touchToolInterface.toolType); var i; - function doneMovingCallback() { + function doneMovingCallback(lastEvent, lastEventData) { //console.log('touchTool touchStartCallback doneMovingCallback'); data.active = false; data.invalidated = true; @@ -1670,38 +1676,62 @@ if (typeof cornerstoneTools === 'undefined') { cornerstone.updateImage(eventData.element); $(element).on('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); $(element).on('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); + + if (touchToolInterface.pressCallback) { + $(element).on('CornerstoneToolsTouchPress', touchToolInterface.pressCallback); + } + + if (lastEvent && lastEvent.type === 'CornerstoneToolsTouchPress') { + var event = $.Event(lastEvent.type, lastEventData); + $(element).trigger(event, lastEventData); + } } // now check to see if there is a handle we can move - var distanceFromTouch = cornerstoneTools.touchSettings.getToolDistanceFromTouch(); - var distanceSq = Math.max(Math.abs(distanceFromTouch.x), Math.abs(distanceFromTouch.y)); - if (toolData) { - for (i = 0; i < toolData.data.length; i++) { - data = toolData.data[i]; + + // Average pixel width of index finger is 45-57 pixels + // https://www.smashingmagazine.com/2012/02/finger-friendly-design-ideal-mobile-touchscreen-target-sizes/ + var distance = 28; + if (!toolData) { + return; + } - var handle = cornerstoneTools.getHandleNearImagePoint(eventData.element, data.handles, coords, distanceSq); - if (handle) { - $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); - $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); - data.active = true; - cornerstoneTools.touchMoveHandle(e, touchToolInterface.toolType, data, handle, doneMovingCallback); - e.stopImmediatePropagation(); - return false; // false = causes jquery to preventDefault() and stopPropagation() this event + for (i = 0; i < toolData.data.length; i++) { + data = toolData.data[i]; + + var handle = cornerstoneTools.getHandleNearImagePoint(eventData.element, data.handles, coords, distance); + if (handle) { + $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); + $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); + if (touchToolInterface.pressCallback) { + $(element).off('CornerstoneToolsTouchPress', touchToolInterface.pressCallback); } + + data.active = true; + cornerstoneTools.touchMoveHandle(e, touchToolInterface.toolType, data, handle, doneMovingCallback); + e.stopImmediatePropagation(); + return false; // false = causes jquery to preventDefault() and stopPropagation() this event } } // Now check to see if we have a tool that we can move - if (toolData && touchToolInterface.pointNearTool) { - for (i = 0; i < toolData.data.length; i++) { - data = toolData.data[i]; - if (touchToolInterface.pointNearTool(eventData.element, data, coords)) { - $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); - $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); - cornerstoneTools.touchMoveAllHandles(e, data, toolData, touchToolInterface.toolType, true, doneMovingCallback); - e.stopImmediatePropagation(); - return false; // false = causes jquery to preventDefault() and stopPropagation() this event + if (!touchToolInterface.pointNearTool) { + return; + } + + for (i = 0; i < toolData.data.length; i++) { + data = toolData.data[i]; + + if (touchToolInterface.pointNearTool(eventData.element, data, coords)) { + $(element).off('CornerstoneToolsTouchStartActive', touchToolInterface.touchDownActivateCallback || touchDownActivateCallback); + $(element).off('CornerstoneToolsTap', touchToolInterface.tapCallback || tapCallback); + if (touchToolInterface.pressCallback) { + $(element).off('CornerstoneToolsTouchPress', touchToolInterface.pressCallback); } + + cornerstoneTools.touchMoveAllHandles(e, data, toolData, touchToolInterface.toolType, true, doneMovingCallback); + e.stopImmediatePropagation(); + return false; // false = causes jquery to preventDefault() and stopPropagation() this event } } } @@ -1786,12 +1816,10 @@ if (typeof cornerstoneTools === 'undefined') { if (touchToolInterface.doubleTapCallback) { $(element).off('CornerstoneToolsDoubleTap', touchToolInterface.doubleTapCallback); - $(element).on('CornerstoneToolsDoubleTap', touchToolInterface.doubleTapCallback); } if (touchToolInterface.pressCallback) { $(element).off('CornerstoneToolsTouchPress', touchToolInterface.pressCallback); - $(element).on('CornerstoneToolsTouchPress', touchToolInterface.pressCallback); } cornerstone.updateImage(element); @@ -2015,7 +2043,7 @@ if (typeof cornerstoneTools === 'undefined') { doneChangingTextCallback(prompt('Enter your annotation:')); } - function changeTextCallback(data, doneChangingTextCallback) { + function changeTextCallback(data, eventData, doneChangingTextCallback) { doneChangingTextCallback(prompt('Change your annotation:')); } @@ -2031,7 +2059,12 @@ if (typeof cornerstoneTools === 'undefined') { ///////// BEGIN ACTIVE TOOL /////// function addNewMeasurement(mouseEventData) { + var measurementData = createNewMeasurement(mouseEventData); + var eventData = { + mouseButtonMask: mouseEventData.which, + }; + function doneChangingTextCallback(text) { if (text !== null) { measurementData.text = text; @@ -2041,13 +2074,11 @@ if (typeof cornerstoneTools === 'undefined') { measurementData.active = false; cornerstone.updateImage(mouseEventData.element); + + $(mouseEventData.element).on('CornerstoneToolsMouseMove', eventData, cornerstoneTools.arrowAnnotate.mouseMoveCallback); + $(mouseEventData.element).on('CornerstoneToolsMouseDown', eventData, cornerstoneTools.arrowAnnotate.mouseDownCallback); + $(mouseEventData.element).on('CornerstoneToolsMouseDownActivate', eventData, cornerstoneTools.arrowAnnotate.mouseDownActivateCallback); } - - var measurementData = createNewMeasurement(mouseEventData); - - var eventData = { - mouseButtonMask: mouseEventData.which, - }; // associate this data with this imageId so we can render it and manipulate it cornerstoneTools.addToolState(mouseEventData.element, toolType, measurementData); @@ -2070,9 +2101,6 @@ if (typeof cornerstoneTools === 'undefined') { config.getTextCallback(doneChangingTextCallback); } - $(mouseEventData.element).on('CornerstoneToolsMouseMove', eventData, cornerstoneTools.arrowAnnotate.mouseMoveCallback); - $(mouseEventData.element).on('CornerstoneToolsMouseDown', eventData, cornerstoneTools.arrowAnnotate.mouseDownCallback); - $(mouseEventData.element).on('CornerstoneToolsMouseDownActivate', eventData, cornerstoneTools.arrowAnnotate.mouseDownActivateCallback); cornerstone.updateImage(mouseEventData.element); }); } @@ -2094,6 +2122,14 @@ if (typeof cornerstoneTools === 'undefined') { y: mouseEventData.currentPoints.image.y, highlight: true, active: false + }, + textBox: { + active: false, + hasMoved: false, + movesIndependently: false, + drawnIndependently: true, + allowedOutsideImage: true, + hasBoundingBox: true } } }; @@ -2109,34 +2145,19 @@ if (typeof cornerstoneTools === 'undefined') { }; var distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords); - if (distanceToPoint < 25) { - return true; - } - - if (data.textCoords) { - var padding = 5; - var fontSize = cornerstoneTools.textStyle.getFontSize(); - - var rect = { - left: data.textCoords.x, - top: data.textCoords.y, - width: data.textWidth + (padding * 2), - height: fontSize + (padding * 2) - }; - - var distanceToTextRect = cornerstoneMath.rect.distanceToPoint(rect, coords); - return (distanceToTextRect < 25); - } + return (distanceToPoint < 25); } ///////// BEGIN IMAGE RENDERING /////// function onImageRendered(e, eventData) { // if we have no toolData for this element, return immediately as there is nothing to do var toolData = cornerstoneTools.getToolState(e.currentTarget, toolType); - if (toolData === undefined) { + if (!toolData) { return; } + var enabledElement = eventData.enabledElement; + // we have tool data for this element - iterate over each one and draw it var context = eventData.canvasContext.canvas.getContext('2d'); context.setTransform(1, 0, 0, 1, 0, 0); @@ -2167,67 +2188,111 @@ if (typeof cornerstoneTools === 'undefined') { var handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.start); var handleEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.end); + //config.arrowFirst = false; if (config.arrowFirst) { cornerstoneTools.drawArrow(context, handleEndCanvas, handleStartCanvas, color, lineWidth); } else { cornerstoneTools.drawArrow(context, handleStartCanvas, handleEndCanvas, color, lineWidth); } + var handleOptions = { + drawHandlesIfActive: (config && config.drawHandlesOnHover) + }; + if (config.drawHandles) { - cornerstoneTools.drawHandles(context, eventData, data.handles, color); - } else if (config.drawHandlesOnHover && data.handles.start.active) { - cornerstoneTools.drawHandles(context, eventData, [ data.handles.start ], color); - } else if (config.drawHandlesOnHover && data.handles.end.active) { - cornerstoneTools.drawHandles(context, eventData, [ data.handles.end ], color); + cornerstoneTools.drawHandles(context, eventData, data.handles, color, handleOptions); } // Draw the text if (data.text && data.text !== '') { context.font = font; - - var distance = 13; - // TODO: add 2 dimensional vector operations to cornerstoneMath - var vector; - - var displacement = { - x: distance, - y: distance / 2 - }; + // Calculate the text coordinates. + var textWidth = context.measureText(data.text).width + 10; + var textHeight = cornerstoneTools.textStyle.getFontSize() + 10; - vector = { - x: handleEndCanvas.x - handleStartCanvas.x, - y: handleEndCanvas.y - handleStartCanvas.y - }; - - var textWidth = context.measureText(data.text).width; - - var textCoords; - if (config.arrowFirst) { - // Fix text placement if arrow faces right - if (vector.x < 0) { - displacement.x = -displacement.x - textWidth; - } - - textCoords = { - x: vector.x + handleStartCanvas.x + displacement.x, - y: vector.y + handleStartCanvas.y + displacement.y - }; - } else { - // Fix text placement if arrow faces right - if (vector.x > 0) { - displacement.x = -displacement.x - textWidth; - } - - textCoords = { - x: -vector.x + handleEndCanvas.x + displacement.x, - y: -vector.y + handleEndCanvas.y + displacement.y - }; + var distance = Math.max(textWidth, textHeight) / 2 + 5; + if (handleEndCanvas.x < handleStartCanvas.x) { + distance = -distance; } - data.textCoords = textCoords; - data.textWidth = textWidth; - cornerstoneTools.drawTextBox(context, data.text, textCoords.x, textCoords.y, color); + var textCoords; + if (!data.handles.textBox.hasMoved) { + if (config.arrowFirst) { + textCoords = { + x: handleEndCanvas.x - textWidth / 2 + distance, + y: handleEndCanvas.y - textHeight / 2 + }; + } else { + // If the arrow is at the End position, the text should + // be placed near the Start position + textCoords = { + x: handleStartCanvas.x - textWidth / 2 - distance, + y: handleStartCanvas.y - textHeight / 2 + }; + } + + var transform = cornerstone.internal.getTransform(enabledElement); + transform.invert(); + + var coords = transform.transformPoint(textCoords.x, textCoords.y); + data.handles.textBox.x = coords.x; + data.handles.textBox.y = coords.y; + } + + textCoords = cornerstone.pixelToCanvas(eventData.element, data.handles.textBox); + + var boundingBox = cornerstoneTools.drawTextBox(context, data.text, textCoords.x, textCoords.y, color); + data.handles.textBox.boundingBox = boundingBox; + + if (data.handles.textBox.hasMoved) { + // Draw dashed link line between tool and text + var link = { + start: {}, + end: {} + }; + + var midpointCanvas = { + x: (handleStartCanvas.x + handleEndCanvas.x) / 2, + y: (handleStartCanvas.y + handleEndCanvas.y) / 2, + }; + + var points = [ handleStartCanvas, handleEndCanvas, midpointCanvas ]; + + link.end.x = textCoords.x; + link.end.y = textCoords.y; + + link.start = cornerstoneMath.point.findClosestPoint(points, link.end); + + var boundingBoxPoints = [ { + // Top middle point of bounding box + x: boundingBox.left + boundingBox.width / 2, + y: boundingBox.top + }, { + // Left middle point of bounding box + x: boundingBox.left, + y: boundingBox.top + boundingBox.height / 2 + }, { + // Bottom middle point of bounding box + x: boundingBox.left + boundingBox.width / 2, + y: boundingBox.top + boundingBox.height + }, { + // Right middle point of bounding box + x: boundingBox.left + boundingBox.width, + y: boundingBox.top + boundingBox.height / 2 + }, + ]; + + link.end = cornerstoneMath.point.findClosestPoint(boundingBoxPoints, link.start); + + context.beginPath(); + context.strokeStyle = color; + context.lineWidth = lineWidth; + context.setLineDash([ 2, 3 ]); + context.moveTo(link.start.x, link.start.y); + context.lineTo(link.end.x, link.end.y); + context.stroke(); + } } context.restore(); @@ -2238,6 +2303,7 @@ if (typeof cornerstoneTools === 'undefined') { ///////// BEGIN ACTIVE TOOL /////// function addNewMeasurementTouch(touchEventData) { var element = touchEventData.element; + var measurementData = createNewMeasurement(touchEventData); function doneChangingTextCallback(text) { if (text !== null) { @@ -2248,10 +2314,14 @@ if (typeof cornerstoneTools === 'undefined') { measurementData.active = false; cornerstone.updateImage(element); + + $(element).on('CornerstoneToolsTouchPress', cornerstoneTools.arrowAnnotateTouch.pressCallback); + $(element).on('CornerstoneToolsTouchStartActive', cornerstoneTools.arrowAnnotateTouch.touchDownActivateCallback); + $(element).on('CornerstoneToolsTap', cornerstoneTools.arrowAnnotateTouch.tapCallback); } - var measurementData = createNewMeasurement(touchEventData); cornerstoneTools.addToolState(element, toolType, measurementData); + $(element).off('CornerstoneToolsTouchPress', cornerstoneTools.arrowAnnotateTouch.pressCallback); $(element).off('CornerstoneToolsTouchStartActive', cornerstoneTools.arrowAnnotateTouch.touchDownActivateCallback); $(element).off('CornerstoneToolsTap', cornerstoneTools.arrowAnnotateTouch.tapCallback); cornerstone.updateImage(element); @@ -2268,9 +2338,6 @@ if (typeof cornerstoneTools === 'undefined') { if (measurementData.text === undefined) { config.getTextCallback(doneChangingTextCallback); } - - $(element).on('CornerstoneToolsTouchStartActive', cornerstoneTools.arrowAnnotateTouch.touchDownActivateCallback); - $(element).on('CornerstoneToolsTap', cornerstoneTools.arrowAnnotateTouch.tapCallback); }); } @@ -2305,11 +2372,12 @@ if (typeof cornerstoneTools === 'undefined') { for (var i = 0; i < toolData.data.length; i++) { data = toolData.data[i]; - if (pointNearTool(element, data, coords)) { + if (pointNearTool(element, data, coords) || + cornerstoneTools.pointInsideBoundingBox(data.handles.textBox, coords)) { data.active = true; cornerstone.updateImage(element); // Allow relabelling via a callback - config.changeTextCallback(data, doneChangingTextCallback); + config.changeTextCallback(data, eventData, doneChangingTextCallback); e.stopImmediatePropagation(); return false; @@ -2324,6 +2392,7 @@ if (typeof cornerstoneTools === 'undefined') { var data; function doneChangingTextCallback(data, updatedText, deleteTool) { + console.log('pressCallback doneChangingTextCallback'); if (deleteTool === true) { cornerstoneTools.removeToolState(element, toolType, data); } else { @@ -2332,8 +2401,10 @@ if (typeof cornerstoneTools === 'undefined') { data.active = false; cornerstone.updateImage(element); + $(element).on('CornerstoneToolsTouchStart', cornerstoneTools.arrowAnnotateTouch.touchStartCallback); $(element).on('CornerstoneToolsTouchStartActive', cornerstoneTools.arrowAnnotateTouch.touchDownActivateCallback); + $(element).on('CornerstoneToolsTap', cornerstoneTools.arrowAnnotateTouch.tapCallback); } if (e.data && e.data.mouseButtonMask && !cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) { @@ -2350,15 +2421,31 @@ if (typeof cornerstoneTools === 'undefined') { return false; } + if (eventData.handlePressed) { + $(element).off('CornerstoneToolsTouchStart', cornerstoneTools.arrowAnnotateTouch.touchStartCallback); + $(element).off('CornerstoneToolsTouchStartActive', cornerstoneTools.arrowAnnotateTouch.touchDownActivateCallback); + $(element).off('CornerstoneToolsTap', cornerstoneTools.arrowAnnotateTouch.tapCallback); + + // Allow relabelling via a callback + config.changeTextCallback(eventData.handlePressed, eventData, doneChangingTextCallback); + + e.stopImmediatePropagation(); + return false; + } + for (var i = 0; i < toolData.data.length; i++) { data = toolData.data[i]; - if (pointNearTool(element, data, coords)) { + if (pointNearTool(element, data, coords) || + cornerstoneTools.pointInsideBoundingBox(data.handles.textBox, coords)) { data.active = true; cornerstone.updateImage(element); - // Allow relabelling via a callback + $(element).off('CornerstoneToolsTouchStart', cornerstoneTools.arrowAnnotateTouch.touchStartCallback); $(element).off('CornerstoneToolsTouchStartActive', cornerstoneTools.arrowAnnotateTouch.touchDownActivateCallback); - config.changeTextCallback(data, doneChangingTextCallback); + $(element).off('CornerstoneToolsTap', cornerstoneTools.arrowAnnotateTouch.tapCallback); + + // Allow relabelling via a callback + config.changeTextCallback(data, eventData, doneChangingTextCallback); e.stopImmediatePropagation(); return false; @@ -2746,14 +2833,14 @@ if (typeof cornerstoneTools === 'undefined') { // Translate the x/y away from the cursor var translation; if (eventData.isTouchEvent === true) { + var handleRadius = 6; var width = context.measureText(text).width; + translation = { x: -width / 2 - 5, - y: -cornerstoneTools.textStyle.getFontSize() * 1.5 + y: -cornerstoneTools.textStyle.getFontSize() - 10 - 2 * handleRadius }; - var handleRadius = 6; - context.beginPath(); context.strokeStyle = color; context.arc(textCoords.x, textCoords.y, handleRadius, 0, 2 * Math.PI); @@ -2834,6 +2921,14 @@ if (typeof cornerstoneTools === 'undefined') { y: mouseEventData.currentPoints.image.y, highlight: true, active: true + }, + textBox: { + active: false, + hasMoved: false, + movesIndependently: false, + drawnIndependently: true, + allowedOutsideImage: true, + hasBoundingBox: true } } }; @@ -2919,14 +3014,14 @@ if (typeof cornerstoneTools === 'undefined') { var minorEllipse = { left: Math.min(startCanvas.x, endCanvas.x) + distance / 2, - top: Math.min(startCanvas.y, endCanvas.y) + distance / 2 , + top: Math.min(startCanvas.y, endCanvas.y) + distance / 2, width: Math.abs(startCanvas.x - endCanvas.x) - distance, height: Math.abs(startCanvas.y - endCanvas.y) - distance }; - + var majorEllipse = { left: Math.min(startCanvas.x, endCanvas.x) - distance / 2, - top: Math.min(startCanvas.y, endCanvas.y) - distance / 2 , + top: Math.min(startCanvas.y, endCanvas.y) - distance / 2, width: Math.abs(startCanvas.x - endCanvas.x) + distance, height: Math.abs(startCanvas.y - endCanvas.y) + distance }; @@ -2949,11 +3044,18 @@ if (typeof cornerstoneTools === 'undefined') { return pointNearEllipse(element, data, coords, 25); } + function numberWithCommas(x) { + // http://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript + var parts = x.toString().split('.'); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); + return parts.join('.'); + } + function onImageRendered(e, eventData) { // if we have no toolData for this element, return immediately as there is nothing to do var toolData = cornerstoneTools.getToolState(e.currentTarget, toolType); - if (toolData === undefined) { + if (!toolData) { return; } @@ -2962,15 +3064,12 @@ if (typeof cornerstoneTools === 'undefined') { context.setTransform(1, 0, 0, 1, 0, 0); //activation color - var color; var lineWidth = cornerstoneTools.toolStyle.getToolWidth(); - var font = cornerstoneTools.textStyle.getFont(); - var fontHeight = cornerstoneTools.textStyle.getFontSize(); var config = cornerstoneTools.ellipticalRoi.getConfiguration(); for (var i = 0; i < toolData.data.length; i++) { context.save(); - + if (config && config.shadow) { context.shadowColor = config.shadowColor || '#000000'; context.shadowOffsetX = config.shadowOffsetX || 1; @@ -2980,11 +3079,7 @@ if (typeof cornerstoneTools === 'undefined') { var data = toolData.data[i]; //differentiate the color of activation tool - if (data.active) { - color = cornerstoneTools.toolColors.getActiveColor(); - } else { - color = cornerstoneTools.toolColors.getToolColor(); - } + var color = cornerstoneTools.toolColors.getColorIfActive(data.active); // draw the ellipse var handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.start); @@ -2994,8 +3089,6 @@ if (typeof cornerstoneTools === 'undefined') { var heightCanvas = Math.abs(handleStartCanvas.y - handleEndCanvas.y); var leftCanvas = Math.min(handleStartCanvas.x, handleEndCanvas.x); var topCanvas = Math.min(handleStartCanvas.y, handleEndCanvas.y); - var centerX = (handleStartCanvas.x + handleEndCanvas.x) / 2; - var centerY = (handleStartCanvas.y + handleEndCanvas.y) / 2; context.beginPath(); context.strokeStyle = color; @@ -3004,18 +3097,16 @@ if (typeof cornerstoneTools === 'undefined') { context.closePath(); // draw the handles - cornerstoneTools.drawHandles(context, eventData, data.handles, color); - - context.font = font; + var handleOptions = { + drawHandlesIfActive: (config && config.drawHandlesOnHover) + }; - var textX, - textY, - area, + cornerstoneTools.drawHandles(context, eventData, data.handles, color, handleOptions); + + var area, meanStdDev; if (!data.invalidated) { - textX = data.textX; - textY = data.textY; meanStdDev = data.meanStdDev; area = data.area; } else { @@ -3048,35 +3139,100 @@ if (typeof cornerstoneTools === 'undefined') { } } - // Draw text - - var areaText, - areaTextWidth = 0; - if (area !== undefined) { - areaText = 'Area: ' + area.toFixed(2) + ' mm' + String.fromCharCode(178); - areaTextWidth = context.measureText(areaText).width; - } - - var meanText = 'Mean: ' + meanStdDev.mean.toFixed(2); - var meanTextWidth = context.measureText(meanText).width; - - var stdDevText = 'StdDev: ' + meanStdDev.stdDev.toFixed(2); - var stdDevTextWidth = context.measureText(stdDevText).width; - - var longestTextWidth = Math.max(meanTextWidth, areaTextWidth, stdDevTextWidth); - - textX = centerX < (eventData.image.columns / 2) ? centerX + (widthCanvas / 2) + longestTextWidth: centerX - (widthCanvas / 2) - longestTextWidth - 15; - textY = centerY < (eventData.image.rows / 2) ? centerY + (heightCanvas / 2): centerY - (heightCanvas / 2); - - context.fillStyle = color; + var textLines = []; if (meanStdDev) { - cornerstoneTools.drawTextBox(context, meanText, textX, textY - fontHeight - 5, color); - cornerstoneTools.drawTextBox(context, stdDevText, textX, textY, color); + var meanText = 'Mean: ' + numberWithCommas(meanStdDev.mean.toFixed(2)); + textLines.push(meanText); + + var stdDevText = 'StdDev: ' + numberWithCommas(meanStdDev.stdDev.toFixed(2)); + textLines.push(stdDevText); } - - // Char code 178 is a superscript 2 for mm^2 + if (area !== undefined && !isNaN(area)) { - cornerstoneTools.drawTextBox(context, areaText, textX, textY + fontHeight + 5, color); + // Char code 178 is a superscript 2 for mm^2 + var areaText = 'Area: ' + numberWithCommas(area.toFixed(2)) + ' mm' + String.fromCharCode(178); + textLines.push(areaText); + } + + if (!data.handles.textBox.hasMoved) { + data.handles.textBox.x = Math.max(data.handles.start.x, data.handles.end.x); + data.handles.textBox.y = (data.handles.start.y + data.handles.end.y) / 2; + } + + var textCoords = cornerstone.pixelToCanvas(eventData.element, data.handles.textBox); + + // Draw text + var options = { + centering: { + x: false, + y: true + } + }; + + var boundingBox = cornerstoneTools.drawTextBox(context, textLines, textCoords.x, + textCoords.y, color, options); + + data.handles.textBox.boundingBox = boundingBox; + + if (data.handles.textBox.hasMoved) { + // Draw dashed link line between tool and text + var link = { + start: {}, + end: {} + }; + + var ellipsePoints = [ { + // Top middle point of ellipse + x: leftCanvas + widthCanvas / 2, + y: topCanvas + }, { + // Left middle point of ellipse + x: leftCanvas, + y: topCanvas + heightCanvas / 2 + }, { + // Bottom middle point of ellipse + x: leftCanvas + widthCanvas / 2, + y: topCanvas + heightCanvas + }, { + // Right middle point of ellipse + x: leftCanvas + widthCanvas, + y: topCanvas + heightCanvas / 2 + }, + ]; + + link.end.x = textCoords.x; + link.end.y = textCoords.y; + + link.start = cornerstoneMath.point.findClosestPoint(ellipsePoints, link.end); + + var boundingBoxPoints = [ { + // Top middle point of bounding box + x: boundingBox.left + boundingBox.width / 2, + y: boundingBox.top + }, { + // Left middle point of bounding box + x: boundingBox.left, + y: boundingBox.top + boundingBox.height / 2 + }, { + // Bottom middle point of bounding box + x: boundingBox.left + boundingBox.width / 2, + y: boundingBox.top + boundingBox.height + }, { + // Right middle point of bounding box + x: boundingBox.left + boundingBox.width, + y: boundingBox.top + boundingBox.height / 2 + }, + ]; + + link.end = cornerstoneMath.point.findClosestPoint(boundingBoxPoints, link.start); + + context.beginPath(); + context.strokeStyle = color; + context.lineWidth = lineWidth; + context.setLineDash([ 2, 3 ]); + context.moveTo(link.start.x, link.start.y); + context.lineTo(link.end.x, link.end.y); + context.stroke(); } context.restore(); @@ -3443,11 +3599,15 @@ if (typeof cornerstoneTools === 'undefined') { } // If the tool is active, draw a handle at the cursor location + var options = { + fill: fillColor + }; + if (data.active){ - cornerstoneTools.drawHandles(context, eventData, config.mouseLocation.handles, color, fillColor); + cornerstoneTools.drawHandles(context, eventData, config.mouseLocation.handles, color, options); } // draw the handles - cornerstoneTools.drawHandles(context, eventData, data.handles, color, fillColor); + cornerstoneTools.drawHandles(context, eventData, data.handles, color, options); context.restore(); } @@ -3742,6 +3902,14 @@ if (typeof cornerstoneTools === 'undefined') { y: mouseEventData.currentPoints.image.y, highlight: true, active: true + }, + textBox: { + active: false, + hasMoved: false, + movesIndependently: false, + drawnIndependently: true, + allowedOutsideImage: true, + hasBoundingBox: true } } }; @@ -3801,7 +3969,11 @@ if (typeof cornerstoneTools === 'undefined') { context.stroke(); // Draw the handles - cornerstoneTools.drawHandles(context, eventData, data.handles, color); + var handleOptions = { + drawHandlesIfActive: (config && config.drawHandlesOnHover) + }; + + cornerstoneTools.drawHandles(context, eventData, data.handles, color, handleOptions); // Draw the text context.fillStyle = color; @@ -3822,27 +3994,88 @@ if (typeof cornerstoneTools === 'undefined') { // Store the length measurement text var text = '' + length.toFixed(2) + suffix; - // Place the length measurement text next to the right-most handle - var fontSize = cornerstoneTools.textStyle.getFontSize(); - var textCoords = { - x: Math.max(handleStartCanvas.x, handleEndCanvas.x), - }; + if (!data.handles.textBox.hasMoved) { + var coords = { + x: Math.max(data.handles.start.x, data.handles.end.x), + }; - // Depending on which handle has the largest x-value, - // set the y-value for the text box - if (textCoords.x === handleStartCanvas.x) { - textCoords.y = handleStartCanvas.y; - } else { - textCoords.y = handleEndCanvas.y; + // Depending on which handle has the largest x-value, + // set the y-value for the text box + if (coords.x === data.handles.start.x) { + coords.y = data.handles.start.y; + } else { + coords.y = data.handles.end.y; + } + + data.handles.textBox.x = coords.x; + data.handles.textBox.y = coords.y; } + + var textCoords = cornerstone.pixelToCanvas(eventData.element, data.handles.textBox); // Move the textbox slightly to the right and upwards // so that it sits beside the length tool handle textCoords.x += 10; - textCoords.y -= fontSize / 2 + 7; + + var options = { + centering: { + x: false, + y: true + } + }; // Draw the textbox - cornerstoneTools.drawTextBox(context, text, textCoords.x, textCoords.y, color); + var boundingBox = cornerstoneTools.drawTextBox(context, text, textCoords.x, textCoords.y, color, options); + data.handles.textBox.boundingBox = boundingBox; + + if (data.handles.textBox.hasMoved) { + // Draw dashed link line between ellipse and text + var link = { + start: {}, + end: {} + }; + + var midpointCanvas = { + x: (handleStartCanvas.x + handleEndCanvas.x) / 2, + y: (handleStartCanvas.y + handleEndCanvas.y) / 2, + }; + + var points = [ handleStartCanvas, handleEndCanvas, midpointCanvas ]; + + link.end.x = textCoords.x; + link.end.y = textCoords.y; + + link.start = cornerstoneMath.point.findClosestPoint(points, link.end); + + var boundingBoxPoints = [ { + // Top middle point of bounding box + x: boundingBox.left + boundingBox.width / 2, + y: boundingBox.top + }, { + // Left middle point of bounding box + x: boundingBox.left, + y: boundingBox.top + boundingBox.height / 2 + }, { + // Bottom middle point of bounding box + x: boundingBox.left + boundingBox.width / 2, + y: boundingBox.top + boundingBox.height + }, { + // Right middle point of bounding box + x: boundingBox.left + boundingBox.width, + y: boundingBox.top + boundingBox.height / 2 + }, + ]; + + link.end = cornerstoneMath.point.findClosestPoint(boundingBoxPoints, link.start); + + context.beginPath(); + context.strokeStyle = color; + context.lineWidth = lineWidth; + context.setLineDash([ 2, 3 ]); + context.moveTo(link.start.x, link.start.y); + context.lineTo(link.end.x, link.end.y); + context.stroke(); + } context.restore(); } @@ -4083,23 +4316,44 @@ if (typeof cornerstoneTools === 'undefined') { 'use strict'; - function multiTouchDragTool(touchDragCallback) { + function multiTouchDragTool(touchDragCallback, options) { var configuration = {}; var events = 'CornerstoneToolsMultiTouchDrag'; - + if (options && options.fireOnTouchStart === true) { + events += ' CornerstoneToolsMultiTouchStart'; + } + var toolInterface = { activate: function(element) { $(element).off(events, touchDragCallback); - $(element).on(events, touchDragCallback); + + if (options && options.eventData) { + $(element).on(events, options.eventData, touchDragCallback); + } else { + $(element).on(events, touchDragCallback); + } + + if (options && options.activateCallback) { + options.activateCallback(element); + } }, disable: function(element) { $(element).off(events, touchDragCallback); + if (options && options.disableCallback) { + options.disableCallback(element); + } }, enable: function(element) { $(element).off(events, touchDragCallback); + if (options && options.enableCallback) { + options.enableCallback(element); + } }, deactivate: function(element) { $(element).off(events, touchDragCallback); + if (options && options.deactivateCallback) { + options.deactivateCallback(element); + } }, getConfiguration: function() { return configuration; @@ -4779,6 +5033,14 @@ if (typeof cornerstoneTools === 'undefined') { y: mouseEventData.currentPoints.image.y, highlight: true, active: false + }, + textBox: { + active: false, + hasMoved: false, + movesIndependently: false, + drawnIndependently: true, + allowedOutsideImage: true, + hasBoundingBox: true } } }; @@ -4814,10 +5076,12 @@ if (typeof cornerstoneTools === 'undefined') { // if we have no toolData for this element, return immediately as there is nothing to do var toolData = cornerstoneTools.getToolState(e.currentTarget, toolType); - if (toolData === undefined) { + if (!toolData) { return; } + var enabledElement = eventData.enabledElement; + // we have tool data for this element - iterate over each one and draw it var context = eventData.canvasContext.canvas.getContext('2d'); context.setTransform(1, 0, 0, 1, 0, 0); @@ -4846,24 +5110,25 @@ if (typeof cornerstoneTools === 'undefined') { color = cornerstoneTools.toolColors.getToolColor(); } + var handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.start); + var handleMiddleCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.middle); + var handleEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.end); + // draw the line context.beginPath(); context.strokeStyle = color; context.lineWidth = lineWidth; - - var handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.start); - var handleMiddleCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.middle); - context.moveTo(handleStartCanvas.x, handleStartCanvas.y); context.lineTo(handleMiddleCanvas.x, handleMiddleCanvas.y); - - var handleEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.end); - context.lineTo(handleEndCanvas.x, handleEndCanvas.y); context.stroke(); // draw the handles - cornerstoneTools.drawHandles(context, eventData, data.handles, color); + var handleOptions = { + drawHandlesIfActive: (config && config.drawHandlesOnHover) + }; + + cornerstoneTools.drawHandles(context, eventData, data.handles, color, handleOptions); // Draw the text context.fillStyle = color; @@ -4907,20 +5172,85 @@ if (typeof cornerstoneTools === 'undefined') { var distance = 15; - var textX = handleMiddleCanvas.x + distance; - var textY = handleMiddleCanvas.y + distance; + var textCoords; + if (!data.handles.textBox.hasMoved) { + textCoords = { + x: handleMiddleCanvas.x, + y: handleMiddleCanvas.y + }; - context.font = font; - var textWidth = context.measureText(text).width; + context.font = font; + var textWidth = context.measureText(text).width; + if (handleMiddleCanvas.x < handleStartCanvas.x) { + textCoords.x -= distance + textWidth + 10; + } else { + textCoords.x += distance; + } + + var transform = cornerstone.internal.getTransform(enabledElement); + transform.invert(); + + var coords = transform.transformPoint(textCoords.x, textCoords.y); + data.handles.textBox.x = coords.x; + data.handles.textBox.y = coords.y; - if ((handleMiddleCanvas.x - handleStartCanvas.x) < 0) { - textX = handleMiddleCanvas.x - distance - textWidth - 10; } else { - textX = handleMiddleCanvas.x + distance; + textCoords = cornerstone.pixelToCanvas(eventData.element, data.handles.textBox); } - textY = handleMiddleCanvas.y; - cornerstoneTools.drawTextBox(context, text, textX, textY, color); + var options = { + centering: { + x: false, + y: true + } + }; + + var boundingBox = cornerstoneTools.drawTextBox(context, text, textCoords.x, textCoords.y, color, options); + data.handles.textBox.boundingBox = boundingBox; + + if (data.handles.textBox.hasMoved) { + // Draw dashed link line between tool and text + var link = { + start: {}, + end: {} + }; + + var points = [ handleStartCanvas, handleEndCanvas, handleMiddleCanvas ]; + + link.end.x = textCoords.x; + link.end.y = textCoords.y; + + link.start = cornerstoneMath.point.findClosestPoint(points, link.end); + + var boundingBoxPoints = [ { + // Top middle point of bounding box + x: boundingBox.left + boundingBox.width / 2, + y: boundingBox.top + }, { + // Left middle point of bounding box + x: boundingBox.left, + y: boundingBox.top + boundingBox.height / 2 + }, { + // Bottom middle point of bounding box + x: boundingBox.left + boundingBox.width / 2, + y: boundingBox.top + boundingBox.height + }, { + // Right middle point of bounding box + x: boundingBox.left + boundingBox.width, + y: boundingBox.top + boundingBox.height / 2 + }, + ]; + + link.end = cornerstoneMath.point.findClosestPoint(boundingBoxPoints, link.start); + + context.beginPath(); + context.strokeStyle = color; + context.lineWidth = lineWidth; + context.setLineDash([ 2, 3 ]); + context.moveTo(link.start.x, link.start.y); + context.lineTo(link.end.x, link.end.y); + context.stroke(); + } } context.restore(); @@ -4989,28 +5319,24 @@ if (typeof cornerstoneTools === 'undefined') { $(element).off('CornerstoneToolsTap', cornerstoneTools.simpleAngleTouch.tapCallback); cornerstone.updateImage(element); - cornerstoneTools.moveNewHandleTouch(touchEventData, measurementData.handles.middle, function() { - measurementData.active = false; + cornerstoneTools.moveNewHandleTouch(touchEventData, toolType, measurementData, measurementData.handles.middle, function() { if (cornerstoneTools.anyHandlesOutsideImage(touchEventData, measurementData.handles)) { // delete the measurement cornerstoneTools.removeToolState(element, toolType, measurementData); + cornerstone.updateImage(element); } - measurementData.handles.end.active = true; - cornerstone.updateImage(element); - - cornerstoneTools.moveNewHandleTouch(touchEventData, measurementData.handles.end, function() { - measurementData.active = false; + cornerstoneTools.moveNewHandleTouch(touchEventData, toolType, measurementData, measurementData.handles.end, function() { if (cornerstoneTools.anyHandlesOutsideImage(touchEventData, measurementData.handles)) { // delete the measurement cornerstoneTools.removeToolState(element, toolType, measurementData); + cornerstone.updateImage(element); } $(element).on('CornerstoneToolsTouchDrag', cornerstoneTools.simpleAngleTouch.touchMoveCallback); $(element).on('CornerstoneToolsTouchStart', cornerstoneTools.simpleAngleTouch.touchStartCallback); $(element).on('CornerstoneToolsTouchStartActive', cornerstoneTools.simpleAngleTouch.touchDownActivateCallback); $(element).on('CornerstoneToolsTap', cornerstoneTools.simpleAngleTouch.tapCallback); - cornerstone.updateImage(element); }); }); } @@ -5095,16 +5421,11 @@ if (typeof cornerstoneTools === 'undefined') { ///////// BEGIN IMAGE RENDERING /////// function pointNearTool(element, data, coords) { - var endCanvas = cornerstone.pixelToCanvas(element, data.handles.end); + if (!data.textBoundingBox) { + return; + } - var rect = { - left: endCanvas.x - data.textWidth / 2, - top: endCanvas.y, - width: data.textWidth, - height: data.textHeight - }; - - var distanceToPoint = cornerstoneMath.rect.distanceToPoint(rect, coords); + var distanceToPoint = cornerstoneMath.rect.distanceToPoint(data.textBoundingBox, coords); return (distanceToPoint < 10); } @@ -5119,8 +5440,6 @@ if (typeof cornerstoneTools === 'undefined') { var context = eventData.canvasContext.canvas.getContext('2d'); context.setTransform(1, 0, 0, 1, 0, 0); - var font = cornerstoneTools.textStyle.getFont(); - var fontSize = cornerstoneTools.textStyle.getFontSize(); var config = cornerstoneTools.textMarker.getConfiguration(); for (var i = 0; i < toolData.data.length; i++) { @@ -5140,21 +5459,14 @@ if (typeof cornerstoneTools === 'undefined') { } // Draw text - context.font = font; context.fillStyle = color; - var measureText = context.measureText(data.text); - data.textWidth = measureText.width; - data.textHeight = fontSize; + data.textWidth = measureText.width + 10; - var coords = { - x: data.handles.end.x, - y: data.handles.end.y - }; + var textCoords = cornerstone.pixelToCanvas(eventData.element, data.handles.end); - var textCoords = cornerstone.pixelToCanvas(eventData.element, coords); - - cornerstoneTools.drawTextBox(context, data.text, textCoords.x - data.textWidth / 2, textCoords.y, color); + var boundingBox = cornerstoneTools.drawTextBox(context, data.text, textCoords.x - data.textWidth / 2, textCoords.y, color); + data.textBoundingBox = boundingBox; context.restore(); } @@ -5204,7 +5516,7 @@ if (typeof cornerstoneTools === 'undefined') { $(element).off('CornerstoneToolsMouseDownActivate', cornerstoneTools.textMarker.mouseDownActivateCallback); $(element).off('CornerstoneToolsMouseDoubleClick', cornerstoneTools.textMarker.mouseDoubleClickCallback); // Allow relabelling via a callback - config.changeTextCallback(data, doneChangingTextCallback); + config.changeTextCallback(data, eventData, doneChangingTextCallback); e.stopImmediatePropagation(); return false; @@ -5227,11 +5539,6 @@ if (typeof cornerstoneTools === 'undefined') { data.active = false; cornerstone.updateImage(element); - $(element).off('CornerstoneToolsTouchDrag', cornerstoneTools.textMarkerTouch.touchMoveCallback); - $(element).off('CornerstoneToolsTouchStartActive', cornerstoneTools.textMarkerTouch.touchDownActivateCallback); - $(element).off('CornerstoneToolsTouchStart', cornerstoneTools.textMarkerTouch.touchStartCallback); - $(element).off('CornerstoneToolsTap', cornerstoneTools.textMarkerTouch.tapCallback); - $(element).off('CornerstoneToolsTouchPress', cornerstoneTools.textMarkerTouch.pressCallback); $(element).on('CornerstoneToolsTouchDrag', cornerstoneTools.textMarkerTouch.touchMoveCallback); $(element).on('CornerstoneToolsTouchStartActive', cornerstoneTools.textMarkerTouch.touchDownActivateCallback); @@ -5254,6 +5561,23 @@ if (typeof cornerstoneTools === 'undefined') { return false; } + if (eventData.handlePressed) { + eventData.handlePressed.active = true; + cornerstone.updateImage(element); + + $(element).off('CornerstoneToolsTouchDrag', cornerstoneTools.textMarkerTouch.touchMoveCallback); + $(element).off('CornerstoneToolsTouchStartActive', cornerstoneTools.textMarkerTouch.touchDownActivateCallback); + $(element).off('CornerstoneToolsTouchStart', cornerstoneTools.textMarkerTouch.touchStartCallback); + $(element).off('CornerstoneToolsTap', cornerstoneTools.textMarkerTouch.tapCallback); + $(element).off('CornerstoneToolsTouchPress', cornerstoneTools.textMarkerTouch.pressCallback); + + // Allow relabelling via a callback + config.changeTextCallback(eventData.handlePressed, eventData, doneChangingTextCallback); + + e.stopImmediatePropagation(); + return false; + } + for (var i = 0; i < toolData.data.length; i++) { data = toolData.data[i]; if (pointNearTool(element, data, coords)) { @@ -5266,7 +5590,7 @@ if (typeof cornerstoneTools === 'undefined') { $(element).off('CornerstoneToolsTap', cornerstoneTools.textMarkerTouch.tapCallback); $(element).off('CornerstoneToolsTouchPress', cornerstoneTools.textMarkerTouch.pressCallback); // Allow relabelling via a callback - config.changeTextCallback(data, doneChangingTextCallback); + config.changeTextCallback(data, eventData, doneChangingTextCallback); e.stopImmediatePropagation(); return false; @@ -6136,7 +6460,7 @@ if (typeof cornerstoneTools === 'undefined') { var handleRadius = 6; - function drawHandles(context, renderData, handles, color, fill) { + function drawHandles(context, renderData, handles, color, options) { context.strokeStyle = color; Object.keys(handles).forEach(function(name) { @@ -6145,6 +6469,10 @@ if (typeof cornerstoneTools === 'undefined') { return; } + if (options && options.drawHandlesIfActive === true && !handle.active) { + return; + } + context.beginPath(); if (handle.active) { @@ -6156,8 +6484,8 @@ if (typeof cornerstoneTools === 'undefined') { var handleCanvasCoords = cornerstone.pixelToCanvas(renderData.element, handle); context.arc(handleCanvasCoords.x, handleCanvasCoords.y, handleRadius, 0, 2 * Math.PI); - if (fill) { - context.fillStyle = fill; + if (options && options.fill) { + context.fillStyle = options.fill; context.fill(); } @@ -6191,6 +6519,11 @@ if (typeof cornerstoneTools === 'undefined') { nearbyHandle = handle; return; } + } else if (handle.hasBoundingBox === true) { + if (cornerstoneTools.pointInsideBoundingBox(handle, coords)) { + nearbyHandle = handle; + return; + } } else { var handleCanvas = cornerstone.pixelToCanvas(element, handle); var distance = cornerstoneMath.point.distance(handleCanvas, coords); @@ -6232,7 +6565,7 @@ if (typeof cornerstoneTools === 'undefined') { function handleActivator(element, handles, canvasPoint, distanceThreshold) { if (!distanceThreshold) { - distanceThreshold = 36; + distanceThreshold = 6; } var activeHandle = getActiveHandle(handles); @@ -6304,7 +6637,6 @@ if (typeof cornerstoneTools === 'undefined') { $(element).on('CornerstoneToolsMouseDrag', mouseDragCallback); function mouseUpCallback(e, eventData) { - data.active = false; data.invalidated = true; $(element).off('CornerstoneToolsMouseDrag', mouseDragCallback); @@ -6312,39 +6644,9 @@ if (typeof cornerstoneTools === 'undefined') { $(element).off('CornerstoneToolsMouseClick', mouseUpCallback); // If any handle is outside the image, delete the tool data - if (options.deleteIfHandleOutsideImage === true) { - var image = eventData.image; - var handleOutsideImage = false; - var rect = { - top: 0, - left: 0, - width: image.width, - height: image.height - }; - - Object.keys(data.handles).forEach(function(name) { - var handle = data.handles[name]; - handle.active = false; - if (cornerstoneMath.point.insideRect(handle, rect) === false) { - handleOutsideImage = true; - return false; - } - }); - - if (handleOutsideImage) { - // find this tool data - var indexOfData = -1; - toolData.data.forEach(function(thisToolData, index) { - if (thisToolData === data) { - indexOfData = index; - return false; - } - }); - - if (indexOfData !== -1) { - toolData.data.splice(indexOfData, 1); - } - } + if (options.deleteIfHandleOutsideImage === true && + cornerstoneTools.anyHandlesOutsideImage(eventData, data.handles)) { + cornerstoneTools.toolState.removeToolState(element, toolType, data); } cornerstone.updateImage(element); @@ -6379,6 +6681,10 @@ if (typeof cornerstoneTools === 'undefined') { }; function mouseDragCallback(e, eventData) { + if (handle.hasMoved === false) { + handle.hasMoved = true; + } + handle.active = true; handle.x = eventData.currentPoints.image.x + distanceFromTool.x; handle.y = eventData.currentPoints.image.y + distanceFromTool.y; @@ -6511,6 +6817,7 @@ if (typeof cornerstoneTools === 'undefined') { 'use strict'; function moveNewHandleTouch(eventData, toolType, data, handle, doneMovingCallback, preventHandleOutsideImage) { + console.log('moveNewHandleTouch'); var element = eventData.element; var imageCoords = cornerstone.pageToPixel(element, eventData.currentPoints.page.x, eventData.currentPoints.page.y + 50); var distanceFromTouch = { @@ -6518,8 +6825,10 @@ if (typeof cornerstoneTools === 'undefined') { y: handle.y - imageCoords.y }; + handle.active = true; + data.active = true; + function moveCallback(e, eventData) { - handle.active = true; handle.x = eventData.currentPoints.image.x + distanceFromTouch.x; handle.y = eventData.currentPoints.image.y + distanceFromTouch.y; @@ -6543,11 +6852,11 @@ if (typeof cornerstoneTools === 'undefined') { } function moveEndCallback(e, eventData) { + console.log(e); $(element).off('CornerstoneToolsTouchDrag', moveCallback); $(element).off('CornerstoneToolsTouchPinch', moveEndCallback); - $(element).off('CornerstoneToolsTouchPress', moveEndCallback); $(element).off('CornerstoneToolsTouchEnd', moveEndCallback); - $(element).off('CornerstoneToolsDragEnd', moveEndCallback); + //$(element).off('CornerstoneToolsDragEnd', moveEndCallback); $(element).off('CornerstoneToolsTap', moveEndCallback); if (e.type === 'CornerstoneToolsTouchPinch' || e.type === 'CornerstoneToolsTouchPress') { @@ -6558,6 +6867,7 @@ if (typeof cornerstoneTools === 'undefined') { } handle.active = false; + data.active = false; handle.x = eventData.currentPoints.image.x + distanceFromTouch.x; handle.y = eventData.currentPoints.image.y + distanceFromTouch.y; @@ -6578,9 +6888,8 @@ if (typeof cornerstoneTools === 'undefined') { $(element).on('CornerstoneToolsTouchDrag', moveCallback); $(element).on('CornerstoneToolsTouchPinch', moveEndCallback); - $(element).on('CornerstoneToolsTouchPress', moveEndCallback); $(element).on('CornerstoneToolsTouchEnd', moveEndCallback); - $(element).on('CornerstoneToolsDragEnd', moveEndCallback); + //$(element).on('CornerstoneToolsDragEnd', moveEndCallback); $(element).on('CornerstoneToolsTap', moveEndCallback); } @@ -6629,7 +6938,7 @@ if (typeof cornerstoneTools === 'undefined') { $(element).on('CornerstoneToolsTouchDrag', touchDragCallback); function touchEndCallback(e, eventData) { - //console.log('touchMoveAllHandles touchEndCallback'); + //console.log('touchMoveAllHandles touchEndCallback: ' + e.type); data.active = false; data.invalidated = false; @@ -6641,44 +6950,16 @@ if (typeof cornerstoneTools === 'undefined') { $(element).off('CornerstoneToolsTap', touchEndCallback); // If any handle is outside the image, delete the tool data - if (deleteIfHandleOutsideImage === true) { - var image = eventData.image; - var handleOutsideImage = false; - var rect = { - top: 0, - left: 0, - width: image.width, - height: image.height - }; - - Object.keys(data.handles).forEach(function(name) { - var handle = data.handles[name]; - if (cornerstoneMath.point.insideRect(handle, rect) === false) { - handleOutsideImage = true; - return; - } - }); - - if (handleOutsideImage) { - // find this tool data - var indexOfData = -1; - for (var i = 0; i < toolData.data.length; i++) { - if (toolData.data[i] === data) { - indexOfData = i; - } - } - - if (indexOfData !== -1) { - toolData.data.splice(indexOfData, 1); - } - } - } - - if (typeof doneMovingCallback === 'function') { - doneMovingCallback(); + if (deleteIfHandleOutsideImage === true && + cornerstoneTools.anyHandlesOutsideImage(eventData, data.handles)) { + cornerstoneTools.toolState.removeToolState(element, toolType, data); } cornerstone.updateImage(element); + + if (typeof doneMovingCallback === 'function') { + doneMovingCallback(e, eventData); + } } $(element).on('CornerstoneToolsTouchPinch', touchEndCallback); @@ -6701,19 +6982,97 @@ if (typeof cornerstoneTools === 'undefined') { 'use strict'; + /* + * define the runAnimation boolean as an object + * so that it can be modified by reference + */ + var runAnimation = { + value: false + }; + + var touchEndEvents = [ 'CornerstoneToolsTouchEnd', + 'CornerstoneToolsDragEnd', + 'CornerstoneToolsTouchPinch', + 'CornerstoneToolsTouchPress', + 'CornerstoneToolsTap' + ].join(' '); + + function animate(lastTime, handle, runAnimation, enabledElement, targetLocation) { + // See http://www.html5canvastutorials.com/advanced/html5-canvas-start-and-stop-an-animation/ + if (!runAnimation.value) { + return; + } + + // update + var time = (new Date()).getTime(); + //var timeDiff = time - lastTime; + + // pixels / second + var distanceRemaining = Math.abs(handle.y - targetLocation.y); + var linearDistEachFrame = distanceRemaining / 10; + + console.log('distanceRemaining: ' + distanceRemaining); + if (distanceRemaining < 1) { + handle.y = targetLocation.y; + runAnimation.value = false; + return; + } + + if (handle.y > targetLocation.y) { + handle.y -= linearDistEachFrame; + } else if (handle.y < targetLocation.y) { + handle.y += linearDistEachFrame; + } + + // Update the image + cornerstone.updateImage(enabledElement.element); + + // Request a new frame + cornerstoneTools.requestAnimFrame(function() { + animate(time, handle, runAnimation, enabledElement, targetLocation); + }); + } + function touchMoveHandle(touchEventData, toolType, data, handle, doneMovingCallback) { //console.log('touchMoveHandle'); + runAnimation.value = true; + var element = touchEventData.element; - var distanceFromTouch = { - x: handle.x - touchEventData.currentPoints.image.x, - y: handle.y - touchEventData.currentPoints.image.y + var enabledElement = cornerstone.getEnabledElement(element); + + var time = (new Date()).getTime(); + + // Average pixel width of index finger is 45-57 pixels + // https://www.smashingmagazine.com/2012/02/finger-friendly-design-ideal-mobile-touchscreen-target-sizes/ + var fingerDistance = -57; + + var aboveFinger = { + x: touchEventData.currentPoints.page.x, + y: touchEventData.currentPoints.page.y + fingerDistance }; + var targetLocation = cornerstone.pageToPixel(element, aboveFinger.x, aboveFinger.y); + function touchDragCallback(e, eventData) { + console.log('touchMoveHandle touchDragCallback: ' + e.type); + runAnimation.value = false; + + if (handle.hasMoved === false) { + handle.hasMoved = true; + } + handle.active = true; - var touchMoveData = eventData; - handle.x = touchMoveData.currentPoints.image.x + distanceFromTouch.x; - handle.y = touchMoveData.currentPoints.image.y + distanceFromTouch.y; + + var currentPoints = eventData.currentPoints; + var aboveFinger = { + x: currentPoints.page.x, + y: currentPoints.page.y + fingerDistance + }; + + targetLocation = cornerstone.pageToPixel(element, aboveFinger.x, aboveFinger.y); + handle.x = targetLocation.x; + handle.y = targetLocation.y; + cornerstone.updateImage(element); var eventType = 'CornerstoneToolsMeasurementModified'; @@ -6727,24 +7086,31 @@ if (typeof cornerstoneTools === 'undefined') { $(element).on('CornerstoneToolsTouchDrag', touchDragCallback); - function touchEndCallback() { + function touchEndCallback(e, eventData) { + console.log('touchMoveHandle touchEndCallback: ' + e.type); + runAnimation.value = false; + handle.active = false; $(element).off('CornerstoneToolsTouchDrag', touchDragCallback); - $(element).off('CornerstoneToolsTouchPinch', touchEndCallback); - $(element).off('CornerstoneToolsTouchEnd', touchEndCallback); - $(element).off('CornerstoneToolsDragEnd', touchEndCallback); - $(element).off('CornerstoneToolsTap', touchEndCallback); + $(element).off(touchEndEvents, touchEndCallback); + cornerstone.updateImage(element); + if (e.type === 'CornerstoneToolsTouchPress') { + eventData.handlePressed = data; + + handle.x = touchEventData.currentPoints.image.x; + handle.y = touchEventData.currentPoints.image.y; + } + if (typeof doneMovingCallback === 'function') { - doneMovingCallback(); + doneMovingCallback(e, eventData); } } - $(element).on('CornerstoneToolsTouchPinch', touchEndCallback); - $(element).on('CornerstoneToolsTouchEnd', touchEndCallback); - $(element).on('CornerstoneToolsDragEnd', touchEndCallback); - $(element).on('CornerstoneToolsTap', touchEndCallback); + $(element).on(touchEndEvents, touchEndCallback); + + animate(time, handle, runAnimation, enabledElement, targetLocation); } // module/private exports @@ -7889,6 +8255,22 @@ Display scroll progress bar across bottom of image. }; cornerstoneTools.stackScrollTouchDrag = cornerstoneTools.touchDragTool(dragCallback, options); + function multiTouchDragCallback(e, eventData) { + var config = cornerstoneTools.stackScrollMultiTouch.getConfiguration(); + if (config && config.testPointers(eventData)) { + dragCallback(e, eventData); + } + } + + var configuration = { + testPointers: function(eventData) { + return (eventData.numPointers >= 3); + } + }; + + cornerstoneTools.stackScrollMultiTouch = cornerstoneTools.multiTouchDragTool(multiTouchDragCallback, options); + cornerstoneTools.stackScrollMultiTouch.setConfiguration(configuration); + })($, cornerstone, cornerstoneTools); // End Source; src/stackTools/stackScroll.js @@ -8714,40 +9096,6 @@ Display scroll progress bar across bottom of image. // End Source; src/stateManagement/toolStyleManager.js -// Begin Source: src/stateManagement/touchSettingsManager.js -(function(cornerstoneTools) { - - 'use strict'; - - function touchSettingsManager() { - var defaultDistanceFromTouch = { - x: 0, - y: -55 - }; - - function setToolDistanceFromTouch(distance){ - defaultDistanceFromTouch = distance; - } - - function getToolDistanceFromTouch(){ - return defaultDistanceFromTouch; - } - - var touchSettings = { - setToolDistanceFromTouch: setToolDistanceFromTouch, - getToolDistanceFromTouch: getToolDistanceFromTouch, - }; - - return touchSettings; - } - - // module/private exports - cornerstoneTools.touchSettings = touchSettingsManager(); - -})(cornerstoneTools); - -// End Source; src/stateManagement/touchSettingsManager.js - // Begin Source: src/synchronization/panZoomSynchronizer.js (function($, cornerstone, cornerstoneTools) { @@ -9956,7 +10304,7 @@ Display scroll progress bar across bottom of image. 'use strict'; - function drawTextBox(context, textLines, x, y, color) { + function drawTextBox(context, textLines, x, y, color, options) { if (Object.prototype.toString.call(textLines) !== '[object Array]') { textLines = [ textLines ]; } @@ -9986,18 +10334,39 @@ Display scroll progress bar across bottom of image. // Calculate the bounding box for this text box var boundingBox = { - left: x, - top: y, width: maxWidth + (padding * 2), - height: textLines.length * (fontSize + (padding * 2)) + height: padding + textLines.length * (fontSize + padding) }; + + if (options && options.centering && options.centering.x === true) { + x -= boundingBox.width / 2; + } + + if (options && options.centering && options.centering.y === true) { + y -= boundingBox.height / 2; + } + + boundingBox.left = x; + boundingBox.top = y; + + if (options && options.debug === true) { + context.fillStyle = '#FF0000'; + } + context.fillRect(boundingBox.left, boundingBox.top, boundingBox.width, boundingBox.height); // Draw each of the text lines on top of the background box textLines.forEach(function(text, index) { context.fillStyle = color; - context.fillText(text, x + padding, y + fontSize * index + padding); + var ypos; + if (index === 0) { + ypos = y + index * (fontSize + padding); + } else { + ypos = y + index * (fontSize + padding * 2); + } + + context.fillText(text, x + padding, y + padding + index * (fontSize + padding)); }); context.restore(); @@ -10090,7 +10459,7 @@ Display scroll progress bar across bottom of image. default: 6 }, Safari: { - default: 4 + default: 6 } }; @@ -10250,6 +10619,26 @@ Display scroll progress bar across bottom of image. // End Source; src/util/pauseEvent.js +// Begin Source: src/util/pointInsideBoundingBox.js +(function(cornerstoneMath, cornerstoneTools) { + + 'use strict'; + + function pointInsideBoundingBox(handle, coords) { + if (!handle.boundingBox) { + return; + } + + return cornerstoneMath.point.insideRect(coords, handle.boundingBox); + } + + // module exports + cornerstoneTools.pointInsideBoundingBox = pointInsideBoundingBox; + +})(cornerstoneMath, cornerstoneTools); + +// End Source; src/util/pointInsideBoundingBox.js + // Begin Source: src/util/pointProjector.js (function($, cornerstone, cornerstoneTools) { @@ -10373,6 +10762,31 @@ Display scroll progress bar across bottom of image. // End Source; src/util/pointProjector.js +// Begin Source: src/util/requestAnimFrame.js +(function(cornerstoneTools) { + + 'use strict'; + + function requestFrame(callback) { + window.setTimeout(callback, 1000 / 60); + } + + function requestAnimFrame(callback) { + return window.requestAnimationFrame(callback) || + window.webkitRequestAnimationFrame(callback) || + window.mozRequestAnimationFrame(callback) || + window.oRequestAnimationFrame(callback) || + window.msRequestAnimationFrame(callback) || + requestFrame(callback); + } + + // Module exports + cornerstoneTools.requestAnimFrame = requestAnimFrame; + +})(cornerstoneTools); + +// End Source; src/util/requestAnimFrame.js + // Begin Source: src/util/scroll.js (function(cornerstone, cornerstoneTools) { @@ -10407,7 +10821,7 @@ Display scroll progress bar across bottom of image. function scrollToIndex(element, newImageIdIndex) { var toolData = cornerstoneTools.getToolState(element, 'stack'); - if (toolData === undefined || toolData.data === undefined || toolData.data.length === 0) { + if (!toolData || !toolData.data || !toolData.data.length) { return; } @@ -10421,9 +10835,9 @@ Display scroll progress bar across bottom of image. var startLoadingHandler = cornerstoneTools.loadHandlerManager.getStartLoadHandler(); var endLoadingHandler = cornerstoneTools.loadHandlerManager.getEndLoadHandler(); var errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler(); + var viewport = cornerstone.getViewport(element); function doneCallback(image) { - //console.log('interaction done: ' + image.imageId); if (stackData.currentImageIdIndex === newImageIdIndex) { cornerstone.displayImage(element, image, viewport); if (endLoadingHandler) { @@ -10453,7 +10867,6 @@ Display scroll progress bar across bottom of image. }; stackData.currentImageIdIndex = newImageIdIndex; - var viewport = cornerstone.getViewport(element); var newImageId = stackData.imageIds[newImageIdIndex]; // Retry image loading in cases where previous image promise @@ -10467,9 +10880,9 @@ Display scroll progress bar across bottom of image. } var requestPoolManager = cornerstoneTools.requestPoolManager; + var type = 'interaction'; - - cornerstoneTools.requestPoolManager.clearRequestStack(type); + requestPoolManager.clearRequestStack(type); // Convert the preventCache value in stack data to a boolean var preventCache = !!stackData.preventCache; diff --git a/Packages/cornerstone/client/cornerstoneWADOImageLoader.js b/Packages/cornerstone/client/cornerstoneWADOImageLoader.js index 9986a745e..9cb4c493a 100644 --- a/Packages/cornerstone/client/cornerstoneWADOImageLoader.js +++ b/Packages/cornerstone/client/cornerstoneWADOImageLoader.js @@ -1,4 +1,4 @@ -/*! cornerstone-wado-image-loader - v0.9.0 - 2016-02-08 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneWADOImageLoader */ +/*! cornerstone-wado-image-loader - v0.9.1 - 2016-02-09 | (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: @@ -4294,7 +4294,7 @@ var JpegImage = (function jpegImage() { "use strict"; // module exports - cornerstoneWADOImageLoader.version = '0.9.0'; + cornerstoneWADOImageLoader.version = '0.9.1'; }(cornerstoneWADOImageLoader)); (function ($, cornerstone, cornerstoneWADOImageLoader) { diff --git a/Packages/cornerstone/client/hammer.js b/Packages/cornerstone/client/hammer.js index 734e9205e..bbabf7378 100644 --- a/Packages/cornerstone/client/hammer.js +++ b/Packages/cornerstone/client/hammer.js @@ -1,4 +1,4 @@ -/*! Hammer.JS - v2.0.4 - 2015-09-25 +/*! Hammer.JS - v2.0.6 - 2015-12-23 * http://hammerjs.github.io/ * * Copyright (c) 2015 Jorik Tangelder; @@ -6,7 +6,7 @@ (function(window, document, exportName, undefined) { 'use strict'; -var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; +var VENDOR_PREFIXES = ['', 'webkit', 'Moz', 'MS', 'ms', 'o']; var TEST_ELEMENT = document.createElement('div'); var TYPE_FUNCTION = 'function'; @@ -71,15 +71,69 @@ function each(obj, iterator, context) { } } +/** + * wrap a method with a deprecation warning and stack trace + * @param {Function} method + * @param {String} name + * @param {String} message + * @returns {Function} A new function wrapping the supplied method. + */ +function deprecate(method, name, message) { + var deprecationMessage = 'DEPRECATED METHOD: ' + name + '\n' + message + ' AT \n'; + return function() { + var e = new Error('get-stack-trace'); + var stack = e && e.stack ? e.stack.replace(/^[^\(]+?[\n$]/gm, '') + .replace(/^\s+at\s+/gm, '') + .replace(/^Object.\s*\(/gm, '{anonymous}()@') : 'Unknown Stack Trace'; + + var log = window.console && (window.console.warn || window.console.log); + if (log) { + log.call(window.console, deprecationMessage, stack); + } + return method.apply(this, arguments); + }; +} + +/** + * extend object. + * means that properties in dest will be overwritten by the ones in src. + * @param {Object} target + * @param {...Object} objects_to_assign + * @returns {Object} target + */ +var assign; +if (typeof Object.assign !== 'function') { + assign = function assign(target) { + if (target === undefined || target === null) { + throw new TypeError('Cannot convert undefined or null to object'); + } + + var output = Object(target); + for (var index = 1; index < arguments.length; index++) { + var source = arguments[index]; + if (source !== undefined && source !== null) { + for (var nextKey in source) { + if (source.hasOwnProperty(nextKey)) { + output[nextKey] = source[nextKey]; + } + } + } + } + return output; + }; +} else { + assign = Object.assign; +} + /** * extend object. * means that properties in dest will be overwritten by the ones in src. * @param {Object} dest * @param {Object} src - * @param {Boolean} [merge] + * @param {Boolean=false} [merge] * @returns {Object} dest */ -function extend(dest, src, merge) { +var extend = deprecate(function extend(dest, src, merge) { var keys = Object.keys(src); var i = 0; while (i < keys.length) { @@ -89,7 +143,7 @@ function extend(dest, src, merge) { i++; } return dest; -} +}, 'extend', 'Use `assign`.'); /** * merge the values from src in the dest. @@ -98,9 +152,9 @@ function extend(dest, src, merge) { * @param {Object} src * @returns {Object} dest */ -function merge(dest, src) { +var merge = deprecate(function merge(dest, src) { return extend(dest, src, true); -} +}, 'merge', 'Use `assign`.'); /** * simple class inheritance @@ -117,7 +171,7 @@ function inherit(child, base, properties) { childP._super = baseP; if (properties) { - extend(childP, properties); + assign(childP, properties); } } @@ -798,7 +852,7 @@ var POINTER_ELEMENT_EVENTS = 'pointerdown'; var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; // IE10 has prefixed support, and case-sensitive -if (window.MSPointerEvent) { +if (window.MSPointerEvent && !window.PointerEvent) { POINTER_ELEMENT_EVENTS = 'MSPointerDown'; POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; } @@ -1185,6 +1239,11 @@ TouchAction.prototype = { } } + if (hasPanX && hasPanY) { + // `pan-x pan-y` means browser handles all scrolling/panning, do not prevent + return; + } + if (hasNone || (hasPanY && direction & DIRECTION_HORIZONTAL) || (hasPanX && direction & DIRECTION_VERTICAL)) { @@ -1216,9 +1275,12 @@ function cleanTouchActions(actions) { var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); - // pan-x and pan-y can be combined + // if both pan-x and pan-y are set (different recognizers + // for different directions, e.g. horizontal pan but vertical swipe?) + // we need none (as otherwise with pan-x pan-y combined none of these + // recognizers will work, since the browser would handle all panning if (hasPanX && hasPanY) { - return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; + return TOUCH_ACTION_NONE; } // pan-x OR pan-y @@ -1276,13 +1338,11 @@ var STATE_FAILED = 32; * @param {Object} options */ function Recognizer(options) { - // make sure, options are copied over to a new object to prevent leaking it outside - options = extend({}, options || {}); + this.options = assign({}, this.defaults, options || {}); this.id = uniqueId(); this.manager = null; - this.options = merge(options, this.defaults); // default is enable true this.options.enable = ifUndefined(this.options.enable, true); @@ -1306,7 +1366,7 @@ Recognizer.prototype = { * @return {Recognizer} */ set: function(options) { - extend(this.options, options); + assign(this.options, options); // also update the touchAction, in case something changed about the directions/enabled state this.manager && this.manager.touchAction.update(); @@ -1467,7 +1527,7 @@ Recognizer.prototype = { recognize: function(inputData) { // make a new copy of the inputData // so we can change the inputData without messing up the other recognizers - var inputDataClone = extend({}, inputData); + var inputDataClone = assign({}, inputData); // is is enabled and allow recognizing? if (!boolOrFn(this.options.enable, [this, inputDataClone])) { @@ -1654,10 +1714,10 @@ inherit(PanRecognizer, AttrRecognizer, { var direction = this.options.direction; var actions = []; if (direction & DIRECTION_HORIZONTAL) { - actions.push(TOUCH_ACTION_PAN_X); + actions.push(TOUCH_ACTION_PAN_Y); } if (direction & DIRECTION_VERTICAL) { - actions.push(TOUCH_ACTION_PAN_Y); + actions.push(TOUCH_ACTION_PAN_X); } return actions; }, @@ -1765,8 +1825,8 @@ inherit(PressRecognizer, Recognizer, { defaults: { event: 'press', pointers: 1, - time: 500, // minimal time of the pointer to be pressed - threshold: 5 // a minimal movement is ok, but keep it low + time: 251, // minimal time of the pointer to be pressed + threshold: 9 // a minimal movement is ok, but keep it low }, getTouchAction: function() { @@ -1864,7 +1924,7 @@ inherit(SwipeRecognizer, AttrRecognizer, { defaults: { event: 'swipe', threshold: 10, - velocity: 0.65, + velocity: 0.3, direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, pointers: 1 }, @@ -1936,7 +1996,7 @@ inherit(TapRecognizer, Recognizer, { taps: 1, interval: 300, // max time between the multi-tap taps time: 250, // max time of the pointer to be down (like finger on the screen) - threshold: 2, // a minimal movement is ok, but keep it low + threshold: 9, // a minimal movement is ok, but keep it low posThreshold: 10 // a multi-tap can be a bit off the initial position }, @@ -2018,7 +2078,7 @@ inherit(TapRecognizer, Recognizer, { }); /** - * Simple way to create an manager with a default set of recognizers. + * Simple way to create a manager with a default set of recognizers. * @param {HTMLElement} element * @param {Object} [options] * @constructor @@ -2032,7 +2092,7 @@ function Hammer(element, options) { /** * @const {string} */ -Hammer.VERSION = '2.0.4'; +Hammer.VERSION = '2.0.6'; /** * default settings @@ -2156,9 +2216,8 @@ var FORCED_STOP = 2; * @constructor */ function Manager(element, options) { - options = options || {}; + this.options = assign({}, Hammer.defaults, options || {}); - this.options = merge(options, Hammer.defaults); this.options.inputTarget = this.options.inputTarget || element; this.handlers = {}; @@ -2171,7 +2230,7 @@ function Manager(element, options) { toggleCssProps(this, true); - each(options.recognizers, function(item) { + each(this.options.recognizers, function(item) { var recognizer = this.add(new (item[0])(item[1])); item[2] && recognizer.recognizeWith(item[2]); item[3] && recognizer.requireFailure(item[3]); @@ -2185,7 +2244,7 @@ Manager.prototype = { * @returns {Manager} */ set: function(options) { - extend(this.options, options); + assign(this.options, options); // Options that need a little more setup if (options.touchAction) { @@ -2319,11 +2378,19 @@ Manager.prototype = { return this; } - var recognizers = this.recognizers; recognizer = this.get(recognizer); - recognizers.splice(inArray(recognizers, recognizer), 1); - this.touchAction.update(); + // let's make sure this recognizer exists + if (recognizer) { + var recognizers = this.recognizers; + var index = inArray(recognizers, recognizer); + + if (index !== -1) { + recognizers.splice(index, 1); + this.touchAction.update(); + } + } + return this; }, @@ -2354,7 +2421,7 @@ Manager.prototype = { if (!handler) { delete handlers[event]; } else { - handlers[event].splice(inArray(handlers[event], handler), 1); + handlers[event] && handlers[event].splice(inArray(handlers[event], handler), 1); } }); return this; @@ -2430,7 +2497,7 @@ function triggerDomEvent(event, data) { data.target.dispatchEvent(gestureEvent); } -extend(Hammer, { +assign(Hammer, { INPUT_START: INPUT_START, INPUT_MOVE: INPUT_MOVE, INPUT_END: INPUT_END, @@ -2477,12 +2544,18 @@ extend(Hammer, { each: each, merge: merge, extend: extend, + assign: assign, inherit: inherit, bindFn: bindFn, prefixed: prefixed }); -if (typeof define == TYPE_FUNCTION && define.amd) { +// this prevents errors when Hammer is loaded in the presence of an AMD +// style loader but by script tag, not by the loader. +var freeGlobal = (typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : {})); // jshint ignore:line +freeGlobal.Hammer = Hammer; + +if (typeof define === 'function' && define.amd) { define(function() { return Hammer; }); diff --git a/Packages/lesiontracker/client/compatibility/lesionTool.js b/Packages/lesiontracker/client/compatibility/lesionTool.js index 409f6a96a..ce0b1c471 100644 --- a/Packages/lesiontracker/client/compatibility/lesionTool.js +++ b/Packages/lesiontracker/client/compatibility/lesionTool.js @@ -32,7 +32,77 @@ //doneCallback(prompt('Change your lesion location:')); } - ///////// BEGIN ACTIVE TOOL /////// + function createNewMeasurement(mouseEventData) { + var imageId = mouseEventData.image.imageId; + + // Get studyInstanceUid + var study = cornerstoneTools.metaData.get('study', imageId); + var studyInstanceUid = study.studyInstanceUid; + var patientId = study.patientId; + + // Get seriesInstanceUid + var series = cornerstoneTools.metaData.get('series', imageId); + var seriesInstanceUid = series.seriesInstanceUid; + + // Create the measurement data for this tool with the end handle activated + var measurementData = { + visible: true, + active: true, + handles: { + start: { + x: mouseEventData.currentPoints.image.x, + y: mouseEventData.currentPoints.image.y, + highlight: true, + active: false, + drawnIndependently: true, + index: 0 + }, + end: { + x: mouseEventData.currentPoints.image.x, + y: mouseEventData.currentPoints.image.y, + highlight: true, + active: true, + drawnIndependently: true, + index: 1 + }, + textBox: { + x: mouseEventData.currentPoints.image.x - 50, + y: mouseEventData.currentPoints.image.y - 70, + active: false, + movesIndependently: false, + drawnIndependently: true, + allowedOutsideImage: true, + hasBoundingBox: true + }, + perpendicularStart: { + x: mouseEventData.currentPoints.image.x, + y: mouseEventData.currentPoints.image.y, + highlight: true, + active: false, + locked: true, // If perpendicular line is connected to long-line + drawnIndependently: true, + index: 2 + }, + perpendicularEnd: { + x: mouseEventData.currentPoints.image.x, + y: mouseEventData.currentPoints.image.y, + highlight: true, + active: false, + drawnIndependently: true, + index: 3 + } + }, + imageId: imageId, + seriesInstanceUid: seriesInstanceUid, + studyInstanceUid: studyInstanceUid, + patientId: patientId, + longestDiameter: 0, + shortestDiameter: 0, + isDeleted: false, + isTarget: true + }; + return measurementData; + } function addNewMeasurement(mouseEventData) { var element = mouseEventData.element; @@ -53,9 +123,9 @@ var eventData = { mouseButtonMask: mouseEventData.which }; - var config = cornerstoneTools.lesion.getConfiguration(); // Set lesion number and lesion name + var config = cornerstoneTools.lesion.getConfiguration(); if (measurementData.lesionNumber === undefined) { config.setLesionNumberCallback(measurementData, mouseEventData, doneCallback); } @@ -128,6 +198,12 @@ var measurementData = createNewMeasurement(touchEventData); + // Set lesion number and lesion name + var config = cornerstoneTools.lesion.getConfiguration(); + 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); @@ -139,9 +215,12 @@ cornerstone.updateImage(element); cornerstoneTools.moveNewHandleTouch(touchEventData, toolType, measurementData, measurementData.handles.end, function() { - if (cornerstoneTools.anyHandlesOutsideImage(touchEventData, measurementData.handles)) { + if (cancelled || cornerstoneTools.anyHandlesOutsideImage(touchEventData, measurementData.handles)) { // delete the measurement cornerstoneTools.removeToolState(element, toolType, measurementData); + } else { + // Set lesionMeasurementData Session + config.getLesionLocationCallback(measurementData, touchEventData, doneCallback); } // perpendicular line is not connected to long-line @@ -154,80 +233,6 @@ }); } - function createNewMeasurement(mouseEventData) { - var imageId = mouseEventData.image.imageId; - - // Get studyInstanceUid - var study = cornerstoneTools.metaData.get('study', imageId); - var studyInstanceUid = study.studyInstanceUid; - var patientId = study.patientId; - - // Get seriesInstanceUid - var series = cornerstoneTools.metaData.get('series', imageId); - var seriesInstanceUid = series.seriesInstanceUid; - - // Create the measurement data for this tool with the end handle activated - var measurementData = { - visible: true, - active: true, - handles: { - start: { - x: mouseEventData.currentPoints.image.x, - y: mouseEventData.currentPoints.image.y, - highlight: true, - active: false, - drawnIndependently: true, - index: 0 - }, - end: { - x: mouseEventData.currentPoints.image.x, - y: mouseEventData.currentPoints.image.y, - highlight: true, - active: true, - drawnIndependently: true, - index: 1 - }, - textBox: { - x: mouseEventData.currentPoints.image.x - 50, - y: mouseEventData.currentPoints.image.y - 70, - pointNearHandle: pointNearTextBox, - active: false, - movesIndependently: false, - drawnIndependently: true, - allowedOutsideImage: true - }, - perpendicularStart: { - x: mouseEventData.currentPoints.image.x, - y: mouseEventData.currentPoints.image.y, - highlight: true, - active: false, - locked: true, // If perpendicular line is connected to long-line - drawnIndependently: true, - index: 2 - }, - perpendicularEnd: { - x: mouseEventData.currentPoints.image.x, - y: mouseEventData.currentPoints.image.y, - highlight: true, - active: false, - drawnIndependently: true, - index: 3 - } - }, - imageId: imageId, - seriesInstanceUid: seriesInstanceUid, - studyInstanceUid: studyInstanceUid, - patientId: patientId, - measurementText: 0, - widthMeasurement: 0, - perpendicularMeasurement: 0, - isDeleted: false, - isTarget: true - }; - return measurementData; - } - ///////// END ACTIVE TOOL /////// - function pointNearTool(element, data, coords) { var lineSegment = { start: cornerstone.pixelToCanvas(element, data.handles.start), @@ -235,7 +240,7 @@ }; var distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords); - if (pointNearTextBox(element, data.handles.textBox, coords)) { + if (cornerstoneTools.pointInsideBoundingBox(data.handles.textBox, coords)) { return true; } @@ -246,14 +251,6 @@ return (distanceToPoint < 5); } - function pointNearTextBox(element, handle, coords) { - if (!handle.boundingBox) { - return; - } - - return cornerstoneMath.point.insideRect(coords, handle.boundingBox); - } - function pointNearPerpendicular(element, handles, coords) { var lineSegment = { start: cornerstone.pixelToCanvas(element, handles.perpendicularStart), @@ -265,7 +262,6 @@ // Move long-axis start point function perpendicularBothFixedLeft(eventData, data) { - var longLine = { start: { x: data.handles.start.x, @@ -273,7 +269,7 @@ }, end: { x: data.handles.end.x, - y: data.handles. end.y + y: data.handles.end.y } }; @@ -284,7 +280,7 @@ }, end: { x: data.handles.perpendicularEnd.x, - y: data.handles. perpendicularEnd.y + y: data.handles.perpendicularEnd.y } }; @@ -322,7 +318,6 @@ // Move long-axis end point function perpendicularBothFixedRight(eventData, data) { - var longLine = { start: { x: data.handles.start.x, @@ -330,7 +325,7 @@ }, end: { x: data.handles.end.x, - y: data.handles. end.y + y: data.handles.end.y } }; @@ -341,7 +336,7 @@ }, end: { x: data.handles.perpendicularEnd.x, - y: data.handles. perpendicularEnd.y + y: data.handles.perpendicularEnd.y } }; @@ -1046,8 +1041,8 @@ data.handles.textBox.boundingBox = boundingBox; // Set measurement text to show lesion table - data.measurementText = length.toFixed(1); - data.widthMeasurement = width.toFixed(1); + data.longestDiameter = length.toFixed(1); + data.shortestDiameter = width.toFixed(1); context.restore(); } diff --git a/Packages/lesiontracker/client/compatibility/nonTargetTool.js b/Packages/lesiontracker/client/compatibility/nonTargetTool.js index 798adb753..8f1146add 100644 --- a/Packages/lesiontracker/client/compatibility/nonTargetTool.js +++ b/Packages/lesiontracker/client/compatibility/nonTargetTool.js @@ -141,18 +141,18 @@ textBox: { x: mouseEventData.currentPoints.image.x - 50, y: mouseEventData.currentPoints.image.y - 50, - pointNearHandle: pointNearTextBox, active: false, movesIndependently: false, drawnIndependently: true, - allowedOutsideImage: true + allowedOutsideImage: true, + hasBoundingBox: true } }, imageId: imageId, seriesInstanceUid: seriesInstanceUid, studyInstanceUid: studyInstanceUid, patientId: patientId, - measurementText: '', + response: '', isTarget: false }; @@ -167,21 +167,13 @@ }; var distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords); - if (pointNearTextBox(element, data.handles.textBox, coords)) { + if (cornerstoneTools.pointInsideBoundingBox(data.handles.textBox, coords)) { return true; } return distanceToPoint < 25; } - function pointNearTextBox(element, handle, coords) { - if (!handle.boundingBox) { - return; - } - - return cornerstoneMath.point.insideRect(coords, handle.boundingBox); - } - ///////// BEGIN IMAGE RENDERING /////// function onImageRendered(e, eventData) { var element = eventData.element; diff --git a/Packages/lesiontracker/client/components/lesionTable/lesionTable.js b/Packages/lesiontracker/client/components/lesionTable/lesionTable.js index 5887e48a2..92c61327e 100644 --- a/Packages/lesiontracker/client/components/lesionTable/lesionTable.js +++ b/Packages/lesiontracker/client/components/lesionTable/lesionTable.js @@ -79,7 +79,7 @@ Template.lesionTable.onRendered(function() { return; } - console.log('ViewerData changed, check for displayed timepoints'); + log.info('ViewerData changed, check for displayed timepoints'); // Get study dates of imageViewerViewport elements var loadedStudyDates = { diff --git a/Packages/lesiontracker/client/components/nonTargetLesionDialog/nonTargetLesionDialog.js b/Packages/lesiontracker/client/components/nonTargetLesionDialog/nonTargetLesionDialog.js index 6f4c96bb5..bd2a5baf0 100644 --- a/Packages/lesiontracker/client/components/nonTargetLesionDialog/nonTargetLesionDialog.js +++ b/Packages/lesiontracker/client/components/nonTargetLesionDialog/nonTargetLesionDialog.js @@ -290,8 +290,7 @@ Template.nonTargetLesionDialog.events({ /// Set the isTarget value to true, since this is the target-lesion dialog callback measurementData.isTarget = false; - // measurementText is set from location response list - measurementData.measurementText = responseOptionId; + // Response is set from location response list measurementData.response = responseOptionId; // Adds lesion data to timepoints array diff --git a/Packages/lesiontracker/client/components/nonTargetResponseDialog/nonTargetResponseDialog.js b/Packages/lesiontracker/client/components/nonTargetResponseDialog/nonTargetResponseDialog.js index 6559eabf4..2bf501e0d 100644 --- a/Packages/lesiontracker/client/components/nonTargetResponseDialog/nonTargetResponseDialog.js +++ b/Packages/lesiontracker/client/components/nonTargetResponseDialog/nonTargetResponseDialog.js @@ -109,8 +109,7 @@ Template.nonTargetResponseDialog.events({ /// Set the isTarget value to true, since this is the target-lesion dialog callback measurementData.isTarget = false; - // measurementText is set from location response list - measurementData.measurementText = responseOptionId; + // Response is set from location response list measurementData.response = responseOptionId; // Adds lesion data to timepoints array diff --git a/Packages/lesiontracker/lib/LesionManager.js b/Packages/lesiontracker/lib/LesionManager.js index d6c8ae417..cba49ddb0 100644 --- a/Packages/lesiontracker/lib/LesionManager.js +++ b/Packages/lesiontracker/lib/LesionManager.js @@ -47,8 +47,8 @@ function updateLesionData(lesionData) { }; if (lesionData.isTarget === true) { - timepointData.shortestDiameter = lesionData.widthMeasurement; - timepointData.longestDiameter = lesionData.measurementText; + timepointData.shortestDiameter = lesionData.shortestDiameter; + timepointData.longestDiameter = lesionData.longestDiameter; } else { timepointData.response = lesionData.response; } @@ -79,27 +79,15 @@ function updateLesionData(lesionData) { measurement.timepoints[timepoint.timepointId] = timepointData; // Set a flag to prevent duplication of toolData - measurement.toolDataInsertedManually = true; + measurement.clientId = ClientId; // 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?) - console.log('LesionManager inserting Measurement'); + log.info('LesionManager inserting Measurement'); measurement.id = Measurements.insert(measurement); - - // Update the database entry so it can be re-added next time the study is loaded - Measurements.update(measurement.id, { - $set: { - toolDataInsertedManually: false - } - }, function(error) { - if (error) { - log.warn(error); - } - OHIF.viewer.manuallyModifyingMeasurement = false; - }); } else { lesionData.id = existingMeasurement._id; lesionData.isNodal = existingMeasurement.isNodal; @@ -111,16 +99,12 @@ function updateLesionData(lesionData) { // Update timepoints from lesion data existingMeasurement.timepoints[timepoint.timepointId] = timepointData; - console.log('LesionManager updating Measurement'); + log.info('LesionManager updating Measurement'); Measurements.update(existingMeasurement._id, { $set: { - timepoints: existingMeasurement.timepoints + timepoints: existingMeasurement.timepoints, + clientId: ClientId } - }, function(error) { - if (error) { - log.warn(error); - } - OHIF.viewer.manuallyModifyingMeasurement = false; }); } } diff --git a/Packages/lesiontracker/lib/handleMeasurementAdded.js b/Packages/lesiontracker/lib/handleMeasurementAdded.js index 09f0107f9..b5395ba6f 100644 --- a/Packages/lesiontracker/lib/handleMeasurementAdded.js +++ b/Packages/lesiontracker/lib/handleMeasurementAdded.js @@ -5,7 +5,6 @@ handleMeasurementAdded = function(e, eventData) { case 'nonTarget': case 'lesion': log.info('CornerstoneToolsMeasurementAdded'); - OHIF.viewer.manuallyModifyingMeasurement = true; LesionManager.updateLesionData(measurementData); TrialResponseCriteria.validateDelayed(measurementData); break; diff --git a/Packages/lesiontracker/lib/handleMeasurementModified.js b/Packages/lesiontracker/lib/handleMeasurementModified.js index e25b11a34..1384c2345 100644 --- a/Packages/lesiontracker/lib/handleMeasurementModified.js +++ b/Packages/lesiontracker/lib/handleMeasurementModified.js @@ -5,7 +5,6 @@ handleMeasurementModified = function(e, eventData) { case 'nonTarget': case 'lesion': log.info('CornerstoneToolsMeasurementModified'); - OHIF.viewer.manuallyModifyingMeasurement = true; LesionManager.updateLesionData(measurementData); TrialResponseCriteria.validateDelayed(measurementData); break; diff --git a/Packages/lesiontracker/lib/removeToolDataWithMeasurementId.js b/Packages/lesiontracker/lib/removeToolDataWithMeasurementId.js index aa49c3ebb..864de1c0f 100644 --- a/Packages/lesiontracker/lib/removeToolDataWithMeasurementId.js +++ b/Packages/lesiontracker/lib/removeToolDataWithMeasurementId.js @@ -21,8 +21,8 @@ removeToolDataWithMeasurementId = function(imageId, toolType, measurementId) { } }); - console.log("Removing Indices: "); - console.log(toRemove); + log.info("Removing Indices: "); + log.info(toRemove); // If any toolData entries need to be removed, splice them from // the toolData array diff --git a/Packages/lesiontracker/lib/syncMeasurementAndToolData.js b/Packages/lesiontracker/lib/syncMeasurementAndToolData.js index 61718957c..8386dbc76 100644 --- a/Packages/lesiontracker/lib/syncMeasurementAndToolData.js +++ b/Packages/lesiontracker/lib/syncMeasurementAndToolData.js @@ -1,5 +1,5 @@ syncMeasurementAndToolData = function(measurement) { - console.log('syncMeasurementAndToolData'); + log.info('syncMeasurementAndToolData'); // Check what toolType we should be adding this to, based on the isTarget value // of the stored Measurement @@ -10,54 +10,48 @@ syncMeasurementAndToolData = function(measurement) { var timepointData = measurement.timepoints[key]; var imageId = timepointData.imageId; + // Sync the Cornerstone ToolData with this Measurement's timepoint-specific data syncTimepointDataWithToolData(measurement, timepointData, imageId, toolType); }); }; function syncTimepointDataWithToolData(measurement, timepointData, imageId, toolType) { + // Get the global imageId-specific toolState from Cornerstone Tools var toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.toolState; + // If no tool state exists for this imageId, create an empty object to store it 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') { - return; - } + // Check if we already have toolData for this imageId and toolType + if (toolState[imageId][toolType] && + toolState[imageId][toolType].data && + toolState[imageId][toolType].data.length) { - 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]) { - var alreadyExists = false; + // If we have toolData, we should search it for any toolData + // related to the current Measurement var toolData = toolState[imageId][toolType].data; - if (!toolData.length) { - return; - } + // Create a flag so we know if we have successfully updated + // this Measurement's timepoint data in the toolData + var alreadyExists = false; + + // Loop through the toolData to search for this Measurement's + // timepoint data toolData.forEach(function(tool) { + // Break the loop if this isn't the Measurement we are looking for if (tool.id !== measurement._id) { return; } + // If we find the Measurement, set the flag to True alreadyExists = true; - // Update the toolData lesionNumber from the Measurement + // Update the toolData from the Measurement data and + // timepoint-specific data from this Measurement tool.lesionNumber = measurement.lesionNumber; tool.isTarget = measurement.isTarget; - tool.active = timepointData.active; tool.visible = timepointData.visible; tool.isDeleted = timepointData.isDeleted; @@ -65,31 +59,30 @@ function syncTimepointDataWithToolData(measurement, timepointData, imageId, tool return false; }); + // If we found the Measurement we intended to update, we can stop + // this function here if (alreadyExists === true) { return; } } else { + // If no toolData exists for this toolType, create an empty array to hold some toolState[imageId][toolType] = { data: [] }; } - // Create measurementData structure based on the lesion data at this timepoint - // We will add this into the toolData for this imageId - var measurementData = timepointData; - measurementData.isTarget = measurement.isTarget; - measurementData.lesionNumber = measurement.lesionNumber; - measurementData.measurementText = measurement.measurementText; - measurementData.isDeleted = measurement.isDeleted; - measurementData.location = measurement.location; - measurementData.locationUID = measurement.locationUID; - measurementData.patientId = measurement.patientId; - measurementData.visible = measurement.visible; - measurementData.active = measurement.active; - measurementData.uid = measurement.uid; - measurementData.id = measurement._id; + // If we have reached this point, it means we haven't found the Measurement we are + // looking for in the current toolData. This means we need to add it. - toolState[imageId][toolType].data.push(measurementData); + // First, create the measurementData structure based on the lesion data at this timepoint. + var tool = timepointData; + tool.lesionNumber = measurement.lesionNumber; + tool.isTarget = measurement.isTarget; + tool.location = measurement.location; + tool.locationUID = measurement.locationUID; + tool.patientId = measurement.patientId; + tool.id = measurement._id; - TrialResponseCriteria.validateSingleMeasurement(measurementData); + // Add the measurementData into the toolData for this imageId + toolState[imageId][toolType].data.push(tool); } diff --git a/Packages/viewerbase/client/collections.js b/Packages/viewerbase/client/collections.js new file mode 100644 index 000000000..199657f79 --- /dev/null +++ b/Packages/viewerbase/client/collections.js @@ -0,0 +1,4 @@ +ViewerStudies = new Meteor.Collection(null); +ViewerStudies._debugName = 'ViewerStudies'; + +ClientId = Random.id(); diff --git a/Packages/viewerbase/client/components/viewer/imageViewerViewport/imageViewerViewport.js b/Packages/viewerbase/client/components/viewer/imageViewerViewport/imageViewerViewport.js index 8f78a6a9c..a80462407 100644 --- a/Packages/viewerbase/client/components/viewer/imageViewerViewport/imageViewerViewport.js +++ b/Packages/viewerbase/client/components/viewer/imageViewerViewport/imageViewerViewport.js @@ -322,7 +322,7 @@ function loadSeriesIntoViewport(data, templateData) { } // Temporary until we have a real window manager with events for series/study changed - console.log('Set NewSeriesLoaded'); + log.info('Set NewSeriesLoaded'); Session.set('NewSeriesLoaded', Random.id()); // Run any renderedCallback that exists in the data context @@ -412,7 +412,7 @@ Meteor.startup(function() { }); Template.imageViewerViewport.onCreated(function() { - console.log('imageViewerViewport onCreated'); + log.info('imageViewerViewport onCreated'); }); Template.imageViewerViewport.onRendered(function() { diff --git a/Packages/viewerbase/lib/WLPresets.js b/Packages/viewerbase/lib/WLPresets.js index 7e1841fa6..0230ea60e 100644 --- a/Packages/viewerbase/lib/WLPresets.js +++ b/Packages/viewerbase/lib/WLPresets.js @@ -1,5 +1,5 @@ applyWLPreset = function(presetName, element) { - console.log("Applying WL Preset: " + presetName); + log.info("Applying WL Preset: " + presetName); var viewport = cornerstone.getViewport(element); if (presetName === 'Default') { diff --git a/Packages/viewerbase/lib/getWADORSImageId.js b/Packages/viewerbase/lib/getWADORSImageId.js index 0195dda67..61766fc99 100644 --- a/Packages/viewerbase/lib/getWADORSImageId.js +++ b/Packages/viewerbase/lib/getWADORSImageId.js @@ -55,6 +55,6 @@ getWADORSImageId = function(instance) { var imageId = cornerstoneWADOImageLoader.imageManager.add(image); - console.log('WADO-RS ImageID: ' + imageId); + log.info('WADO-RS ImageID: ' + imageId); return imageId; }; diff --git a/Packages/viewerbase/package.js b/Packages/viewerbase/package.js index 14d9f98e0..8ece9f8b2 100644 --- a/Packages/viewerbase/package.js +++ b/Packages/viewerbase/package.js @@ -27,6 +27,10 @@ Package.onUse(function (api) { // TODO= Find a meteor package for this api.addFiles('client/compatibility/jquery.hotkeys.js', 'client'); + + // ---------- Collections ---------- + api.addFiles('client/collections.js', 'client'); + // ---------- Components ---------- // Basic components @@ -148,8 +152,12 @@ Package.onUse(function (api) { api.export('toolManager', 'client'); api.export('WindowManager', 'client'); - // Global data object + // Global objects api.export('OHIF', 'client'); + api.export('ClientId', 'client'); + + // Collections + api.export('ViewerStudies', 'client'); // UI Helpers api.addFiles('lib/helpers/formatDA.js', 'client'); diff --git a/Packages/viewerbase/server/services/dimse/retrieveMetadata.js b/Packages/viewerbase/server/services/dimse/retrieveMetadata.js index a92ea23ac..9a2520f6b 100755 --- a/Packages/viewerbase/server/services/dimse/retrieveMetadata.js +++ b/Packages/viewerbase/server/services/dimse/retrieveMetadata.js @@ -26,7 +26,7 @@ function getSourceImageInstanceUid(instance) { * @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}} */ function resultDataToStudyMetadata(studyInstanceUid, resultData) { - console.log('resultDataToStudyMetadata'); + log.info('resultDataToStudyMetadata'); var seriesMap = {}; var seriesList = []; diff --git a/Packages/viewerbase/server/services/dimse/studies.js b/Packages/viewerbase/server/services/dimse/studies.js index 74b172e9f..4ebb3d204 100755 --- a/Packages/viewerbase/server/services/dimse/studies.js +++ b/Packages/viewerbase/server/services/dimse/studies.js @@ -32,7 +32,7 @@ function resultDataToStudies(resultData) { Services.DIMSE.Studies = function(filter) { - console.log('Services.DIMSE.Studies'); + log.info('Services.DIMSE.Studies'); var parameters = { 0x00100010: filter.patientName, 0x00100020: filter.patientId, diff --git a/Packages/worklist/client/components/worklistResult/worklistResult.js b/Packages/worklist/client/components/worklistResult/worklistResult.js index d5102503a..2df880305 100644 --- a/Packages/worklist/client/components/worklistResult/worklistResult.js +++ b/Packages/worklist/client/components/worklistResult/worklistResult.js @@ -116,7 +116,6 @@ function search() { } Template.worklistResult.onCreated(function() { - console.log('WorklistResult onCreated!'); var self = this; if (Worklist.subscriptions) { Worklist.subscriptions.forEach(function(collectionName) { @@ -125,10 +124,6 @@ Template.worklistResult.onCreated(function() { } }); -Template.worklistResult.onDestroyed(function() { - console.log('WorklistResult onDestroyed!'); -}); - Template.worklistResult.events({ 'keydown input': function(e) { if (e.which === 13) { // Enter