package name.abuchen.portfolio.model;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import name.abuchen.portfolio.model.Taxonomy.Visitor;
import name.abuchen.portfolio.money.Values;
import name.abuchen.portfolio.util.ColorConversion;
public class Classification implements Named
{
public static final class ByRank implements Comparator<Classification>, Serializable
{
private static final long serialVersionUID = 1L;
@Override
public int compare(Classification c1, Classification c2)
{
return c1.getRank() > c2.getRank() ? -1 : c1.getRank() < c2.getRank() ? 1 : 0;
}
}
public static class Assignment
{
private InvestmentVehicle investmentVehicle;
private int weight;
private int rank;
public Assignment()
{
// needed for xstream de-serialization
}
public Assignment(InvestmentVehicle vehicle)
{
this(vehicle, ONE_HUNDRED_PERCENT);
}
public Assignment(InvestmentVehicle vehicle, int weight)
{
this.weight = weight;
this.investmentVehicle = vehicle;
}
public int getWeight()
{
return weight;
}
public void setWeight(int weight)
{
this.weight = weight;
}
public InvestmentVehicle getInvestmentVehicle()
{
return investmentVehicle;
}
public int getRank()
{
return rank;
}
public void setRank(int rank)
{
this.rank = rank;
}
}
public static final int ONE_HUNDRED_PERCENT = 100 * Values.Weight.factor();
public static final String UNASSIGNED_ID = "$unassigned$"; //$NON-NLS-1$
public static final String VIRTUAL_ROOT = "$virtualroot$"; //$NON-NLS-1$
private String id;
private String name;
private String description;
private String color;
private Classification parent;
private List<Classification> children = new ArrayList<>();
private List<Assignment> assignments = new ArrayList<>();
private int weight;
private int rank;
public Classification()
{
// needed for xstream de-serialization
}
public Classification(String id, String name)
{
this(null, id, name);
}
public Classification(Classification parent, String id, String name, String color)
{
this.parent = parent;
this.id = id;
this.name = name;
this.color = color;
if (color == null)
{
Random r = new Random();
this.color = '#' + Integer.toHexString(((r.nextInt(128) + 127) << 16) //
| ((r.nextInt(128) + 127) << 8) //
| (r.nextInt(128) + 127));
}
this.weight = ONE_HUNDRED_PERCENT;
}
public Classification(Classification parent, String id, String name)
{
this(parent, id, name, null);
}
public String getId()
{
return id;
}
/* package */void setId(String id)
{
this.id = id;
}
@Override
public String getName()
{
return name;
}
@Override
public void setName(String name)
{
this.name = name;
}
@Override
public String getNote()
{
return description;
}
@Override
public void setNote(String note)
{
this.description = note;
}
public String getColor()
{
return color;
}
public void setColor(String color)
{
this.color = color;
}
public Classification getParent()
{
return parent;
}
public void setParent(Classification parent)
{
this.parent = parent;
}
public List<Classification> getChildren()
{
return children;
}
public void addChild(Classification classification)
{
children.add(classification);
}
public List<Assignment> getAssignments()
{
return assignments;
}
public void addAssignment(Assignment assignment)
{
assignments.add(assignment);
}
public void removeAssignment(Assignment assignment)
{
assignments.remove(assignment);
}
public int getWeight()
{
return weight;
}
public int getChildrenWeight()
{
int sum = 0;
for (Classification child : children)
sum += child.getWeight();
return sum;
}
public void setWeight(int weight)
{
this.weight = weight;
}
public int getRank()
{
return rank;
}
public void setRank(int rank)
{
this.rank = rank;
}
public String getPathName(boolean includeParent, int limit)
{
LinkedList<Classification> path = getPath();
// remove root node
if (!includeParent && path.size() > 1)
path.removeFirst();
// short circuit
if (path.size() == 1)
return path.get(0).getName();
// add as many elements from left to right as possible
int available = limit;
StringBuilder leftBuffer = new StringBuilder();
StringBuilder rightBuffer = new StringBuilder();
int left = 0;
int right = 0;
while (left + right < path.size())
{
if ((left + right) % 2 == 0) // start right
{
// do right
Classification c = path.get(path.size() - 1 - right);
available -= c.getName().length();
if (available < 0)
break;
if (rightBuffer.length() > 0)
rightBuffer.insert(0, " » "); //$NON-NLS-1$
rightBuffer.insert(0, c.getName());
right++;
}
else
{
// do left
Classification c = path.get(left);
available -= c.getName().length();
if (available < 0)
break;
if (leftBuffer.length() > 0)
leftBuffer.append(" » "); //$NON-NLS-1$
leftBuffer.append(c.getName());
left++;
}
}
if (left + right == path.size())
return leftBuffer.toString() + " » " + rightBuffer.toString(); //$NON-NLS-1$
else
return leftBuffer.toString() + " ... " + rightBuffer.toString(); //$NON-NLS-1$
}
public String getPathName(boolean includeParent)
{
LinkedList<Classification> path = getPath();
if (!includeParent && path.size() > 1)
path.removeFirst();
StringBuilder buf = new StringBuilder();
for (Classification c : path)
{
if (buf.length() > 0)
buf.append(" » "); //$NON-NLS-1$
buf.append(c.getName());
}
return buf.toString();
}
private LinkedList<Classification> getPath()
{
LinkedList<Classification> path = new LinkedList<Classification>();
Classification c = this;
while (c != null)
{
path.addFirst(c);
c = c.getParent();
}
return path;
}
public List<Classification> getTreeElements()
{
List<Classification> answer = new ArrayList<Classification>();
LinkedList<Classification> stack = new LinkedList<Classification>();
stack.addAll(getChildren());
while (!stack.isEmpty())
{
Classification c = stack.pop();
answer.add(c);
stack.addAll(0, c.getChildren());
}
return answer;
}
public List<Classification> getPathToRoot()
{
LinkedList<Classification> path = new LinkedList<Classification>();
Classification item = this;
while (item != null)
{
path.addFirst(item);
item = item.getParent();
}
return path;
}
public void assignRandomColors()
{
Random random = new Random();
float hue = random.nextFloat() * 360f;
float saturation = (random.nextFloat() * 0.5f) + 0.3f;
float brightness = (random.nextFloat() * 0.4f) + 0.5f;
assignRandomColors(hue, saturation, brightness);
}
/* package */void assignRandomColors(float hue, float saturation, float brightness)
{
if (children.isEmpty())
return;
Collections.sort(children, new ByRank());
int size = children.size();
float step = 360f / (float) size;
int index = 0;
for (Classification child : children)
{
float h = (hue + (step * index)) % 360f;
child.setColor(ColorConversion.toHex(h, saturation, brightness));
child.cascadeColorDown(h, saturation, brightness);
index++;
}
}
public void cascadeColorDown()
{
if (children.isEmpty())
return;
float[] hsb = ColorConversion.toHSB(color);
cascadeColorDown(hsb[0], hsb[1], hsb[2]);
}
private void cascadeColorDown(float hue, float saturation, float brightness)
{
if (children.isEmpty())
return;
float childSaturation = Math.max(0f, saturation - 0.1f);
float childBrightness = Math.min(1f, brightness + 0.1f);
for (Classification child : children)
{
child.setColor(ColorConversion.toHex(hue, childSaturation, childBrightness));
child.cascadeColorDown(hue, childSaturation, childBrightness);
}
}
public void accept(Visitor visitor)
{
visitor.visit(this);
for (Classification child : new ArrayList<Classification>(children))
child.accept(visitor);
for (Assignment assignment : new ArrayList<Assignment>(assignments))
visitor.visit(this, assignment);
}
@Override
public String toString()
{
return name;
}
/**
* Recursively creates a copy of the classification including all
* assignments with newly generated UUIDs.
*/
public Classification copy()
{
Classification copy = new Classification(null, UUID.randomUUID().toString(), this.name, this.color);
copy.rank = this.rank;
copy.weight = this.weight;
for (Classification classification : children)
{
Classification c = classification.copy();
c.setParent(copy);
copy.addChild(c);
}
for (Assignment assignment : assignments)
{
Assignment a = new Assignment(assignment.getInvestmentVehicle());
a.setWeight(assignment.getWeight());
a.setRank(assignment.getRank());
copy.addAssignment(a);
}
return copy;
}
}