src/io/state.js

// namespaces
var dwv = dwv || {};
dwv.io = dwv.io || {};
// external
var Konva = Konva || {};

/**
 * State class.
 * Saves: data url/path, display info.
 *
 * History:
 * - v0.5 (dwv 0.30.0, ??/2021)
 *   - store position as array
 *   - new draw position group key
 * - v0.4 (dwv 0.29.0, 06/2021)
 *   - move drawing details into meta property
 *   - remove scale center and translation, add offset
 * - v0.3 (dwv v0.23.0, 03/2018)
 *   - new drawing structure, drawings are now the full layer object and
 *     using toObject to avoid saving a string representation
 *   - new details structure: simple array of objects referenced by draw ids
 * - v0.2 (dwv v0.17.0, 12/2016)
 *   - adds draw details: array [nslices][nframes] of detail objects
 * - v0.1 (dwv v0.15.0, 07/2016)
 *   - adds version
 *   - drawings: array [nslices][nframes] with all groups
 * - initial release (dwv v0.10.0, 05/2015), no version number...
 *   - content: window-center, window-width, position, scale,
 *       scaleCenter, translation, drawings
 *   - drawings: array [nslices] with all groups
 *
 * @class
 */
dwv.io.State = function () {
  /**
   * Save the application state as JSON.
   *
   * @param {object} app The associated application.
   * @returns {string} The state as a JSON string.
   */
  this.toJSON = function (app) {
    var layerGroup = app.getActiveLayerGroup();
    var viewController =
      layerGroup.getActiveViewLayer().getViewController();
    var drawLayer = layerGroup.getActiveDrawLayer();
    var position = viewController.getCurrentPosition();
    // return a JSON string
    return JSON.stringify({
      version: '0.5',
      'window-center': viewController.getWindowLevel().center,
      'window-width': viewController.getWindowLevel().width,
      position: [position.getX(), position.getY(), position.getZ()],
      scale: app.getAddedScale(),
      offset: app.getOffset(),
      drawings: drawLayer.getKonvaLayer().toObject(),
      drawingsDetails: app.getDrawStoreDetails()
    });
  };
  /**
   * Load an application state from JSON.
   *
   * @param {string} json The JSON representation of the state.
   * @returns {object} The state object.
   */
  this.fromJSON = function (json) {
    var data = JSON.parse(json);
    var res = null;
    if (data.version === '0.1') {
      res = readV01(data);
    } else if (data.version === '0.2') {
      res = readV02(data);
    } else if (data.version === '0.3') {
      res = readV03(data);
    } else if (data.version === '0.4') {
      res = readV04(data);
    } else if (data.version === '0.5') {
      res = readV05(data);
    } else {
      throw new Error('Unknown state file format version: \'' +
        data.version + '\'.');
    }
    return res;
  };
  /**
   * Load an application state from JSON.
   *
   * @param {object} app The app to apply the state to.
   * @param {object} data The state data.
   */
  this.apply = function (app, data) {
    var layerGroup = app.getActiveLayerGroup();
    var viewController =
      layerGroup.getActiveViewLayer().getViewController();
    // display
    viewController.setWindowLevel(
      data['window-center'], data['window-width']);
    viewController.setCurrentPosition(
      new dwv.math.Point3D(
        data.position[0], data.position[1], data.position[2]), true);
    // apply saved scale on top of current base one
    var baseScale = app.getActiveLayerGroup().getBaseScale();
    var scale = null;
    var offset = null;
    if (typeof data.scaleCenter !== 'undefined') {
      scale = {
        x: data.scale * baseScale.x,
        y: data.scale * baseScale.y,
        z: 1
      };
      // ---- transform translation (now) ----
      // Tx = -offset.x * scale.x
      // => offset.x = -Tx / scale.x
      // ---- transform translation (before) ----
      // origin.x = centerX - (centerX - origin.x) * (newZoomX / zoom.x);
      // (zoom.x -> initial zoom = base scale, origin.x = 0)
      // Tx = origin.x + (trans.x * zoom.x)
      var originX = data.scaleCenter.x - data.scaleCenter.x * data.scale;
      var originY = data.scaleCenter.y - data.scaleCenter.y * data.scale;
      var oldTx = originX + data.translation.x * scale.x;
      var oldTy = originY + data.translation.y * scale.y;
      offset = {
        x: -oldTx / scale.x,
        y: -oldTy / scale.y,
        z: 0
      };
    } else {
      scale = {
        x: data.scale.x * baseScale.x,
        y: data.scale.y * baseScale.y,
        z: 1
      };
      offset = {
        x: data.offset.x,
        y: data.offset.y,
        z: 0
      };
    }
    app.getActiveLayerGroup().setScale(scale);
    app.getActiveLayerGroup().setOffset(offset);
    // render to draw the view layer
    app.render(0); //todo: fix
    // drawings (will draw the draw layer)
    app.setDrawings(data.drawings, data.drawingsDetails);
  };
  /**
   * Read an application state from an Object in v0.1 format.
   *
   * @param {object} data The Object representation of the state.
   * @returns {object} The state object.
   * @private
   */
  function readV01(data) {
    // v0.1 -> v0.2
    var v02DAndD = dwv.io.v01Tov02DrawingsAndDetails(data.drawings);
    // v0.2 -> v0.3, v0.4
    data.drawings = dwv.io.v02Tov03Drawings(v02DAndD.drawings).toObject();
    data.drawingsDetails = dwv.io.v03Tov04DrawingsDetails(
      v02DAndD.drawingsDetails);
    // v0.4 -> v0.5
    data = dwv.io.v04Tov05Data(data);
    data.drawings = dwv.io.v04Tov05Drawings(data.drawings);
    return data;
  }
  /**
   * Read an application state from an Object in v0.2 format.
   *
   * @param {object} data The Object representation of the state.
   * @returns {object} The state object.
   * @private
   */
  function readV02(data) {
    // v0.2 -> v0.3, v0.4
    data.drawings = dwv.io.v02Tov03Drawings(data.drawings).toObject();
    data.drawingsDetails = dwv.io.v03Tov04DrawingsDetails(
      dwv.io.v02Tov03DrawingsDetails(data.drawingsDetails));
    // v0.4 -> v0.5
    data = dwv.io.v04Tov05Data(data);
    data.drawings = dwv.io.v04Tov05Drawings(data.drawings);
    return data;
  }
  /**
   * Read an application state from an Object in v0.3 format.
   *
   * @param {object} data The Object representation of the state.
   * @returns {object} The state object.
   * @private
   */
  function readV03(data) {
    // v0.3 -> v0.4
    data.drawingsDetails = dwv.io.v03Tov04DrawingsDetails(data.drawingsDetails);
    // v0.4 -> v0.5
    data = dwv.io.v04Tov05Data(data);
    data.drawings = dwv.io.v04Tov05Drawings(data.drawings);
    return data;
  }
  /**
   * Read an application state from an Object in v0.4 format.
   *
   * @param {object} data The Object representation of the state.
   * @returns {object} The state object.
   * @private
   */
  function readV04(data) {
    // v0.4 -> v0.5
    data = dwv.io.v04Tov05Data(data);
    data.drawings = dwv.io.v04Tov05Drawings(data.drawings);
    return data;
  }
  /**
   * Read an application state from an Object in v0.5 format.
   *
   * @param {object} data The Object representation of the state.
   * @returns {object} The state object.
   * @private
   */
  function readV05(data) {
    return data;
  }

}; // State class

/**
 * Convert drawings from v0.2 to v0.3.
 * v0.2: one layer per slice/frame
 * v0.3: one layer, one group per slice. setDrawing expects the full stage
 *
 * @param {Array} drawings An array of drawings.
 * @returns {object} The layer with the converted drawings.
 */
dwv.io.v02Tov03Drawings = function (drawings) {
  // Auxiliar variables
  var group, groupShapes, parentGroup;
  // Avoid errors when dropping multiple states
  //drawLayer.getChildren().each(function(node){
  //    node.visible(false);
  //});

  var drawLayer = new Konva.Layer({
    listening: false,
    visible: true
  });

  // Get the positions-groups data
  var groupDrawings = typeof drawings === 'string'
    ? JSON.parse(drawings) : drawings;
  // Iterate over each position-groups
  for (var k = 0, lenk = groupDrawings.length; k < lenk; ++k) {
    // Iterate over each frame
    for (var f = 0, lenf = groupDrawings[k].length; f < lenf; ++f) {
      groupShapes = groupDrawings[k][f];
      if (groupShapes.length !== 0) {
        // Create position-group set as visible and append it to drawLayer
        parentGroup = new Konva.Group({
          id: dwv.draw.getDrawPositionGroupId(new dwv.math.Index([1, 1, k, f])),
          name: 'position-group',
          visible: false
        });

        // Iterate over shapes-group
        for (var g = 0, leng = groupShapes.length; g < leng; ++g) {
          // create the konva group
          group = Konva.Node.create(groupShapes[g]);
          // enforce draggable: only the shape was draggable in v0.2,
          // now the whole group is.
          group.draggable(true);
          group.getChildren().forEach(function (gnode) {
            gnode.draggable(false);
          });
          // add to position group
          parentGroup.add(group);
        }
        // add to layer
        drawLayer.add(parentGroup);
      }
    }
  }

  return drawLayer;
};

/**
 * Convert drawings from v0.1 to v0.2.
 * v0.1: text on its own
 * v0.2: text as part of label
 *
 * @param {Array} inputDrawings An array of drawings.
 * @returns {object} The converted drawings.
 */
dwv.io.v01Tov02DrawingsAndDetails = function (inputDrawings) {
  var newDrawings = [];
  var drawingsDetails = {};

  var drawGroups;
  var drawGroup;
  // loop over each slice
  for (var k = 0, lenk = inputDrawings.length; k < lenk; ++k) {
    // loop over each frame
    newDrawings[k] = [];
    for (var f = 0, lenf = inputDrawings[k].length; f < lenf; ++f) {
      // draw group
      drawGroups = inputDrawings[k][f];
      var newFrameDrawings = [];
      // Iterate over shapes-group
      for (var g = 0, leng = drawGroups.length; g < leng; ++g) {
        // create konva group from input
        drawGroup = Konva.Node.create(drawGroups[g]);
        // force visible (not set in state)
        drawGroup.visible(true);
        // label position
        var pos = {x: 0, y: 0};
        // update shape colour
        var kshape = drawGroup.getChildren(function (node) {
          return node.name() === 'shape';
        })[0];
        kshape.stroke(dwv.utils.colourNameToHex(kshape.stroke()));
        // special line case
        if (drawGroup.name() === 'line-group') {
          // update name
          drawGroup.name('ruler-group');
          // add ticks
          var ktick0 = new Konva.Line({
            points: [kshape.points()[0],
              kshape.points()[1],
              kshape.points()[0],
              kshape.points()[1]],
            name: 'shape-tick0'
          });
          drawGroup.add(ktick0);
          var ktick1 = new Konva.Line({
            points: [kshape.points()[2],
              kshape.points()[3],
              kshape.points()[2],
              kshape.points()[3]],
            name: 'shape-tick1'
          });
          drawGroup.add(ktick1);
        }
        // special protractor case: update arc name
        var karcs = drawGroup.getChildren(function (node) {
          return node.name() === 'arc';
        });
        if (karcs.length === 1) {
          karcs[0].name('shape-arc');
        }
        // get its text
        var ktexts = drawGroup.getChildren(function (node) {
          return node.name() === 'text';
        });
        // update text: move it into a label
        var ktext = new Konva.Text({
          name: 'text',
          text: ''
        });
        if (ktexts.length === 1) {
          pos.x = ktexts[0].x();
          pos.y = ktexts[0].y();
          // remove it from the group
          ktexts[0].remove();
          // use it
          ktext = ktexts[0];
        } else {
          // use shape position if no text
          if (kshape.points().length !== 0) {
            pos = {x: kshape.points()[0],
              y: kshape.points()[1]};
          }
        }
        // create new label with text and tag
        var klabel = new Konva.Label({
          x: pos.x,
          y: pos.y,
          name: 'label'
        });
        klabel.add(ktext);
        klabel.add(new Konva.Tag());
        // add label to group
        drawGroup.add(klabel);
        // add group to list
        newFrameDrawings.push(JSON.stringify(drawGroup.toObject()));

        // create details (v0.3 format)
        var textExpr = ktext.text();
        var txtLen = textExpr.length;
        var quant = null;
        // adapt to text with flag
        if (drawGroup.name() === 'ruler-group') {
          quant = {
            length: {
              value: parseFloat(textExpr.substr(0, txtLen - 2)),
              unit: textExpr.substr(-2, 2)
            }
          };
          textExpr = '{length}';
        } else if (drawGroup.name() === 'ellipse-group' ||
                    drawGroup.name() === 'rectangle-group') {
          quant = {
            surface: {
              value: parseFloat(textExpr.substr(0, txtLen - 3)),
              unit: textExpr.substr(-3, 3)
            }
          };
          textExpr = '{surface}';
        } else if (drawGroup.name() === 'protractor-group' ||
                    drawGroup.name() === 'rectangle-group') {
          quant = {
            angle: {
              value: parseFloat(textExpr.substr(0, txtLen - 1)),
              unit: textExpr.substr(-1, 1)
            }
          };
          textExpr = '{angle}';
        }
        // set details
        drawingsDetails[drawGroup.id()] = {
          textExpr: textExpr,
          longText: '',
          quant: quant
        };

      }
      newDrawings[k].push(newFrameDrawings);
    }
  }

  return {drawings: newDrawings, drawingsDetails: drawingsDetails};
};

/**
 * Convert drawing details from v0.2 to v0.3.
 * - v0.2: array [nslices][nframes] with all
 * - v0.3: simple array of objects referenced by draw ids
 *
 * @param {Array} details An array of drawing details.
 * @returns {object} The converted drawings.
 */
dwv.io.v02Tov03DrawingsDetails = function (details) {
  var res = {};
  // Get the positions-groups data
  var groupDetails = typeof details === 'string'
    ? JSON.parse(details) : details;
  // Iterate over each position-groups
  for (var k = 0, lenk = groupDetails.length; k < lenk; ++k) {
    // Iterate over each frame
    for (var f = 0, lenf = groupDetails[k].length; f < lenf; ++f) {
      // Iterate over shapes-group
      for (var g = 0, leng = groupDetails[k][f].length; g < leng; ++g) {
        var group = groupDetails[k][f][g];
        res[group.id] = {
          textExpr: group.textExpr,
          longText: group.longText,
          quant: group.quant
        };
      }
    }
  }
  return res;
};

/**
 * Convert drawing details from v0.3 to v0.4.
 * - v0.3: properties at group root
 * - v0.4: properties in group meta object
 *
 * @param {Array} details An array of drawing details.
 * @returns {object} The converted drawings.
 */
dwv.io.v03Tov04DrawingsDetails = function (details) {
  var res = {};
  var keys = Object.keys(details);
  // Iterate over each position-groups
  for (var k = 0, lenk = keys.length; k < lenk; ++k) {
    var detail = details[keys[k]];
    res[keys[k]] = {
      meta: {
        textExpr: detail.textExpr,
        longText: detail.longText,
        quantification: detail.quant
      }
    };
  }
  return res;
};

/**
 * Convert drawing from v0.4 to v0.5.
 * - v0.4: position as object
 * - v0.5: position as array
 *
 * @param {Array} data An array of drawing.
 * @returns {object} The converted drawings.
 */
dwv.io.v04Tov05Data = function (data) {
  var pos = data.position;
  data.position = [pos.i, pos.j, pos.k];
  return data;
};

/**
 * Convert drawing from v0.4 to v0.5.
 * - v0.4: draw id as 'slice-0_frame-1'
 * - v0.5: draw id as '#2-0_#3-1''
 *
 * @param {Array} inputDrawings An array of drawing.
 * @returns {object} The converted drawings.
 */
dwv.io.v04Tov05Drawings = function (inputDrawings) {
  // Iterate over each position-groups
  var posGroups = inputDrawings.children;
  for (var k = 0, lenk = posGroups.length; k < lenk; ++k) {
    var posGroup = posGroups[k];
    var id = posGroup.attrs.id;
    var ids = id.split('_');
    var sliceNumber = parseInt(ids[0].substring(6), 10); // 'slice-0'
    var frameNumber = parseInt(ids[1].substring(6), 10); // 'frame-0'
    var newId = '#2-';
    if (sliceNumber === 0 && frameNumber !== 0) {
      newId += frameNumber;
    } else {
      newId += sliceNumber;
    }
    posGroup.attrs.id = newId;
  }
  return inputDrawings;
};