imageTools/wwwcRegion.js

import $ from '../jquery.js';
import * as cornerstone from '../cornerstone-core.js';
import toolStyle from '../stateManagement/toolStyle.js';
import toolColors from '../stateManagement/toolColors.js';
import { getToolState, addToolState } from '../stateManagement/toolState.js';
import getLuminance from '../util/getLuminance.js';
import isMouseButtonEnabled from '../util/isMouseButtonEnabled.js';

const toolType = 'wwwcRegion';

let configuration = {
  minWindowWidth: 10
};

let currentMouseButtonMask;

/** Calculates the minimum, maximum, and mean value in the given pixel array */
function calculateMinMaxMean (storedPixelLuminanceData, globalMin, globalMax) {
  const numPixels = storedPixelLuminanceData.length;

  if (numPixels < 2) {
    return {
      min: globalMin,
      max: globalMax,
      mean: (globalMin + globalMax) / 2
    };
  }

  let min = globalMax;
  let max = globalMin;
  let sum = 0;

  for (let index = 0; index < numPixels; index++) {
    const spv = storedPixelLuminanceData[index];

    min = Math.min(min, spv);
    max = Math.max(max, spv);
    sum += spv;
  }

  return {
    min,
    max,
    mean: sum / numPixels
  };
}

/* Erases the toolData and rebinds the handlers when the image changes */
function newImageCallback (e, eventData) {
  const toolData = getToolState(eventData.element, toolType);

  if (toolData && toolData.data) {
    toolData.data = [];
  }

  $(eventData.element).off('CornerstoneToolsMouseMove', dragCallback);
  $(eventData.element).off('CornerstoneToolsMouseDrag', dragCallback);

  $(eventData.element).off('CornerstoneToolsMouseUp', dragEndCallback);
  $(eventData.element).off('CornerstoneToolsMouseClick', dragEndCallback);

  const mouseData = {
    mouseButtonMask: currentMouseButtonMask
  };

  $(eventData.element).on('CornerstoneToolsMouseDown', mouseData, mouseDownCallback);
}

/* Applies the windowing procedure when the mouse drag ends */
function dragEndCallback (e, eventData) {
  $(eventData.element).off('CornerstoneToolsMouseMove', dragCallback);
  $(eventData.element).off('CornerstoneToolsMouseDrag', dragCallback);

  $(eventData.element).off('CornerstoneToolsMouseUp', dragEndCallback);
  $(eventData.element).off('CornerstoneToolsMouseClick', dragEndCallback);

  const mouseData = {
    mouseButtonMask: currentMouseButtonMask
  };

  $(eventData.element).on('CornerstoneToolsMouseDown', mouseData, mouseDownCallback);

  const toolData = getToolState(eventData.element, toolType);

  if (!toolData || !toolData.data || !toolData.data.length) {
    return;
  }

    // Update the endpoint as the mouse/touch is dragged
  toolData.data[0].endPoint = {
    x: eventData.currentPoints.image.x,
    y: eventData.currentPoints.image.y
  };

  applyWWWCRegion(eventData);
}

/** Calculates the minimum and maximum value in the given pixel array */
function applyWWWCRegion (eventData) {
  const toolData = getToolState(eventData.element, toolType);

  if (!toolData || !toolData.data || !toolData.data.length) {
    return;
  }

  const startPoint = toolData.data[0].startPoint;
  const endPoint = toolData.data[0].endPoint;

    // Get the rectangular region defined by the handles
  let width = Math.abs(startPoint.x - endPoint.x);
  let height = Math.abs(startPoint.y - endPoint.y);

  let left = Math.min(startPoint.x, endPoint.x);
  let top = Math.min(startPoint.y, endPoint.y);

    // Bound the rectangle so we don't get undefined pixels
  left = Math.max(left, 0);
  left = Math.min(left, eventData.image.width);
  top = Math.max(top, 0);
  top = Math.min(top, eventData.image.height);
  width = Math.floor(Math.min(width, Math.abs(eventData.image.width - left)));
  height = Math.floor(Math.min(height, Math.abs(eventData.image.height - top)));

    // Get the pixel data in the rectangular region
  const pixelLuminanceData = getLuminance(eventData.element, left, top, width, height);

    // Calculate the minimum and maximum pixel values
  const minMaxMean = calculateMinMaxMean(pixelLuminanceData, eventData.image.minPixelValue, eventData.image.maxPixelValue);

    // Adjust the viewport window width and center based on the calculated values
  const config = wwwcRegion.getConfiguration();
  const viewport = cornerstone.getViewport(eventData.element);

  if (config.minWindowWidth === undefined) {
    config.minWindowWidth = 10;
  }

  viewport.voi.windowWidth = Math.max(Math.abs(minMaxMean.max - minMaxMean.min), config.minWindowWidth);
  viewport.voi.windowCenter = minMaxMean.mean;
  cornerstone.setViewport(eventData.element, viewport);

    // Clear the toolData
  toolData.data = [];

  cornerstone.updateImage(eventData.element);
}

function whichMovement (e, eventData) {
  const element = eventData.element;

  $(element).off('CornerstoneToolsMouseMove');
  $(element).off('CornerstoneToolsMouseDrag');

  $(element).on('CornerstoneToolsMouseMove', dragCallback);
  $(element).on('CornerstoneToolsMouseDrag', dragCallback);

  $(element).on('CornerstoneToolsMouseClick', dragEndCallback);
  if (e.type === 'CornerstoneToolsMouseDrag') {
    $(element).on('CornerstoneToolsMouseUp', dragEndCallback);
  }
}

/** Records the start point and attaches the drag event handler */
function mouseDownCallback (e, eventData) {
  if (isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
    $(eventData.element).on('CornerstoneToolsMouseDrag', eventData, whichMovement);
    $(eventData.element).on('CornerstoneToolsMouseMove', eventData, whichMovement);

    $(eventData.element).off('CornerstoneToolsMouseDown', mouseDownCallback);
    recordStartPoint(eventData);

    return false;
  }
}

/** Records the start point of the click or touch */
function recordStartPoint (eventData) {
  const toolData = getToolState(eventData.element, toolType);

  if (toolData && toolData.data) {
    toolData.data = [];
  }

  const measurementData = {
    startPoint: {
      x: eventData.currentPoints.image.x,
      y: eventData.currentPoints.image.y
    }
  };

  addToolState(eventData.element, toolType, measurementData);
}

/** Draws the rectangular region while the touch or mouse event drag occurs */
function dragCallback (e, eventData) {
    // If we have no toolData for this element, return immediately as there is nothing to do
  const toolData = getToolState(eventData.element, toolType);

  if (!toolData || !toolData.data || !toolData.data.length) {
    return;
  }

    // Update the endpoint as the mouse/touch is dragged
  const endPoint = {
    x: eventData.currentPoints.image.x,
    y: eventData.currentPoints.image.y
  };

  toolData.data[0].endPoint = endPoint;
  cornerstone.updateImage(eventData.element);
}

function onImageRendered (e, eventData) {
  const toolData = getToolState(eventData.element, toolType);

  if (!toolData || !toolData.data || !toolData.data.length) {
    return;
  }

  const startPoint = toolData.data[0].startPoint;
  const endPoint = toolData.data[0].endPoint;

  if (!startPoint || !endPoint) {
    return;
  }

    // Get the current element's canvas
  const canvas = $(eventData.element).find('canvas').get(0);
  const context = canvas.getContext('2d');

  context.setTransform(1, 0, 0, 1, 0, 0);

    // Set to the active tool color
  const color = toolColors.getActiveColor();

    // Calculate the rectangle parameters
  const startPointCanvas = cornerstone.pixelToCanvas(eventData.element, startPoint);
  const endPointCanvas = cornerstone.pixelToCanvas(eventData.element, endPoint);

  const left = Math.min(startPointCanvas.x, endPointCanvas.x);
  const top = Math.min(startPointCanvas.y, endPointCanvas.y);
  const width = Math.abs(startPointCanvas.x - endPointCanvas.x);
  const height = Math.abs(startPointCanvas.y - endPointCanvas.y);

  const lineWidth = toolStyle.getToolWidth();
  const config = wwwcRegion.getConfiguration();

    // Draw the rectangle
  context.save();

  if (config && config.shadow) {
    context.shadowColor = config.shadowColor || '#000000';
    context.shadowOffsetX = config.shadowOffsetX || 1;
    context.shadowOffsetY = config.shadowOffsetY || 1;
  }

  context.beginPath();
  context.strokeStyle = color;
  context.lineWidth = lineWidth;
  context.rect(left, top, width, height);
  context.stroke();

  context.restore();
}

// --- Mouse tool enable / disable --- ///
function disable (element) {
  $(element).off('CornerstoneToolsMouseDown', mouseDownCallback);

  $(element).off('CornerstoneToolsMouseUp', dragEndCallback);
  $(element).off('CornerstoneToolsMouseClick', dragEndCallback);

  $(element).off('CornerstoneToolsMouseDrag', dragCallback);
  $(element).off('CornerstoneToolsMouseMove', dragCallback);

  $(element).off('CornerstoneImageRendered', onImageRendered);
  $(element).off('CornerstoneNewImage', newImageCallback);

  cornerstone.updateImage(element);
}

function activate (element, mouseButtonMask) {
  const eventData = {
    mouseButtonMask
  };

  currentMouseButtonMask = mouseButtonMask;

  const toolData = getToolState(element, toolType);

  if (!toolData) {
    const data = [];

    addToolState(element, toolType, data);
  }

  $(element).off('CornerstoneToolsMouseDown', mouseDownCallback);

  $(element).off('CornerstoneToolsMouseUp', dragEndCallback);
  $(element).off('CornerstoneToolsMouseClick', dragEndCallback);

  $(element).off('CornerstoneToolsMouseDrag', dragCallback);
  $(element).off('CornerstoneToolsMouseMove', dragCallback);

  $(element).off('CornerstoneImageRendered', onImageRendered);
  $(element).off('CornerstoneNewImage', newImageCallback);

  $(element).on('CornerstoneToolsMouseDown', eventData, mouseDownCallback);
  $(element).on('CornerstoneImageRendered', onImageRendered);

    // If the displayed image changes after the user has started clicking, we should
    // Cancel the handlers and prepare for another click
  $(element).on('CornerstoneNewImage', newImageCallback);

  cornerstone.updateImage(element);
}

// --- Touch tool enable / disable --- //
function disableTouchDrag (element) {
  $(element).off('CornerstoneToolsTouchDrag', dragCallback);
  $(element).off('CornerstoneToolsTouchStart', recordStartPoint);
  $(element).off('CornerstoneToolsDragEnd', applyWWWCRegion);
  $(element).off('CornerstoneImageRendered', onImageRendered);
}

function activateTouchDrag (element) {
  const toolData = getToolState(element, toolType);

  if (!toolData) {
    const data = [];

    addToolState(element, toolType, data);
  }

  $(element).off('CornerstoneToolsTouchDrag', dragCallback);
  $(element).off('CornerstoneToolsTouchStart', recordStartPoint);
  $(element).off('CornerstoneToolsDragEnd', applyWWWCRegion);
  $(element).off('CornerstoneImageRendered', onImageRendered);

  $(element).on('CornerstoneToolsTouchDrag', dragCallback);
  $(element).on('CornerstoneToolsTouchStart', recordStartPoint);
  $(element).on('CornerstoneToolsDragEnd', applyWWWCRegion);
  $(element).on('CornerstoneImageRendered', onImageRendered);
}

function getConfiguration () {
  return configuration;
}

function setConfiguration (config) {
  configuration = config;
}

// Module exports
const wwwcRegion = {
  activate,
  deactivate: disable,
  disable,
  setConfiguration,
  getConfiguration
};

const wwwcRegionTouch = {
  activate: activateTouchDrag,
  deactivate: disableTouchDrag,
  disable: disableTouchDrag
};

export {
  wwwcRegion,
  wwwcRegionTouch
};