/*
* Copyright (C) 2007 Snorre Gylterud, Stein Magnus Jodal, Johannes Knutsen,
* Erik Bagge Ottesen, Ralf Bjarne Taraldset, and Iterate AS
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2
* as published by the Free Software Foundation.
*/
package no.ntnu.mmfplanner.ui.graph;
import java.awt.Color;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import edu.umd.cs.piccolo.PNode;
import no.ntnu.mmfplanner.model.Category;
import no.ntnu.mmfplanner.model.Mmf;
import no.ntnu.mmfplanner.model.Project;
/**
* This class handles movement and updating of the decomposition graph.
*/
public class DecompositionGraphNode extends ProjectGraphNode {
/**
* @param project
*/
public DecompositionGraphNode(Project project) {
super(project);
setPaint(Color.WHITE);
}
private static final long serialVersionUID = 1L;
/**
* Removes all nodes and re-adds them to the graph. Piccolo will call
* layoutChildren() afterwards to place the children appropriately.
*
* All categories and MMFs that have a parent category are added as children
* of that categories nodes. This ensure that layoutChildren() will work
* correctly.
*/
@Override
protected void invalidateModel() {
// XXX: should we update only the relevant nodes? in a project with
// dependencies this could be quite a lot of nodes anyway -bagge
removeAllChildren();
// add all categories
HashMap<Category, CategoryNode> catNodes = new HashMap<Category, CategoryNode>();
while (catNodes.size() < project.getCategorySize()) {
for (int i = 0; i < project.getCategorySize(); i++) {
Category category = project.getCategory(i);
CategoryNode parent = catNodes.get(category.getParent());
if (catNodes.containsKey(category)
|| ((null == parent) && (null != category.getParent()))) {
continue;
}
CategoryNode node = new CategoryNode(category);
catNodes.put(category, node);
if (null == parent) {
addChild(node);
} else {
parent.addChild(node);
// link category->category
addChild(new LinkNode(parent, node));
}
}
}
// move all childless category nodes to the beginning of the list
int pos = 0;
for (int i = 0; i < getChildrenCount(); i++) {
PNode node = getChild(i);
if (node instanceof CategoryNode) {
if (node.getChildrenCount() == 0) {
removeChild(node);
addChild(pos, node);
pos++;
}
}
}
// add all MMFs
for (int i = 0; i < project.size(); i++) {
Mmf mmf = project.get(i);
CategoryNode parent = catNodes.get(mmf.getCategory());
MmfNode node = new MmfNode(mmf);
if (null == parent) {
addChild(node);
} else {
parent.addChild(node);
// link category->mmf
addChild(new LinkNode(parent, node));
}
}
}
/**
* Lays out all children in a hierarchical decomposition graph. Nodes are
* arranged using layoutNode() and edges are updated afterwards. This also
* updates it's own bounds according to the placement of the children.
*/
@Override
protected void layoutChildren() {
// layout all top nodes (not LinkNodes)
double firstX[] = new double[project.getCategorySize() + 2];
for (int i = 0; i < getChildrenCount(); i++) {
PNode node = getChild(i);
if (!(node instanceof LinkNode)) {
layoutNode(node, 0, firstX);
}
}
// Update all LinkNodes
for (int i = 0; i < getChildrenCount(); i++) {
PNode node = getChild(i);
if (node instanceof LinkNode) {
((LinkNode) node).updateLine();
}
}
// Update bounds
double maxX = 0;
for (int i = 0; i < firstX.length; i++) {
if (firstX[i] > maxX) {
maxX = firstX[i];
} else if (firstX[i] == 0) {
double height = i
* (CategoryNode.HEIGHT + CategoryNode.PADDING_HEIGHT);
setBounds(0, 0, maxX, height);
break;
}
}
}
/**
* Arranges the layout for a single node. Recursively calls itself to
* arrange all children first, and then the parent. Leaves are simply placed
* at the first available spot. As all nodes are arranged from left to
* right, this ensures the leaves are grouped together under their parent.
*
* This version tries to always align the parent in the exact center of it's
* leftmost and rightmost children.
*
* @param node the node to arrange
* @param level the current level (0 = top)
* @param firstX first available absolute x-position
*/
@SuppressWarnings("unchecked")
private void layoutNode(PNode node, int level, double firstX[]) {
double x = 0;
if (0 == node.getChildrenCount()) {
// leaves are placed at first available spot
x = firstX[level];
} else {
// sort the children and find the first x-position for next level
List<PNode> children = sortChildrenNonMoving(node
.getChildrenReference());
firstX[level + 1] += CategoryNode.PADDING_WIDTH / 2;
double max = firstX[level]
- ((children.size() - 1)
* (CategoryNode.WIDTH + CategoryNode.PADDING_WIDTH / 2) / 2.0);
firstX[level + 1] = Math.max(firstX[level + 1], max);
double xx[] = new double[children.size()];
// lay out all the children in a consecutive row, might contain gaps
// due to other children
int xi = 0;
for (PNode child : children) {
layoutNode(child, level + 1, firstX);
xx[xi] = firstX[level + 1];
xi++;
}
// reposition leave nodes to fill out blank spaces
int first = 0, last = 0;
for (int i = 1; i < children.size(); i++) {
int childCount = children.get(i).getChildrenCount();
if ((childCount == 0) && (first == 0)) {
first = i;
} else if (childCount > 0) {
last = i - 1;
break;
}
}
if ((first > 0) && (last >= first)) {
double dx = (xx[last + 1] - xx[last] - (CategoryNode.WIDTH + CategoryNode.PADDING_WIDTH / 2))
/ (last - first + 2);
if (dx > 1.0) {
for (int i = first; i <= last; i++) {
children.get(i).setX(
children.get(i).getX() + dx * (i - first + 1));
}
}
}
// place this node in the center of all the children
x = (xx[0] + xx[xx.length - 1]) / 2
- (CategoryNode.WIDTH + CategoryNode.PADDING_WIDTH / 2);
}
firstX[level] = (x + CategoryNode.WIDTH + CategoryNode.PADDING_WIDTH / 2);
double px = x + CategoryNode.PADDING_WIDTH / 4;
double py = level * (CategoryNode.HEIGHT + CategoryNode.PADDING_HEIGHT)
+ CategoryNode.PADDING_HEIGHT / 2;
node.setBounds(px, py, CategoryNode.WIDTH, CategoryNode.HEIGHT);
}
/**
* Places the first half of the children with grandchildren at the beginning
* of the row, then all the childless children, and last the rest of the
* children with grandchildren. Note that this does not rearrange nodes, and
* will produce a wider graph. However it doesn't have the problem of
* rearranging the children when adding or removing grandchildren.
*
* @param children
* @return sorted list according to the above criteria
*/
private List<PNode> sortChildrenNonMoving(List<PNode> children) {
List<PNode> childrenNC = new ArrayList<PNode>();
List<PNode> childrenWC = new ArrayList<PNode>();
// separate children with grandchildren from childless
for (PNode child : children) {
int childCount = child.getChildrenCount();
if (childCount > 0) {
childrenWC.add(child);
} else {
childrenNC.add(child);
}
}
// add the first half of the children with children at the beginning
// of the graph, and the other half at the end
int first = Math.max(1, childrenWC.size() / 2);
for (int i = 0; i < childrenWC.size(); i++) {
if (i < first) {
childrenNC.add(0, childrenWC.get(i));
} else {
childrenNC.add(childrenWC.get(i));
}
}
return childrenNC;
}
}