Home Reference Source Test Repository

src/js/data/graph.js

import Node from '../data/node/node';
import Edge from '../data/edge/edge';
import Label from '../data/label';

/**
 * Data representation of a graph.
 * @class Graph
 */
class Graph {

  /**
   * Constructs a Graph instance.
   * @param  {Set.<Node>} nodes - Iterable containing nodes to add.
   * @param  {Set.<Edge>} edges - Iterable containing edges to add.
   * @constructs Graph
   */
  constructor(nodes, edges) {
    this.nodes = new Set(nodes);
    this.edges = new Set(edges);
  }

  /**
   * Validate the graph. Called to check if imported graph is valid.
   * @return {boolean} - Whether or not the graph is valid.
   */
  validate() {
    if (!(this.nodes instanceof Set) || !(this.edges instanceof Set)) {
      return false;
    }
    for (let node of this.nodes) {
      if (!(node instanceof Node) || !(node.id >= 0)) {
        return false;
      }
      if (node.label !== null && !(node.label instanceof Label)) {
        return false;
      }
    }

    for (let edge of this.edges) {
      if (!(edge instanceof Edge) || !(edge.id >= 0)) {
        return false;
      }
      if (edge.label !== null && !(edge.label instanceof Label)) {
        return false;
      }
    }
    return true;
  }

  /**
   * Add a node to the graph.
   * @param {Node} node - The node to add.
   */
  addNode(node) {
    this.nodes.add(node);
  }

  /**
   * Add an edge to the graph.
   * @param {Edge} edge - The edge to add.
   */
  addEdge(edge) {
    if (!this.nodes.has(edge.startNode) || !this.nodes.has(edge.destNode)) {
      throw new Error('Edge nodes are not in the graph');
    }
    this.edges.add(edge);
  }

  /**
   * Remove a node from the graph.
   * @param  {Node} node - The node to remove.
   */
  removeNode(node) {
    // Temp copy of edges to work on while we remove them
    let tempEdges = new Set();
    for (let edge of node.edges) {
      tempEdges.add(edge);
    }
    for (let edge of tempEdges) {
      this.removeEdge(edge);
    }
    this.nodes.delete(node);
  }

  /**
   * Remove an edge from the graph.
   * @param  {Edge} edge - The edge to remove.
   */
  removeEdge(edge) {
    let id = edge.id;
    for (let i = 0; i < edge.partners.length; i++) {
      for (let j = 0; j < edge.partners[i].length; j++) {
        if (edge.partners[i].partners[j].id === id) {
          edge.partners[i].partners.splice(j, 1);
          edge.partners[i].updateEndpoints();
          break;
        }
      }
    }
    this.edges.delete(edge);
    edge.detach();
  }

  /**
   * Check if the graph has an edge from start to dest.
   * @param  {Node}  start - The start node.
   * @param  {Node}  dest - The dest node.
   * @return {boolean} - Whether or not the graph contains an edge from start to dest.
   */
  hasEdge(start, dest) {
    if (!this.nodes.has(start) || !this.nodes.has(dest)) {
      throw new Error('Nodes are not in the graph');
    }

    for (let edge of this.edges) {
      if (edge.startNode === start && edge.destNode === dest) {
        return true;
      }
    }
    return false;
  }

  /**
   * Check if the graph has a component at the given point.
   * @param  {number} x - x-coordinate of the point.
   * @param  {number} y - y-coordinate of the point.
   * @param  {(Node|Edge|Label)}  ignore - Object to ignore checking. This is to prevent returning true when dragging items.
   * @return {boolean} - Whether or not the graph has a component at the point.
   */
  hasComponent(x, y, ignore) {
    let component = this.getComponent(x, y);
    return component !== ignore && component !== null;
  }

  /**
   * Get the graph component at the given point, if it exists. If multiple objects overlap, the priority is (highest to lowest): Label, Node, Edge.
   * @param  {number} x - x-coordinate of the point.
   * @param  {number} y - y-coordinate of the point.
   * @return {(Node|Edge|Label)} - Object at the given point if it exists, null otherwise.
   */
  getComponent(x, y) {
    for (let node of this.nodes) {
      if (node.label.containsPoint(x, y)) {
        return node.label;
      }
    }

    for (let edge of this.edges) {
      if (edge.label.containsPoint(x, y)) {
        return edge.label;
      }
    }

    for (let node of this.nodes) {
      if (node.containsPoint(x, y)) {
        return node;
      }
    }

    for (let edge of this.edges) {
      if (edge.containsPoint(x, y)) {
        return edge;
      }
    }

    return null;
  }

  /**
   * Checks if there is a node collision by moving a node
   * @param  {Node} testNode - The node to check collisions for.
   * @param  {number} x - x-coordinate of the node.
   * @param  {number} y - y-coordinate of the node.
   * @return {boolean} - Whether or not there is a collision.
   */
  isNodeCollision(testNode, x, y) {
    let collision = false;
    this.forEachNode((node) => {
      if (node === testNode) {
        return true;
      }
      let nodePoint = node.edgePointInDirection(x, y);
      let testPoint = testNode.edgePointInDirection(node.x, node.y);
      if (testNode.containsPoint(nodePoint.x, nodePoint.y)
          || node.containsPoint(testPoint.x, testPoint.y)) {
        collision = true;
        return false;
      }
      return true;
    });
    return collision;
  }

  /**
   * Iterator for all graph nodes. Remaining nodes are skipped if the callback returns false.
   * @param {function(node: Node): boolean} callback - Called once for each node, with the node as a parameter. Return false to skip remaining nodes.
   */
  forEachNode(callback) {
    for (let node of this.nodes) {
      if (callback(node) === false) {
        break;
      }
    }
  }

  /**
   * Iterator for all graph edges. Remaining edges are skipped if the callback returns false.
   * @param {function(edge: Edge): boolean} callback - Called once for each edge, with the edge as a parameter. Return false to skip remaining edges.
   */
  forEachEdge(callback) {
    for (let edge of this.edges) {
      if (callback(edge) === false) {
        break;
      }
    }
  }

  /**
   * Draw the graph on the given canvas context.
   * @param  {CanvasRenderingContext2D} context - Canvas 2D context.
   */
  draw(context) {
    this.edges.forEach((edge) => {
      edge.draw(context);
    });

    this.nodes.forEach((node) => {
      node.draw(context);
    });
  }
}

export { Graph };
export default Graph;