Home Reference Source Test Repository

src/js/util/graph-serialize.js

const nSerial = require('node-serialize');
import { Graph } from '../data/graph';
import { Node } from '../data/node/node';
import { Edge } from '../data/edge/edge';
import { Label } from '../data/label';

import { CircleNode } from '../data/node/circle-node';
import { TriangleNode } from '../data/node/triangle-node';
import { SquareNode } from '../data/node/square-node';
import { DiamondNode } from '../data/node/diamond-node';
import { PentagonNode } from '../data/node/pentagon-node';
import { HexagonNode } from '../data/node/hexagon-node';
import { OctagonNode } from '../data/node/octagon-node';

import { SolidEdge } from '../data/edge/solid-edge';
import { DashedEdge } from '../data/edge/dashed-edge';

let CIRCULARFLAG = '_$$ND_CC$$_';
let KEYPATHSEPARATOR = '_$$.$$_';
let IDTYPE = '_$$TYPE$$_';
let IDSET = '_$$SET$$_';
let IDNODE = '_$$NODE$$_';
let IDEDGE = '_$$EDGE$$_';
let IDLABEL = '_$$LABEL$$_';
let IDDATA = '_$$DATA$$_';

/**
 * Serializer class takes care of saving
 * and loading graph via JSON manipulation
 * @class Serializer
 */
export class Serializer {

  static classesByName = {
    CircleNode: CircleNode,
    TriangleNode: TriangleNode,
    SquareNode: SquareNode,
    DiamondNode: DiamondNode,
    PentagonNode: PentagonNode,
    HexagonNode: HexagonNode,
    OctagonNode: OctagonNode,
    SolidEdge: SolidEdge,
    DashedEdge: DashedEdge,
    Label: Label
  };

  /**
   * Serializer constructor
   * @param {Graph} graph Current graph structure
   * @param {function} resetFn Function to reset graph
   */
  constructor(graph, resetFn) {
    this.currentGraph = graph;
    this.resetFn = resetFn;
    this.quickString = '';

    if (typeof document !== 'undefined') {
      this.reader = new FileReader();
      this.reader.addEventListener('load', (event) => {
        this.uploadGraph();
      });

      this.uploader = document.getElementById('graph-uploader');
      this.uploader.addEventListener('change', (event) => {
        this.reader.readAsText(this.uploader.files[0]);
      });
      this.uploader.addEventListener('click', (event) => {
        this.uploader.value = null;
      });

      this.downloadBtn = document.getElementById('load-graph-button');
      this.downloadBtn.addEventListener('click', (event) => {
        this.uploader.click();
      });
      this.downloadBtn = document.getElementById('save-graph-button');
      this.downloadBtn.addEventListener('click', (event) => {
        this.downloadGraph();
      });
      this.importBtn = document.getElementById('import-graph-button');
      this.importBtn.addEventListener('click', (event) => {
        this.importGraph();
      });
      this.exportBtn = document.getElementById('export-graph-button');
      this.exportBtn.addEventListener('click', (event) => {
        this.exportGraph();
      });
    }
  }

   /**
   * Exports element of graph in object form
   * @param {Object} elem The element of graph to be exported
   * @param {Object} cache A remnant of the original serializer
   * @param {string} path A remnant of the original serializer
   * @returns {Object} outputObj Contains the element object data
   */
  exportElement(elem, cache, path) {
    let outputObj = {};
    let key;
    let modKey;
    let setElem;
    let outElem;
    cache[path] = elem;

    for (let name of Object.keys(Serializer.classesByName)) {
      if (elem instanceof Serializer.classesByName[name]) {
        outputObj[IDTYPE] = name;
        break;
      }
    }

    for (key in elem) {
      if (elem.hasOwnProperty(key)) {
        if (typeof elem[key] === 'object' && elem[key] !== null) {
          if (elem[key] instanceof Label) {
            outputObj[IDLABEL + key] = this.exportElement(elem[key], cache, path + KEYPATHSEPARATOR + IDLABEL + key);
          } else if (elem[key] instanceof Node) {
            // Node Field Detected
            outputObj[IDNODE + key] = elem[key].id;
          } else if (elem[key] instanceof Edge) {
            // Edge Field Detected
            outputObj[IDEDGE + key] = elem[key].id;
          } else if (elem[key] instanceof Array || elem[key] instanceof Set) {
            if (elem[key] instanceof Set) {
              modKey = IDSET + key;
            } else {
              modKey = key;
            }
            outputObj[modKey] = [];
            for (setElem of elem[key]) {
              if (setElem instanceof Node) {
                // Node-Array Field Detected
                outElem = IDNODE + setElem.id.toString();
              } else if (setElem instanceof Edge) {
                // Edge-Array Field Detected
                outElem = IDEDGE + setElem.id.toString();
              } else {
                // Primitive-Array Field Detected
                outElem = setElem;
              }
              outputObj[modKey].push(outElem);
            }
          } else {
            outputObj[key] = nSerial.serialize(elem[key], false, outputObj[key], cache, path + KEYPATHSEPARATOR + key);
          }
        } else {
          outputObj[key] = elem[key];
        }
      }
    }
    return outputObj;
  }

  /**
   * Is called when user wants to download the graph
   */
  downloadGraph() {
    let graphStr = JSON.stringify(this.serializeGraph());
    let element = document.createElement('a');
    element.setAttribute('href', 'data:application/json;charset=utf-8,' + encodeURIComponent(graphStr));
    element.setAttribute('download', 'graphdata.json');

    element.style.display = 'none';
    document.body.appendChild(element);

    element.click();

    document.body.removeChild(element);
  }

  /**
   * Sets value of textbox to the serialzed graph in JSON form
   */
  exportGraph() {
    let graphStr = JSON.stringify(this.serializeGraph());
    this.quickString = graphStr;
  }

  /**
   * Abstracts graph into outputOBj
   * @returns {Object} outputObj JSON object that can be stringified, contains graph data
   */
  serializeGraph() {
    let obj = this.currentGraph;
    let cache = {};
    let path = '$';
    let outputObj = {};
    let key;
    let modKey;
    let setElem;
    let outElem;
    cache[path] = obj;

    for (key in obj) {
      if (obj.hasOwnProperty(key)) {
        if (typeof obj[key] === 'object' && obj[key] !== null) {
          if (obj[key] instanceof Set) {
            modKey = IDSET + key;
            outputObj[modKey] = [];
            for (setElem of obj[key]) {
              if (setElem instanceof Node || setElem instanceof Edge) {
                outElem = this.exportElement(setElem, cache,
                    path + KEYPATHSEPARATOR + modKey + KEYPATHSEPARATOR + outputObj[modKey].length.toString());
              } else {
                outElem = nSerial.serialize(setElem, false, outElem, cache,
                    path + KEYPATHSEPARATOR + modKey + KEYPATHSEPARATOR + outputObj[modKey].length.toString());
                // Foreign Set Object
              }
              outputObj[modKey].push(outElem);
            }
          } else {
            outputObj[key] = nSerial.serialize(obj[key], false, outputObj[key], cache, path + KEYPATHSEPARATOR + key);
          }
        } else {
          outputObj[key] = obj[key];
        }
      }
    }
    return outputObj;
  }

  /**
   * Helper function to construct empty objects by class name
   * @param {string} name Name of element type to allocate
   * @returns {Object} outObj The constructed object
   */
  allocateElement(name) {
    let outObj = null;
    // Element allocation of type has type 'name'
    if (name.indexOf('Node') >= 0) {
      outObj = new Serializer.classesByName[name](0, 0);
    } else if (name.indexOf('Edge') >= 0) {
      outObj = new Serializer.classesByName[name](null, null);
    } else if (name.indexOf('Label') >= 0) {
      outObj = new Serializer.classesByName[name](0, 0, null);
    }
    return outObj;
  }

  /**
   * Reads the elem contents and converts it to a raw JSON object newElem
   * @param {Object} elem The element of graph to be imported
   * @param {Object} newElem The return element and contains the imported data
   * @param {Object} nodeCache Allows for loading references to existing nodes
   * @param {Object} edgeCache Allows for loading references to existing edges
   * @returns {Object} newElem A fully initialized graph element.
   */
  importElement(elem, newElem, nodeCache, edgeCache) {
    let key;
    let modKey;
    let refKey;
    let arrayElem;
    let addObj;
   // Element found of type 'elem[IDTYPE]'
    delete elem[IDTYPE];
    for (key in elem) {
      if (elem.hasOwnProperty(key)) {
        if (key.indexOf(IDLABEL) === 0) {
          modKey = key.substring(IDLABEL.length);
          newElem[modKey] = this.importElement(elem[key], this.allocateElement(elem[key][IDTYPE]), nodeCache, edgeCache);
        } else if (key.indexOf(IDNODE) === 0) {
          modKey = key.substring(IDNODE.length);
          newElem[modKey] = nodeCache[elem[key]];
          // Inner Node found called
        } else if (key.indexOf(IDEDGE) === 0) {
          modKey = key.substring(IDEDGE.length);
          newElem[modKey] = edgeCache[elem[key]];
          // Inner Edge found called
        } else if (elem[key] instanceof Array) {
          if (key.indexOf(IDSET) === 0) {
            // Inner Set found
            modKey = key.substring(IDSET.length);
            newElem[modKey] = new Set();
          } else {
            // Inner Array found
            modKey = key;
            newElem[modKey] = [];
          }
          for (arrayElem of elem[key]) {
            if (typeof arrayElem === 'string' || arrayElem instanceof String) {
              if (arrayElem.indexOf(IDNODE) === 0) {
                refKey = Number(arrayElem.substring(IDNODE.length));
                addObj = nodeCache[refKey];
                // Inner-Set Node found
              } else if (arrayElem.indexOf(IDEDGE) === 0) {
                refKey = Number(arrayElem.substring(IDEDGE.length));
                addObj = edgeCache[refKey];
                // Inner-Set Edge found
              } else {
                addObj = nSerial.unserialize(arrayElem, elem);
              }
            } else {
              addObj = nSerial.unserialize(arrayElem, elem);
            }
            if (newElem[modKey] instanceof Set) {
              newElem[modKey].add(addObj);
            } else {
              newElem[modKey].push(addObj);
            }
          }
        } else if (typeof elem[key] === 'string' || elem[key] instanceof String) {
          // Inner String found
          if (elem[key].indexOf(CIRCULARFLAG) === 0) {
            throw new Error('Can\'t deserialize a circular dependency in the top-level graph.');
          } else {
            newElem[key] = elem[key];
          }
        } else if (key === 'isSelected' || key === 'showTextCtrl') {
          // Inner Generic found
          newElem[key] = false;
        } else {
          newElem[key] = nSerial.unserialize(elem[key], elem);
        }
      }
    }
    return newElem;
  }

  /**
   * Will not run if the JSON reader fails. Loads the graph from JSON file
   */
  uploadGraph() {
    let obj;
    let deserializeInfo;
    if (typeof this.reader.result === 'undefined' || this.reader.result === '') {
      return;
    }
    try {
      obj = JSON.parse(this.reader.result);
      deserializeInfo = this.deserializeGraph(obj);
    } catch (ex) {
      // Exception thrown from parser or deserializer. Abort.
      throw ex;
    }
    // Graph Upload OK
    Node.numNodes = deserializeInfo.nodes;
    Edge.numEdges = deserializeInfo.edges;
    this.resetFn(deserializeInfo.graph);
  }

  /**
   * Imports the graph from JSON string
   */
  importGraph() {
    let obj;
    let deserializeInfo;
    if (this.quickString === '') {
      throw new Error('Nothing has been saved');
    }
    try {
      obj = JSON.parse(this.quickString);
      deserializeInfo = this.deserializeGraph(obj);
    } catch (ex) {
      // Exception thrown from parser or deserializer. Abort.
      throw ex;
    }
    // Graph Quickload OK
    Node.numNodes = deserializeInfo.nodes;
    Edge.numEdges = deserializeInfo.edges;
    this.resetFn(deserializeInfo.graph);
  }

  /**
   * Reads the graph data and creates new elements accordingly
   * @param {Object} obj The pre-parsed serial graph data
   * @returns {Object} importResult Data containing the graph and nodes and edge counts
   */
  deserializeGraph(obj) {
    let importResult;
    let newGraph = new Graph();
    let key;
    let modKey;
    let maxNodeID = 0;
    let nodeCache = {};
    let maxEdgeID = 0;
    let edgeCache = {};
    let elem;
    let newElem;
    if (obj === null || obj === {}) {
      // Bad Object
      return null;
    }
    for (key in obj) {
      if (obj.hasOwnProperty(key)) {
        if (key.indexOf(IDSET) === 0 && obj[key] instanceof Array) {
          modKey = key.substring(IDSET.length);
          // Set found
          newGraph[modKey] = new Set();
          for (elem of obj[key]) {
            if (elem[IDTYPE]) {
              newElem = this.allocateElement(elem[IDTYPE]);
              newElem.id = elem.id;
              newElem[IDDATA] = elem;
              if (newElem instanceof Node) {
               // Cached Node with ID
                nodeCache[newElem.id] = newElem;
                maxNodeID = Math.max(maxNodeID, newElem.id);
              } else if (newElem instanceof Edge) {
                // Cached Edge with ID
                edgeCache[newElem.id] = newElem;
                maxEdgeID = Math.max(maxEdgeID, newElem.id);
              }
            } else {
              newElem = nSerial.unserialize(elem, obj);
            }
            if (newElem !== null) {
              newGraph[modKey].add(newElem);
            }
          }
        } else if (typeof obj[key] === 'string' || obj[key] instanceof String) {
          if (obj[key].indexOf(CIRCULARFLAG) === 0) {
            throw new Error('Can\'t deserialize a circular dependency in the top-level graph.');
          } else {
            newGraph[key] = obj[key];
          }
        } else {
          newGraph[key] = nSerial.unserialize(obj[key], obj);
        }
      }
    }
    for (key in newGraph) {
      if (newGraph.hasOwnProperty(key)) {
        if (newGraph[key] instanceof Set) {
          // Iterating Set
          for (elem of newGraph[key]) {
            if (elem[IDDATA]) {
              this.importElement(elem[IDDATA], elem, nodeCache, edgeCache);
              delete elem[IDDATA];
            }
          }
        }
      }
    }

    if (!newGraph.validate()) {
      throw new Error('New Graph failed validation check');
    }

    importResult = {
      nodes: maxNodeID + 1,
      edges: maxEdgeID + 1,
      graph: newGraph
    };
    return importResult;
  }

  /**
   * Resets the graph
   * @param {Graph} newGraph The graph data to reset with
   */
  resetGraph(newGraph) {
     // Formality, in case it's triggered by something other than us.
    if (newGraph instanceof Graph) {
      this.currentGraph = newGraph;
    }
  }

}