tests_pacs_viewer.ui.datatable.js

// Do not warn if these variables were not defined before.
/* global dwv */

// namespace
// eslint-disable-next-line no-var
var test = test || {};
test.ui = test.ui || {};

/**
 * Get the layer group div ids associated to a view config.
 *
 * @param {Array} dataViewConfig The data view config.
 * @returns {Array} The list of div ids.
 */
function getDivIds(dataViewConfig) {
  const divIds = [];
  for (let j = 0; j < dataViewConfig.length; ++j) {
    divIds.push(dataViewConfig[j].divId);
  }
  return divIds;
}

/**
 * Data table UI.
 *
 * @param {object} app The associated application.
 */
test.ui.DataTable = function (app) {

  /**
   * Bind app to ui.
   *
   * @param {string} layout The layout.
   */
  this.registerListeners = function (layout) {
    // add data row on layer creation
    app.addEventListener('viewlayeradd', function (event) {
      clearDataTableRow(event.dataid);
      addDataRow(event.dataid, layout);
    });
    app.addEventListener('drawlayeradd', function (event) {
      clearDataTableRow(event.dataid);
      addDataRow(event.dataid, layout);
    });

    app.addEventListener('wlchange', onWLChange);
    app.addEventListener('opacitychange', onOpacityChange);
  };

  /**
   * Unbind app to controls.
   */
  this.unregisterListeners = function () {
    app.removeEventListener('wlchange', onWLChange);
    app.removeEventListener('opacitychange', onOpacityChange);
  };

  /**
   * Handle app wl change.
   *
   * @param {object} event The change event.
   */
  function onWLChange(event) {
    // width number
    let elemId = 'width-' + event.dataid + '-number';
    let elem = document.getElementById(elemId);
    if (elem) {
      elem.value = event.value[1];
    } else {
      console.warn('wl change: HTML not ready?');
    }
    // width range
    elemId = 'width-' + event.dataid + '-range';
    elem = document.getElementById(elemId);
    if (elem) {
      elem.value = event.value[1];
    }
    // center number
    elemId = 'center-' + event.dataid + '-number';
    elem = document.getElementById(elemId);
    if (elem) {
      elem.value = event.value[0];
    }
    // center range
    elemId = 'center-' + event.dataid + '-range';
    elem = document.getElementById(elemId);
    if (elem) {
      elem.value = event.value[0];
    }
    // preset select
    elemId = 'preset-' + event.dataid + '-select';
    const selectElem = document.getElementById(elemId);
    if (selectElem) {
      const ids = getDataLayerGroupDivIds(event.dataid);
      const lg = app.getLayerGroupByDivId(ids[0]);
      const vls = lg.getViewLayersByDataId(event.dataid);
      if (typeof vls !== 'undefined' && vls.length !== 0) {
        const vl = vls[0];
        const vc = vl.getViewController();
        const presetName = vc.getCurrentWindowPresetName();
        const optName = 'manual';
        if (presetName === optName) {
          const options = selectElem.options;
          const optId = 'preset-manual';
          let manualOpt = options.namedItem(optId);
          if (!manualOpt) {
            const opt = document.createElement('option');
            opt.id = optId;
            opt.value = optName;
            opt.appendChild(document.createTextNode(optName));
            manualOpt = selectElem.appendChild(opt);
          }
          selectElem.selectedIndex = manualOpt.index;
        }
      }
    }
  }

  /**
   * Handle app opacity change.
   *
   * @param {object} event The change event.
   */
  function onOpacityChange(event) {
    const value = parseFloat(event.value[0]).toPrecision(3);
    // number
    let elemId = 'opacity-' + event.dataid + '-number';
    let elem = document.getElementById(elemId);
    if (elem) {
      elem.value = value;
    } else {
      console.warn('opacity change: HTML not ready?');
    }
    // range
    elemId = 'opacity-' + event.dataid + '-range';
    elem = document.getElementById(elemId);
    if (elem) {
      elem.value = value;
    }
  }

  /**
   * Get the layer group div ids associated to a data id.
   *
   * @param {string} dataId The data id.
   * @returns {Array} The list of div ids.
   */
  function getDataLayerGroupDivIds(dataId) {
    const dataViewConfigs = app.getDataViewConfigs();
    let viewConfig = dataViewConfigs[dataId];
    if (typeof viewConfig === 'undefined') {
      viewConfig = dataViewConfigs['*'];
    }
    return getDivIds(viewConfig);
  }

  /**
   * Clear the data table.
   */
  this.clearDataTable = function () {
    const detailsDiv = document.getElementById('layersdetails');
    if (detailsDiv) {
      detailsDiv.innerHTML = '';
    }
  };

  /**
   * Clear a layer details table row.
   *
   * @param {string} dataId The associated data id.
   */
  function clearDataTableRow(dataId) {
    const row = document.getElementById('data-' + dataId);
    if (row) {
      row.remove();
    }
  }

  /**
   *
   * @param {number} [numberOfLayerGroups] The number of layer groups
   *   used to create the table.
   * @returns {HTMLTableElement} The table element.
   */
  function getLayersTable(numberOfLayerGroups) {
    let table = document.getElementById('layerstable');
    // create table if not present
    if (!table) {
      table = document.createElement('table');
      table.id = 'layerstable';
      const header = table.createTHead();
      const trow = header.insertRow(0);
      const insertTCell = function (text) {
        const th = document.createElement('th');
        th.innerHTML = text;
        trow.appendChild(th);
      };
      insertTCell('Id');
      for (let j = 0; j < numberOfLayerGroups; ++j) {
        insertTCell('LG' + j);
      }
      insertTCell('Alpha Range');
      insertTCell('Contrast');
      insertTCell('Preset');
      insertTCell('Alpha');
      table.createTBody();
      const div = document.getElementById('layersdetails');
      div.appendChild(table);
    }
    return table;
  }

  /**
   * Add a data row.
   *
   * @param {string} dataId The data id.
   * @param {string} layout The layout.
   */
  function addDataRow(dataId, layout) {
    // bind app to controls on first id
    // if (dataId === '0') {
    //   this.registerListeners();
    // }

    const image = app.getData(dataId).image;
    const dataIsImage = typeof image !== 'undefined';
    const canAlpha = dataIsImage;
    const isMonochrome = dataIsImage && image.isMonochrome();

    const dataViewConfigs = app.getDataViewConfigs();
    const allLayerGroupDivIds = test.getLayerGroupDivIds(dataViewConfigs);

    const table = getLayersTable(allLayerGroupDivIds.length);
    const body = table.tBodies[0];

    // add new layer row
    const row = body.insertRow();
    row.id = 'data-' + dataId;

    let cell;

    // get the selected layer group ids
    const getSelectedLayerGroupIds = function () {
      const res = [];
      for (const divId of allLayerGroupDivIds) {
        const elemId = 'layerselect-' + divId + '-' + dataId;
        const elem = document.getElementById(elemId);
        if (elem && elem.checked) {
          res.push(divId);
        }
      }
      return res;
    };

    // get a layer radio button
    const getLayerRadio = function (index, divId) {
      const radio = document.createElement('input');
      radio.type = 'radio';
      radio.name = 'layerselect-' + index;
      radio.id = 'layerselect-' + divId + '-' + dataId;
      radio.checked = true;
      radio.onchange = function (event) {
        const element = event.target;
        const fullId = element.id;
        const split = fullId.split('-');
        const groupDivId = split[1];
        const dataId = split[2];
        const lg = app.getLayerGroupByDivId(groupDivId);
        if (dataIsImage) {
          lg.setActiveViewLayerByDataId(dataId);
          lg.setActiveDrawLayer(undefined);
        } else {
          lg.setActiveDrawLayerByDataId(dataId);
        }
      };
      return radio;
    };

    // get a layer add button
    const getLayerAdd = function (index, divId) {
      const button = document.createElement('button');
      button.name = 'layeradd-' + index;
      button.id = 'layeradd-' + divId + '-' + dataId;
      button.title = 'Add layer';
      button.appendChild(document.createTextNode('+'));
      button.onclick = function () {
        // update app
        app.addDataViewConfig(dataId, test.getViewConfig(layout, divId));
        // update html
        const parent = button.parentElement;
        if (parent) {
          parent.replaceChildren();
          parent.appendChild(getLayerRadio(index, divId));
          parent.appendChild(getLayerRem(index, divId));
          parent.appendChild(
            getLayerUpdate(index, divId, dwv.Orientation.Axial));
          parent.appendChild(
            getLayerUpdate(index, divId, dwv.Orientation.Coronal));
          parent.appendChild(
            getLayerUpdate(index, divId, dwv.Orientation.Sagittal));
        }
      };
      return button;
    };

    // get a layer remove button
    const getLayerRem = function (index, divId) {
      const button = document.createElement('button');
      button.name = 'layerrem-' + index;
      button.id = 'layerrem-' + divId + '-' + dataId;
      button.title = 'Remove layer';
      button.appendChild(document.createTextNode('-'));
      button.onclick = function () {
        // update app
        app.removeDataViewConfig(dataId, divId);
        // update html
        const parent = button.parentElement;
        parent.replaceChildren();
        parent.appendChild(getLayerAdd(index, divId));
      };
      return button;
    };

    // get a layer update button
    const getLayerUpdate = function (index, divId, orientation) {
      const button = document.createElement('button');
      const letter = orientation[0].toUpperCase();
      button.name = 'layerupd-' + index + '_' + letter;
      button.id = 'layerupd-' + divId + '-' + dataId + '_' + letter;
      button.title = 'Change layer orientation to ' + orientation;
      button.style.borderStyle = 'outset';
      button.appendChild(document.createTextNode(letter));
      button.onclick = function () {
        // update app
        const config = test.getViewConfig(layout, divId);
        config.orientation = orientation;
        app.updateDataViewConfig(dataId, divId, config);
      };
      return button;
    };

    // cell: id
    cell = row.insertCell();
    cell.appendChild(document.createTextNode(dataId));

    const orientations = [
      dwv.Orientation.Axial,
      dwv.Orientation.Coronal,
      dwv.Orientation.Sagittal
    ];

    // cell: radio
    let viewConfigs = dataViewConfigs[dataId];
    if (typeof viewConfigs === 'undefined') {
      viewConfigs = dataViewConfigs['*'];
    }

    const dataLayerGroupsIds = getDivIds(viewConfigs);
    for (let i = 0; i < allLayerGroupDivIds.length; ++i) {
      const layerGroupDivId = allLayerGroupDivIds[i];
      const viewConfig =
        viewConfigs.find(element => element.divId === layerGroupDivId);
      cell = row.insertCell();
      if (dataLayerGroupsIds.includes(layerGroupDivId)) {
        cell.appendChild(getLayerRadio(i, layerGroupDivId));
        cell.appendChild(getLayerRem(i, layerGroupDivId));
        for (const orientation of orientations) {
          const button = getLayerUpdate(i, layerGroupDivId, orientation);
          if (orientation === viewConfig.orientation) {
            button.style.borderStyle = 'inset';
          }
          cell.appendChild(button);
        }
      } else {
        cell.appendChild(getLayerAdd(i, layerGroupDivId));
      }
    }

    // use first layer
    const initialVls = app.getViewLayersByDataId(dataId);
    const initialDls = app.getDrawLayersByDataId(dataId);
    let initialLayer;
    if (initialVls.length !== 0) {
      initialLayer = initialVls[0];
    } else if (initialDls.length !== 0) {
      initialLayer = initialDls[0];
    }

    const floatPrecision = 4;

    // cell: alpha range
    cell = row.insertCell();
    const minId = 'value-min-' + dataId;
    const maxId = 'value-max-' + dataId;
    // callback
    const changeAlphaFunc = function () {
      const minElement = document.getElementById(minId + '-number');
      const min = parseFloat(minElement.value);
      const maxElement = document.getElementById(maxId + '-number');
      const max = parseFloat(maxElement.value);
      const func = function (value, _index) {
        if (value >= min && value <= max) {
          return 255;
        }
        return 0;
      };
      // update selected layers
      const lgIds = getSelectedLayerGroupIds();
      for (let i = 0; i < lgIds.length; ++i) {
        const lg = app.getLayerGroupByDivId(lgIds[i]);
        const vl = lg.getActiveViewLayer();
        if (typeof vl !== 'undefined') {
          const vc = vl.getViewController();
          vc.setViewAlphaFunction(func);
        }
      }
    };
    // add controls
    if (canAlpha) {
      const dataRange = image.getDataRange();
      cell.appendChild(test.getControlDiv(minId, 'min',
        dataRange.min, dataRange.max, dataRange.min,
        changeAlphaFunc, floatPrecision));
      cell.appendChild(test.getControlDiv(maxId, 'max',
        dataRange.min, dataRange.max, dataRange.max,
        changeAlphaFunc, floatPrecision));
    }

    // cell: contrast
    cell = row.insertCell();
    const widthId = 'width-' + dataId;
    const centerId = 'center-' + dataId;
    // callback
    const changeContrast = function () {
      const wElement = document.getElementById(widthId + '-number');
      const width = parseFloat(wElement.value);
      const cElement = document.getElementById(centerId + '-number');
      const center = parseFloat(cElement.value);
      // update selected layers
      const lgIds = getSelectedLayerGroupIds();
      for (let i = 0; i < lgIds.length; ++i) {
        const lg = app.getLayerGroupByDivId(lgIds[i]);
        const vl = lg.getActiveViewLayer();
        if (typeof vl !== 'undefined') {
          const vc = vl.getViewController();
          vc.setWindowLevel(new dwv.WindowLevel(center, width));
        }
      }
    };
    // add controls
    if (isMonochrome) {
      const initialVc = initialLayer.getViewController();
      const rescaledDataRange = image.getRescaledDataRange();
      cell.appendChild(test.getControlDiv(widthId, 'width',
        0,
        rescaledDataRange.max - rescaledDataRange.min,
        initialVc.getWindowLevel().width,
        changeContrast, floatPrecision));
      cell.appendChild(test.getControlDiv(centerId, 'center',
        rescaledDataRange.min,
        rescaledDataRange.max,
        initialVc.getWindowLevel().center,
        changeContrast, floatPrecision));
    }

    // cell: presets
    cell = row.insertCell();

    // window level preset
    // callback
    const changePreset = function (event) {
      const element = event.target;
      // update selected layers
      const lgIds = getSelectedLayerGroupIds();
      for (let i = 0; i < lgIds.length; ++i) {
        const lg = app.getLayerGroupByDivId(lgIds[i]);
        const vl = lg.getActiveViewLayer();
        if (typeof vl !== 'undefined') {
          const vc = vl.getViewController();
          vc.setWindowLevelPreset(element.value);
        }
      }
    };
    if (isMonochrome) {
      const selectPreset = document.createElement('select');
      selectPreset.id = 'preset-' + dataId + '-select';
      const initialVc = initialLayer.getViewController();
      const presets = initialVc.getWindowLevelPresetsNames();
      const currentPresetName = initialVc.getCurrentWindowPresetName();
      for (const preset of presets) {
        const option = document.createElement('option');
        option.value = preset;
        if (preset === currentPresetName) {
          option.selected = true;
        }
        option.appendChild(document.createTextNode(preset));
        selectPreset.appendChild(option);
      }
      selectPreset.onchange = changePreset;
      const labelPreset = document.createElement('label');
      labelPreset.htmlFor = selectPreset.id;
      labelPreset.appendChild(document.createTextNode('wl: '));
      cell.appendChild(labelPreset);
      cell.appendChild(selectPreset);
    }

    // break line
    const br = document.createElement('br');
    cell.appendChild(br);

    // colour map
    // callback
    const changeColourMap = function (event) {
      const element = event.target;
      // update selected layers
      const lgIds = getSelectedLayerGroupIds();
      for (let i = 0; i < lgIds.length; ++i) {
        const lg = app.getLayerGroupByDivId(lgIds[i]);
        const vl = lg.getActiveViewLayer();
        if (typeof vl !== 'undefined') {
          const vc = vl.getViewController();
          vc.setColourMap(element.value);
        }
      }
    };
    if (isMonochrome) {
      const selectColourMap = document.createElement('select');
      selectColourMap.id = 'colourmap-' + dataId + '-select';
      const initialVc = initialLayer.getViewController();
      const colourMaps = Object.keys(dwv.luts);
      const currentColourMap = initialVc.getColourMap();
      for (const colourMap of colourMaps) {
        const option = document.createElement('option');
        option.value = colourMap;
        if (colourMap === currentColourMap) {
          option.selected = true;
        }
        option.appendChild(document.createTextNode(colourMap));
        selectColourMap.appendChild(option);
      }
      selectColourMap.onchange = changeColourMap;
      const labelColourMap = document.createElement('label');
      labelColourMap.htmlFor = selectColourMap.id;
      labelColourMap.appendChild(document.createTextNode('cm: '));
      cell.appendChild(labelColourMap);
      cell.appendChild(selectColourMap);
    }

    // cell: opactiy
    cell = row.insertCell();
    const opacityId = 'opacity-' + dataId;
    // callback
    const changeOpacity = function (value) {
      // update selected layers
      const lgIds = getSelectedLayerGroupIds();
      for (let i = 0; i < lgIds.length; ++i) {
        const lg = app.getLayerGroupByDivId(lgIds[i]);
        let layer;
        if (dataIsImage) {
          layer = lg.getActiveViewLayer();
        } else {
          layer = lg.getActiveDrawLayer();
        }
        if (typeof layer !== 'undefined') {
          layer.setOpacity(value);
          layer.draw();
        }
      }
    };
    // add controls
    cell.appendChild(test.getControlDiv(opacityId, 'opacity',
      0, 1, initialLayer.getOpacity(), changeOpacity, floatPrecision));
  }

}; // test.DataTable