import { Meteor } from 'meteor/meteor'; import { Session } from 'meteor/session'; import { Random } from 'meteor/random'; import { $ } from 'meteor/jquery'; import { OHIF } from 'meteor/ohif:core'; import { getFrameOfReferenceUID } from './getFrameOfReferenceUID'; import { updateCrosshairsSynchronizer } from './updateCrosshairsSynchronizer'; import { crosshairsSynchronizers } from './crosshairsSynchronizers'; import { annotateTextUtils } from './annotateTextUtils'; import { textMarkerUtils } from './textMarkerUtils'; import { isTouchDevice } from './helpers/isTouchDevice'; let defaultTool = { left: 'wwwc', right: 'zoom', middle: 'pan' }; let activeTool; let defaultMouseButtonTools; let tools = {}; let gestures = { zoomTouchPinch: { enabled: true }, panMultiTouch: { enabled: true }, stackScrollMultiTouch: { enabled: true }, doubleTapZoom: { enabled: true } }; let toolDefaultStates = { activate: [], deactivate: ['length', 'angle', 'annotate', 'ellipticalRoi', 'rectangleRoi', 'spine'], enable: [], disable: [], disabledToolButtons: [], shadowConfig: { shadow: false, shadowColor: '#000000', shadowOffsetX: 0, shadowOffsetY: 0 }, textBoxConfig: { centering: { x: true, y: true } } }; let initialized = false; /** * Exported "toolManager" Singleton */ export const toolManager = { init() { toolManager.addTool('wwwc', { mouse: cornerstoneTools.wwwc, touch: cornerstoneTools.wwwcTouchDrag }); toolManager.addTool('zoom', { mouse: cornerstoneTools.zoom, touch: cornerstoneTools.zoomTouchDrag }); toolManager.addTool('wwwcRegion', { mouse: cornerstoneTools.wwwcRegion, touch: cornerstoneTools.wwwcRegionTouch }); toolManager.addTool('dragProbe', { mouse: cornerstoneTools.dragProbe, touch: cornerstoneTools.dragProbeTouch }); toolManager.addTool('pan', { mouse: cornerstoneTools.pan, touch: cornerstoneTools.panTouchDrag }); toolManager.addTool('stackScroll', { mouse: cornerstoneTools.stackScroll, touch: cornerstoneTools.stackScrollTouchDrag }); toolManager.addTool('length', { mouse: cornerstoneTools.length, touch: cornerstoneTools.lengthTouch }); toolManager.addTool('angle', { mouse: cornerstoneTools.simpleAngle, touch: cornerstoneTools.simpleAngleTouch }); toolManager.addTool('magnify', { mouse: cornerstoneTools.magnify, touch: cornerstoneTools.magnifyTouchDrag }); toolManager.addTool('ellipticalRoi', { mouse: cornerstoneTools.ellipticalRoi, touch: cornerstoneTools.ellipticalRoiTouch }); toolManager.addTool('rectangleRoi', { mouse: cornerstoneTools.rectangleRoi, touch: cornerstoneTools.rectangleRoiTouch }); toolManager.addTool('annotate', { mouse: cornerstoneTools.arrowAnnotate, touch: cornerstoneTools.arrowAnnotateTouch }); toolManager.addTool('rotate', { mouse: cornerstoneTools.rotate, touch: cornerstoneTools.rotateTouchDrag }); toolManager.addTool('spine', { mouse: cornerstoneTools.textMarker, touch: cornerstoneTools.textMarkerTouch }); toolManager.addTool('crosshairs', { mouse: cornerstoneTools.crosshairs, touch: cornerstoneTools.crosshairsTouch }); // if a default tool is globally defined, make it the default tool... if (OHIF.viewer.defaultTool) { defaultTool.left = OHIF.viewer.defaultTool; } defaultMouseButtonTools = Meteor.settings && Meteor.settings.public && Meteor.settings.public.defaultMouseButtonTools; // Override default tool if defined in settings const defaultLeft = (defaultMouseButtonTools && defaultMouseButtonTools.left) || 'wwwc'; const defaultRight = (defaultMouseButtonTools && defaultMouseButtonTools.right) || 'zoom'; const defaultMiddle = (defaultMouseButtonTools && defaultMouseButtonTools.middle) || 'pan'; defaultTool = { left: defaultLeft, right: defaultRight, middle: defaultMiddle }; this.configureTools(); initialized = true; }, configureTools() { // Get Cornerstone Tools const { panMultiTouch, textStyle, toolStyle, toolColors, length, arrowAnnotate, zoom, ellipticalRoi, textMarker, magnify } = cornerstoneTools; // Set the configuration for the multitouch pan tool const multiTouchPanConfig = { testPointers: eventData => { return (eventData.numPointers >= 3); } }; panMultiTouch.setConfiguration(multiTouchPanConfig); // Set text box background color textStyle.setBackgroundColor('transparent'); // Set the tool font and font size // context.font = "[style] [variant] [weight] [size]/[line height] [font family]"; const fontFamily = 'Roboto, OpenSans, HelveticaNeue-Light, Helvetica Neue Light, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif'; textStyle.setFont('15px ' + fontFamily); // Set the tool width toolStyle.setToolWidth(2); // Set color for inactive tools toolColors.setToolColor('rgb(255, 255, 0)'); // Set color for active tools toolColors.setActiveColor('rgb(0, 255, 0)'); // Set shadow configuration const shadowConfig = toolManager.getToolDefaultStates().shadowConfig; // Get some tools config to not override them const lengthConfig = length.getConfiguration(); const ellipticalRoiConfig = ellipticalRoi.getConfiguration(); // Add shadow to length tool length.setConfiguration(Object.assign({}, lengthConfig, shadowConfig, { drawHandlesOnHover: true })); // Add shadow to length tool ellipticalRoi.setConfiguration(Object.assign({}, ellipticalRoiConfig, shadowConfig)); // Set the configuration values for the Text Marker (Spine Labelling) tool const $startFrom = $('#startFrom'); const $ascending = $('#ascending'); const textMarkerConfig = { markers: [ 'L5', 'L4', 'L3', 'L2', 'L1', // Lumbar spine 'T12', 'T11', 'T10', 'T9', 'T8', 'T7', // Thoracic spine 'T6', 'T5', 'T4', 'T3', 'T2', 'T1', 'C7', 'C6', 'C5', 'C4', 'C3', 'C2', 'C1', // Cervical spine ], current: $startFrom.val(), ascending: $ascending.is(':checked'), loop: true, changeTextCallback: textMarkerUtils.changeTextCallback, shadow: shadowConfig.shadow, shadowColor: shadowConfig.shadowColor, shadowOffsetX: shadowConfig.shadowOffsetX, shadowOffsetY: shadowConfig.shadowOffsetY }; textMarker.setConfiguration(textMarkerConfig); // Set the configuration values for the text annotation (Arrow) tool const annotateConfig = { getTextCallback: annotateTextUtils.getTextCallback, changeTextCallback: annotateTextUtils.changeTextCallback, drawHandles: false, arrowFirst: true }; arrowAnnotate.setConfiguration(annotateConfig); const zoomConfig = { minScale: 0.05, maxScale: 10 }; zoom.setConfiguration(zoomConfig); const magnifyConfig = { magnifySize: 300, magnificationLevel: 3 }; magnify.setConfiguration(magnifyConfig); }, /** * This function searches an object to return the keys that contain a specific value * * @param object {object} The object to be searched * @param value The value to be found * * @returns {array} The keys for which the object has the specified value */ getKeysByValue(object, value) { // http://stackoverflow.com/questions/9907419/javascript-object-get-key-by-value return Object.keys(object).filter(key => object[key] === value); }, configureLoadProcess() { // Whenever the CornerstoneImageLoadProgress is fired, identify which viewports // the "in-progress" image is to be displayed in. Then pass the percent complete // via the Meteor Session to the other templates to be displayed in the relevant viewports. $(cornerstone.events).on('CornerstoneImageLoadProgress', (e, eventData) => { viewportIndices = this.getKeysByValue(window.ViewportLoading, eventData.imageId); viewportIndices.forEach(viewportIndex => { Session.set('CornerstoneLoadProgress' + viewportIndex, eventData.percentComplete); }); const encodedId = OHIF.string.encodeId(eventData.imageId); Session.set('CornerstoneThumbnailLoadProgress' + encodedId, eventData.percentComplete); }); }, setGestures(newGestures) { gestures = newGestures; }, getGestures() { return gestures; }, addTool(name, base) { tools[name] = base; }, getTools() { return tools; }, setToolDefaultStates(states) { toolDefaultStates = states; }, getToolDefaultStates() { return toolDefaultStates; }, setActiveToolForElement(toolId, element, button) { const canvases = $(element).find('canvas'); if (element.classList.contains('empty') || !canvases.length) { return; } // If button is not defined, we should consider it left if (!button) { button = 'left'; } // First, deactivate the current active tool tools[activeTool.left].mouse.deactivate(element, 1); // 1 means left mouse button tools[activeTool.middle].mouse.deactivate(element, 2); // 2 means middle mouse button tools[activeTool.right].mouse.deactivate(element, 4); // 3 means right mouse button if (tools[activeTool.left].touch) { tools[activeTool.left].touch.deactivate(element); } // Enable tools based on their default states Object.keys(toolDefaultStates).forEach(action => { const relevantTools = toolDefaultStates[action]; if (!relevantTools || !relevantTools.length || action === 'disabledToolButtons') return; relevantTools.forEach(toolType => { // the currently active tool has already been deactivated and can be skipped if (action === 'deactivate' && (toolType === activeTool.left || toolType === activeTool.middle || toolType === activeTool.right)) { return; } tools[toolType].mouse[action]( element, (action === 'activate' || action === 'deactivate' ? 1 : void 0) ); tools[toolType].touch[action](element); }); }); // Get the stack toolData const toolData = cornerstoneTools.getToolState(element, 'stack'); if (!toolData || !toolData.data || !toolData.data.length) { return; } // Get the imageIds for this element const imageIds = toolData.data[0].imageIds; // Get the mouse button tools let newToolIdLeft = activeTool.left; if (button === 'left') { newToolIdLeft = toolId; } const newCornerstoneToolLeft = tools[newToolIdLeft]; // left mouse tool is used for touch as well let newToolIdMiddle = activeTool.middle; if (button === 'middle') { newToolIdMiddle = toolId; } const newCornerstoneToolMiddle = cornerstoneTools[newToolIdMiddle]; let newToolIdRight = activeTool.right; if (button === 'right') { newToolIdRight = toolId; } const newCornerstoneToolRight = cornerstoneTools[newToolIdRight]; // Deactivate scroll wheel tools cornerstoneTools.zoomWheel.deactivate(element); cornerstoneTools.stackScrollWheel.deactivate(element); cornerstoneTools.panMultiTouch.disable(element); cornerstoneTools.zoomTouchPinch.disable(element); cornerstoneTools.stackScrollMultiTouch.disable(element); cornerstoneTools.doubleTapZoom.disable(element); // Reactivate the relevant scrollwheel tool for this element let multiTouchPanConfig; if (imageIds.length > 1) { // scroll is the default tool for middle mouse wheel for stacks cornerstoneTools.stackScrollWheel.activate(element); if (gestures.stackScrollMultiTouch.enabled === true) { cornerstoneTools.stackScrollMultiTouch.activate(element); // Three finger scroll } multiTouchPanConfig = { testPointers(eventData) { return (eventData.numPointers === 2); } }; cornerstoneTools.panMultiTouch.setConfiguration(multiTouchPanConfig); } else { // zoom is the default tool for middle mouse wheel for single images (non stacks) cornerstoneTools.zoomWheel.activate(element); multiTouchPanConfig = { testPointers(eventData) { return (eventData.numPointers >= 2); } }; cornerstoneTools.panMultiTouch.setConfiguration(multiTouchPanConfig); } // TODO: Remove this messy approach for adding synchronizer when necessary. let leftToolSynchronizer; if (newToolIdLeft === 'crosshairs') { const currentFrameOfReferenceUID = getFrameOfReferenceUID(element); if (currentFrameOfReferenceUID) { updateCrosshairsSynchronizer(currentFrameOfReferenceUID); leftToolSynchronizer = crosshairsSynchronizers.synchronizers[currentFrameOfReferenceUID]; } if (newToolIdLeft === newToolIdMiddle && newToolIdMiddle === newToolIdRight) { newCornerstoneToolRight.activate(element, 7); // 7 means left mouse button, right mouse button and middle mouse button } else if (newToolIdLeft === newToolIdMiddle) { newCornerstoneToolMiddle.activate(element, 3); // 3 means left mouse button and middle mouse button newCornerstoneToolRight.activate(element, 4); // 4 means right mouse button } else if (newToolIdMiddle === newToolIdRight) { newCornerstoneToolRight.activate(element, 6); // 6 means right mouse button and middle mouse button newCornerstoneToolLeft.mouse.activate(element, 1, leftToolSynchronizer); // 1 means left mouse button } else if (newToolIdLeft === newToolIdRight) { newCornerstoneToolMiddle.activate(element, 2); // 2 means middle mouse button newCornerstoneToolRight.activate(element, 5); // 5 means left mouse button and right mouse button } else { newCornerstoneToolLeft.mouse.activate(element, 1, leftToolSynchronizer); // 1 means left mouse button newCornerstoneToolMiddle.activate(element, 2); // 2 means middle mouse button newCornerstoneToolRight.activate(element, 4); // 4 means right mouse button } } else { // This block ensures that all mouse button tools keep working if (newToolIdLeft === newToolIdMiddle && newToolIdMiddle === newToolIdRight) { newCornerstoneToolRight.activate(element, 7); // 7 means left mouse button, right mouse button and middle mouse button } else if (newToolIdLeft === newToolIdMiddle) { newCornerstoneToolMiddle.activate(element, 3); // 3 means left mouse button and middle mouse button newCornerstoneToolRight.activate(element, 4); // 4 means right mouse button } else if (newToolIdMiddle === newToolIdRight) { newCornerstoneToolRight.activate(element, 6); // 6 means right mouse button and middle mouse button newCornerstoneToolLeft.mouse.activate(element, 1); // 1 means left mouse button } else if (newToolIdLeft === newToolIdRight) { newCornerstoneToolMiddle.activate(element, 2); // 2 means middle mouse button newCornerstoneToolRight.activate(element, 5); // 5 means left mouse button and right mouse button } else { newCornerstoneToolLeft.mouse.activate(element, 1); // 1 means left mouse button newCornerstoneToolMiddle.activate(element, 2); // 2 means middle mouse button newCornerstoneToolRight.activate(element, 4); // 4 means right mouse button } } if (newCornerstoneToolLeft.touch) { if (leftToolSynchronizer) { newCornerstoneToolLeft.touch.activate(element, leftToolSynchronizer); } else { newCornerstoneToolLeft.touch.activate(element); } } if (gestures.zoomTouchPinch.enabled === true) { cornerstoneTools.zoomTouchPinch.activate(element); // Two finger pinch } if (gestures.panMultiTouch.enabled === true) { cornerstoneTools.panMultiTouch.activate(element); // Two or >= Two finger pan } if (gestures.doubleTapZoom.enabled === true) { cornerstoneTools.doubleTapZoom.activate(element); } }, setActiveTool(toolId, elements, button) { if (!initialized) { toolManager.init(); } let $elements; if (!elements || !elements.length) { $elements = $('.imageViewerViewport'); } else { $elements = $(elements); } const checkElementEnabled = function(allElementsEnabled, element) { try { cornerstone.getEnabledElement(element); return allElementsEnabled; } catch (error) { return true; } }; if ($elements.toArray().reduce(checkElementEnabled, false)) { // if at least one element is not enabled, we do not activate tool. OHIF.log.info(`Could not activate tool ${toolId} due to a viewport not being enabled. Try again later.`); return; } if (!activeTool) { activeTool = defaultTool; } // If button is not defined, we should consider it left if (!button) { button = 'left'; } const activeToolId = activeTool[button]; /** * TODO: Add textMarkerDialogs template to OHIF's */ const dialog = document.getElementById('textMarkerOptionsDialog'); if (dialog) { if (toolId === 'spine' && activeToolId !== 'spine' && dialog.getAttribute('open') !== 'open') { dialog.show(); } else if (activeToolId !== 'spine' && dialog.getAttribute('open') === 'open') { dialog.close(); } } if (!toolId) { toolId = defaultTool[button]; } // Otherwise, set the active tool for all viewport elements $elements.each((index, element) => { toolManager.setActiveToolForElement(toolId, element, button); }); activeTool[button] = toolId; // Enable reactivity Session.set('ToolManagerActiveToolUpdated', Random.id()); }, getNearbyToolData(element, coords, toolTypes) { const allTools = this.getTools(); const touchDevice = isTouchDevice(); const nearbyTool = {}; let pointNearTool = false; toolTypes.forEach(function(toolType) { const toolData = cornerstoneTools.getToolState(element, toolType); if (!toolData) { return; } toolData.data.forEach(function(data, index) { let toolInterfaceName = toolType; let toolInterface; // Edge cases where the tool is not the same as the typeName if (toolType === 'simpleAngle') { toolInterfaceName = 'angle'; } else if (toolType === 'arrowAnnotate') { toolInterfaceName = 'annotate'; } if (touchDevice) { toolInterface = allTools[toolInterfaceName].touch; } else { toolInterface = allTools[toolInterfaceName].mouse; } if (toolInterface.pointNearTool(element, data, coords)) { pointNearTool = true; nearbyTool.tool = data; nearbyTool.index = index; nearbyTool.toolType = toolType; } }); if (pointNearTool) { return false; } }); return pointNearTool ? nearbyTool : undefined; }, getActiveTool(button) { if (!initialized) { toolManager.init(); } // If activeTool is not defined, we should set as defaultTool if (!activeTool) { activeTool = defaultTool; } // If button is not defined, we should consider it left if (!button) { button = 'left'; } return activeTool[button]; }, setDefaultTool(tool, button) { // If button is not defined, we should consider it left if (!button) { button = 'left'; } defaultTool[button] = tool; }, getDefaultTool(button) { // If button is not defined, we should consider it left if (!button) { button = 'left'; } return defaultTool[button]; }, setConfigureTools(configureTools) { if (typeof configureTools === 'function') { this.configureTools = configureTools; } }, activateCommandButton(button) { const activeCommandButtons = Session.get('ToolManagerActiveCommandButtons') || []; if (activeCommandButtons.indexOf(button) === -1) { activeCommandButtons.push(button); Session.set('ToolManagerActiveCommandButtons', activeCommandButtons); } }, deactivateCommandButton(button) { const activeCommandButtons = Session.get('ToolManagerActiveCommandButtons') || []; const index = activeCommandButtons.indexOf(button); if (index !== -1) { activeCommandButtons.splice(index, 1); Session.set('ToolManagerActiveCommandButtons', activeCommandButtons); } } };