/*
* 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.model;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The MMF model class
*
* Has the MMF properties. Changes are handled and notifies if changes are made.
*/
public class Mmf {
public static final String EVENT_ID = "mmf.id";
public static final String EVENT_NAME = "mmf.name";
public static final String EVENT_PERIOD = "mmf.period";
public static final String EVENT_PERIOD_COUNT = "mmf.periodCount";
public static final String EVENT_SWIMLANE = "mmf.swimlane";
public static final String EVENT_CATEGORY = "mmf.category";
public static final String EVENT_PRECURSORS = "mmf.precursors";
public static final String EVENT_REVENUES = "mmf.revenues";
public static final String EVENT_PROJECT = "mmf.project";
private static final String EVENT_LOCKED = "mmf.locked";
/**
* The unique ID of the MMF. This is used to identify the MMF and displayed
* in both the graph and table views. Some places this is the only
* identifier used.
*/
private String id;
/**
* The name of the MMF. Should concisely describe what the MMF does. This is
* displayed in the graph view.
*/
private String name;
/**
* A period is the basic time measurement used in MMFs. A period could be a
* week, a month, or any other period of time. This usually corresponds to
* one or more development iteration(s), so that there is enough time to
* finish at least one MMF. The periods could also be used more loosely,
* simply identifying the approximate order of work.
* <p>
* This value assigns period work on the MMF will start. This is where the
* first part of the MMF is drawn. If periodCount == 1 then work will also
* finish in this period.
*/
private int period;
/**
* The view is divided into several swimlanes from top to bottom. This
* parameter tells which swimlane the MMF will be drawn in. This parameter
* has no effect expect for positioning in the graph view.
*/
private int swimlane;
/**
* The category for the MMF
*/
private Category category;
/**
* List of precursors by id string. These are MMFs that have to be completed
* before this MMF. In the graph view this is shown with a directed edge
* from the precursors towards this MMF. Usually the MMF is positioned in a
* period after all precursors are completed.
*/
private List<Mmf> precursors;
/**
* ArrayList of integers representing the revenue for each period.
*/
private List<Integer> revenues;
/**
* The project this MMF belongs to.
*/
private Project project;
private PropertyChangeSupport changeSupport;
/**
* Specify a MMF as locked to it's period. Helps automatic sorting.
*/
private boolean locked;
/**
* Creates a new MMF with the given id and name.
*
* @param id
* @param name
*/
public Mmf(String id, String name) {
this.id = id;
this.name = name;
this.period = 1;
this.swimlane = 1;
this.precursors = new ArrayList<Mmf>();
this.category = null;
this.revenues = new ArrayList<Integer>();
this.locked = false;
this.changeSupport = new PropertyChangeSupport(this);
}
/**
* Returns a string representation of this MMF.
*/
@Override
public String toString() {
return "MMF " + id + ": " + name + " [" + period + "," + swimlane
+ "] > " + precursors;
}
public String getId() {
return id;
}
/**
* Sets the id and fires an EVENT_ID event. The id must a valid id, and not
* have duplicates in the project.
*
* @param id
* @throws MmfException
*/
public void setId(String id) throws MmfException {
if ((null != project) && !project.isValidId(id)) {
throw new MmfException("The id is not valid or has a duplicate: "
+ id);
}
String oldValue = this.id;
this.id = id;
changeSupport.firePropertyChange(EVENT_ID, oldValue, id);
}
public String getName() {
return name;
}
/**
* Sets the name and fires an EVENT_NAME event
*
* @param name
*/
public void setName(String name) {
String oldValue = this.name;
this.name = name;
changeSupport.firePropertyChange(EVENT_NAME, oldValue, name);
}
public int getPeriod() {
return period;
}
/**
* Sets the period and fires an EVENT_PERIOD event
*
* @param period
* Valid values of period are 1 <= period < ...
* @throws MmfException
*/
public void setPeriod(int period) throws MmfException {
if (period < 1) {
throw new MmfException("Invalid period: " + period);
}
if (period == this.period) {
return;
}
try {
if (project != null) {
setSwimlane(project.getFirstFreeSwimlane(period, swimlane));
}
} catch (MmfException e) {
// Should never happen!
e.printStackTrace();
}
int oldValue = this.period;
this.period = period;
changeSupport.firePropertyChange(EVENT_PERIOD, oldValue, period);
}
public int getSwimlane() {
return swimlane;
}
/**
* Sets the swimlane and fires an EVENT_SWIMLANE event
*
* @param swimlane
* Valid values of swimlane are 1 <= swimlane < ...
* @throws MmfException
*/
public void setSwimlane(int swimlane) throws MmfException {
if (swimlane < 1) {
throw new MmfException("Invalid swimlane: " + swimlane);
}
int oldValue = this.swimlane;
this.swimlane = swimlane;
changeSupport.firePropertyChange(EVENT_SWIMLANE, oldValue, swimlane);
}
public int getPeriodCount() {
int count = 1;
for (int p = 2; (p <= getRevenueLength()) && (getRevenue(p) < 0); p++) {
count++;
}
return count;
}
public List<Mmf> getPrecursors() {
return Collections.unmodifiableList(precursors);
}
/**
* @return all precursors as a comma-separated string of ids.
*/
public String getPrecursorString() {
if (precursors.size() == 0) {
return "";
} else {
String result = "";
for (Mmf mmfPre : precursors) {
result += ", " + mmfPre.getId();
}
return result.substring(2);
}
}
/**
* Sets all the precurors
*
* @param prestring
*/
public void setPrecursorString(String prestring) throws MmfException {
List<Mmf> newPrecursors = new ArrayList<Mmf>();
Pattern pattern = Pattern.compile("Z*[A-Y]");
Matcher matcher = pattern.matcher(prestring.toUpperCase());
// check validity of all new precursors
while (matcher.find()) {
Mmf preMmf = project.get(matcher.group());
if (newPrecursors.contains(preMmf)) {
continue;
}
checkValidPrecursor(preMmf);
newPrecursors.add(preMmf);
}
// replace existing list
this.precursors = newPrecursors;
changeSupport.firePropertyChange(EVENT_PRECURSORS, null, null);
}
/**
* Adds a precursor and fires an EVENT_PRECURSORS event. Will cause an
* exception if a circle of precedence will be created.
*
* @param precursor
* @throws MmfException
*/
public void addPrecursor(Mmf precursor) throws MmfException {
if (this.precursors.indexOf(precursor) < 0) {
checkValidPrecursor(precursor);
this.precursors.add(precursor);
changeSupport.firePropertyChange(EVENT_PRECURSORS, null, precursor);
}
}
/**
* Checks if the precursor is valid. Mostly that no circular precursors
* exists.
*
* @param precursor
* @throws MmfException
*/
private void checkValidPrecursor(Mmf precursor) throws MmfException {
if (null == precursor) {
throw new MmfException("Precursor does not exist");
} else if (this.getProject() != precursor.getProject()) {
throw new MmfException(
"Precursor is not a part of the same project");
} else if (this == precursor) {
throw new MmfException(
"MMF can not be a precursor to itself (circular precedence)");
}
List<Mmf> prePre = precursor.getPrecursors();
for (Mmf pre : prePre) {
checkValidPrecursor(pre);
}
}
/**
* Remove a precursor and fires an EVENT_PRECURSORS event.
*
* @param precursor
*/
public void removePrecursor(Mmf precursor) {
if (this.precursors.indexOf(precursor) >= 0) {
this.precursors.remove(precursor);
changeSupport.firePropertyChange(EVENT_PRECURSORS, precursor, null);
}
}
public Category getCategory() {
return category;
}
/**
* Sets the category of this MMF and fires an EVENT_CATEGORY event.
*
* @param category
* @throws MmfException
*/
public void setCategory(Category category) throws MmfException {
if ((null != project) && !project.isValidCategory(category)) {
throw new MmfException("Category is not a part of this project: "
+ category);
}
Category oldValue = this.category;
this.category = category;
changeSupport.firePropertyChange(EVENT_CATEGORY, oldValue, category);
}
/**
* Sets the revenue for the given period. Can be both positive and negative,
* and can be for a period beyond what is the periodCount in the project.
*
* @param period
* to set revenue for
* @param revenue
* value of revenue in this period
*/
public void setRevenue(int period, int revenue) {
while (period > revenues.size()) {
revenues.add(0);
}
int oldValue = this.getRevenue(period);
this.revenues.set(period - 1, revenue);
changeSupport.firePropertyChange(EVENT_REVENUES, oldValue, revenue);
}
public int getRevenue(int period) {
if (period > revenues.size()) {
return 0;
}
return revenues.get(period - 1);
}
public int getRevenueLength() {
return revenues.size();
}
/**
* Add a PropertyChangeListener to be notified of changes to this object
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
changeSupport.addPropertyChangeListener(listener);
}
/**
* Remove a PropertyChangeListener
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
changeSupport.removePropertyChangeListener(listener);
}
/**
* Returns a list of SANPV for this project for each start period. This
* function will call getSaNpv(int, int) for each possible start period.
*
* @param interestRate
*/
public int[] getSaNpvList(double interestRate) {
int periods = project.getPeriods();
int sanpv[] = new int[periods];
for (int p = 0; p < periods; p++) {
sanpv[p] = getSaNpv(interestRate, p);
}
return sanpv;
}
/**
* Returns the SANPV for the given start period and interest rate.
*
* @param interestRate
* @param skipPeriods
* @throws MmfException
*/
public int getSaNpv(double interestRate, int skipPeriods) {
if (skipPeriods < 0) {
throw new IllegalArgumentException("Invalid startPeriod: "
+ skipPeriods);
}
double npv = 0.0F;
for (int p = 1; p <= project.getPeriods() - skipPeriods; p++) {
int rev = getRevenue(p);
int per = (skipPeriods + p);
npv += rev / Math.pow(interestRate + 1, per);
}
return (int) Math.round(npv);
}
public Project getProject() {
return project;
}
/**
* Sets the project for this MMF and fires an EVENT_PROJECT event.
*
* @param project
*/
public void setProject(Project project) {
Project oldValue = this.project;
this.project = project;
changeSupport.firePropertyChange(EVENT_PROJECT, oldValue, project);
}
public boolean isLocked() {
return locked;
}
public void setLocked(boolean locked) {
boolean oldValue = this.locked;
this.locked = locked;
changeSupport.firePropertyChange(EVENT_LOCKED, oldValue, locked);
}
}