/**
   * Move the source graph into the destination graph using a specified offset.
   *
   * @param destGraph the destination graph.
   * @param sourceGraph the source graph.
   * @param offsetx x coordinate offset.
   * @param offsety y coordinate offset.
   */
  private void moveGraph(
      final FGraph destGraph,
      final FGraph sourceGraph,
      final double offsetx,
      final double offsety) {

    KVector graphOffset = new KVector(offsetx, offsety);
    graphOffset.sub(sourceGraph.getProperty(Properties.BB_UPLEFT));

    for (FNode node : sourceGraph.getNodes()) {
      node.getPosition().add(graphOffset);
      destGraph.getNodes().add(node);
    }

    for (FEdge edge : sourceGraph.getEdges()) {
      for (FBendpoint bendpoint : edge.getBendpoints()) {
        bendpoint.getPosition().add(graphOffset);
      }
      destGraph.getEdges().add(edge);
    }

    for (FLabel label : sourceGraph.getLabels()) {
      label.getPosition().add(graphOffset);
      destGraph.getLabels().add(label);
    }
  }
  /**
   * Split the given graph into its connected components.
   *
   * @param graph an input graph.
   * @return a list of components that can be processed one by one.
   */
  public List<FGraph> split(final FGraph graph) {
    Boolean separate = graph.getProperty(LayoutOptions.SEPARATE_CC);
    if (separate == null || separate.booleanValue()) {
      boolean[] visited = new boolean[graph.getNodes().size()];
      List<FEdge>[] incidence = buildIncidenceLists(graph);

      // perform DFS starting on each node, collecting connected components
      List<FGraph> components = new LinkedList<FGraph>();
      for (FNode node : graph.getNodes()) {
        FGraph comp = dfs(node, null, visited, incidence);
        if (comp != null) {
          comp.copyProperties(graph);
          components.add(comp);
        }
      }

      // redistribute identifier numbers to each component
      if (components.size() > 1) {
        for (FGraph comp : components) {
          int id = 0;
          for (FNode node : comp.getNodes()) {
            node.id = id++;
          }
        }
      }
      return components;
    }
    return Lists.newArrayList(graph);
  }
  /**
   * Pack the given components into a single graph.
   *
   * @param components a list of components.
   * @return a single graph that contains all components.
   */
  public FGraph recombine(final List<FGraph> components) {
    if (components.size() == 1) {
      return components.get(0);
    } else if (components.size() <= 0) {
      return new FGraph();
    }

    // assign priorities and sizes
    for (FGraph graph : components) {
      int priority = 0;
      double minx = Integer.MAX_VALUE,
          miny = Integer.MAX_VALUE,
          maxx = Integer.MIN_VALUE,
          maxy = Integer.MIN_VALUE;
      for (FNode node : graph.getNodes()) {
        Integer p = node.getProperty(LayoutOptions.PRIORITY);
        if (p != null) {
          priority += p;
        }
        minx = Math.min(minx, node.getPosition().x);
        miny = Math.min(miny, node.getPosition().y);
        maxx = Math.max(maxx, node.getPosition().x + node.getSize().x);
        maxy = Math.max(maxy, node.getPosition().y + node.getSize().y);
      }
      graph.setProperty(Properties.PRIORITY, priority);
      graph.setProperty(Properties.BB_UPLEFT, new KVector(minx, miny));
      graph.setProperty(Properties.BB_LOWRIGHT, new KVector(maxx, maxy));
    }

    // sort the components by their priority and size
    Collections.sort(
        components,
        new Comparator<FGraph>() {
          public int compare(final FGraph graph1, final FGraph graph2) {
            int prio =
                graph2.getProperty(Properties.PRIORITY) - graph1.getProperty(Properties.PRIORITY);
            if (prio == 0) {
              KVector size1 =
                  graph1
                      .getProperty(Properties.BB_LOWRIGHT)
                      .clone()
                      .sub(graph1.getProperty(Properties.BB_UPLEFT));
              KVector size2 =
                  graph2
                      .getProperty(Properties.BB_LOWRIGHT)
                      .clone()
                      .sub(graph2.getProperty(Properties.BB_UPLEFT));
              return Double.compare(size1.x * size1.y, size2.x * size2.y);
            }
            return prio;
          }
        });

    FGraph result = new FGraph();
    result.copyProperties(components.get(0));

    // determine the maximal row width by the maximal box width and the total area
    double maxRowWidth = 0.0f;
    double totalArea = 0.0f;
    for (FGraph graph : components) {
      KVector size =
          graph
              .getProperty(Properties.BB_LOWRIGHT)
              .clone()
              .sub(graph.getProperty(Properties.BB_UPLEFT));
      maxRowWidth = Math.max(maxRowWidth, size.x);
      totalArea += size.x * size.y;
    }
    maxRowWidth =
        Math.max(
            maxRowWidth,
            (float) Math.sqrt(totalArea) * result.getProperty(Properties.ASPECT_RATIO));
    double spacing = result.getProperty(Properties.SPACING).doubleValue();

    // place nodes iteratively into rows
    double xpos = 0, ypos = 0, highestBox = 0, broadestRow = spacing;
    for (FGraph graph : components) {
      KVector size =
          graph
              .getProperty(Properties.BB_LOWRIGHT)
              .clone()
              .sub(graph.getProperty(Properties.BB_UPLEFT));
      if (xpos + size.x > maxRowWidth) {
        // place the graph into the next row
        xpos = 0;
        ypos += highestBox + spacing;
        highestBox = 0;
      }
      moveGraph(result, graph, xpos, ypos);
      broadestRow = Math.max(broadestRow, xpos + size.x);
      highestBox = Math.max(highestBox, size.y);
      xpos += size.x + spacing;
    }

    return result;
  }