001: package com.technoetic.xplanner.charts;
002:
003: import org.jfree.chart.tooltips.CategoryToolTipGenerator;
004: import org.jfree.data.CategoryDataset;
005: import org.jfree.data.DefaultCategoryDataset;
006: import com.technoetic.xplanner.db.IterationStatisticsQuery;
007: import com.technoetic.xplanner.domain.Task;
008: import com.technoetic.xplanner.domain.TimeEntry;
009: import de.laures.cewolf.CategoryItemLinkGenerator;
010: import de.laures.cewolf.DatasetProduceException;
011: import de.laures.cewolf.DatasetProducer;
012: import org.apache.log4j.Category;
013:
014: import java.io.Serializable;
015: import java.text.SimpleDateFormat;
016: import java.util.Calendar;
017: import java.util.Collection;
018: import java.util.Date;
019: import java.util.GregorianCalendar;
020: import java.util.HashMap;
021: import java.util.Iterator;
022: import java.util.Map;
023:
024: public class TaskVelocityData implements DatasetProducer,
025: CategoryItemLinkGenerator, CategoryToolTipGenerator,
026: Serializable {
027: private static final Category log = Category
028: .getInstance("TaskVelocityData");
029:
030: private static final int COMPLETED_SERIES = 0;
031: private static final int REQUIRED_SERIES = 1;
032:
033: private String[] seriesNames = new String[REQUIRED_SERIES + 1];
034: private String[] seriesPrefix = new String[REQUIRED_SERIES + 1];
035: private SimpleDateFormat majorFormatter = null;
036:
037: private Integer[][] values = null;
038: private DefaultCategoryDataset dataSet = null;
039:
040: private static final SimpleDateFormat keyFormatter = new SimpleDateFormat(
041: "yyyy.MM.dd");
042:
043: // Utility methods ======================================================
044:
045: /**
046: * Indicates if the specified day of the week is a working day. Saturday and Sunday are assumed to not be
047: * working days and are not shown on the velocity graph.
048: *
049: * @param dayOfWeek the specified day (from the <code>Calendar</code> interface) to check the status of.
050: *
051: * @return <code>true</code> if the specified day is a work day, otherwise <code>false</code>.
052: */
053: private static boolean isWorkingDay(int dayOfWeek) {
054: if ((dayOfWeek == Calendar.SATURDAY)
055: || (dayOfWeek == Calendar.SUNDAY)) {
056: return false;
057: } else {
058: return true;
059: }
060: }
061:
062: /**
063: * Indicates if the specified day of the week is a the first day of the working week.
064: *
065: * @param dayOfWeek the specified day (from the <code>Calendar</code> interface) to check the status of.
066: *
067: * @return <code>true</code> if the specified day is the first day of the week, otherwise <code>false</code>.
068: */
069: private static boolean isFirstDayOfWeek(int dayOfWeek) {
070: if (dayOfWeek == Calendar.MONDAY) {
071: return true;
072: } else {
073: return false;
074: }
075: }
076:
077: /**
078: * Returns the number of working days between the sepcifed dates.
079: *
080: * @param startTime the start of the date range.
081: * @param endTime the end of the date range.
082: *
083: * @return the number of working days between the specified dates.
084: */
085: private static int getNumberOfWorkingDays(Date startTime,
086: Date endTime) {
087: Calendar start = new GregorianCalendar();
088: start.setTime(startTime);
089: Calendar end = new GregorianCalendar();
090: end.setTime(endTime);
091:
092: Calendar current = start;
093: int numWorkingDays = 0;
094:
095: while (current.before(end)) {
096: if (isWorkingDay(current.get(Calendar.DAY_OF_WEEK))) {
097: numWorkingDays++;
098: }
099:
100: current.add(Calendar.DATE, 1);
101: }
102:
103: return numWorkingDays;
104: }
105:
106: /**
107: * Generates a <code>HashMap</code> key from the supplied date.
108: *
109: * @param date the date for which a key is to be generated.
110: *
111: * @return the key associated with the specified date.
112: */
113: private static String buildKeyFromDate(Date date) {
114: return keyFormatter.format(date);
115: }
116:
117: /**
118: * Increments the value stored in the map at the specified date.
119: *
120: * @param dateValues the map which contains the set of existing dates and their associated values.
121: * @param dateToChange the date for which the amount is to be increment.
122: * @param incrementAmount the amount to increment by.
123: */
124: private void incrementValueOnDate(HashMap dateValues,
125: Date dateToChange, double incrementAmount) {
126: String dateKey = buildKeyFromDate(dateToChange);
127: Double currentAmount = (Double) dateValues.get(dateKey);
128:
129: if (currentAmount == null) {
130: currentAmount = new Double(incrementAmount);
131: } else {
132: currentAmount = new Double(currentAmount.doubleValue()
133: + incrementAmount);
134: }
135:
136: dateValues.put(dateKey, currentAmount);
137: }
138:
139: /**
140: * Returns then data at which the specified task was completed. The completed time is assumed to be the last
141: * time entry specified for the task rounded to the end of the day.
142: *
143: * @param task the task to be examined.
144: *
145: * @return The date at which the task was completed.
146: */
147: private Date getCompletedDate(Task task) {
148: Date completedTime = null;
149:
150: Collection timeEntries = task.getTimeEntries();
151: if (timeEntries != null) {
152: Iterator iter = timeEntries.iterator();
153:
154: while (iter.hasNext()) {
155: TimeEntry entry = (TimeEntry) iter.next();
156: Date endTime = entry.getEndTime();
157:
158: // This is a fix for duration-only entries
159: // It may not be quite right.
160: if (endTime == null && entry.getDuration() > 0) {
161: endTime = entry.getLastUpdateTime();
162: }
163:
164: if (endTime != null) {
165: if ((completedTime == null)
166: || (endTime.after(completedTime))) {
167: completedTime = endTime;
168: }
169: }
170: }
171: }
172:
173: if (completedTime == null) {
174: log.error("Asked for completion time of task ["
175: + task.getId()
176: + "] which wasn't marked as completed");
177: return null;
178: } else {
179: return completedTime;
180: }
181: }
182:
183: /**
184: * Returns a two dimensional array, for the category and series information, sized to the duration of the
185: * iteration. It is assumed that the data will only contain entries for working days.
186: *
187: * @param startTime the date at which the iteration started.
188: * @param endTime the date at which the iteration ended.
189: *
190: * @return A two dimensional array where the first dimension contains the hour values and the second
191: * dimension contains the series.
192: */
193: private Integer[][] initializeDataArray(Date startTime, Date endTime) {
194: return new Integer[seriesNames.length][getNumberOfWorkingDays(
195: startTime, endTime)];
196: }
197:
198: /**
199: * Addes the supplied data values to the specified series of the chart. The series are culuminative.
200: * The data supplied to create the series indicates the increment at each date in the series. Note,
201: * since the source date is doubles, but the series data can only contains whole numbers, some small
202: * rounding errors may occur.
203: *
204: * @param lineData two dimensional array to be populated with the series point information.
205: * @param seriesNum the index of the series in the line data to which the data should be added.
206: * @param startDate the date at which the first entry in the chart starts at.
207: * @param dataValues expected to contain a set of <code>Double</code> values keys by <code>Dates</code>.
208: * For example it may specify the effort (hours) complete on individual dates.
209: */
210: private void addSeriesData(Integer[][] lineData, int seriesNum,
211: Date startDate, HashMap dataValues) {
212: if (dataValues.size() == 0) {
213: return;
214: }
215:
216: // Step through the points of the series and accumulate their values one day at a time
217: //
218: Calendar now = new GregorianCalendar();
219: Calendar currentDate = new GregorianCalendar();
220: currentDate.setTime(startDate);
221: double cumulativeValue = 0.0;
222: int dayIndex = 0;
223:
224: while (dayIndex < lineData[seriesNum].length) {
225: Double value = (Double) dataValues
226: .get(buildKeyFromDate(currentDate.getTime()));
227:
228: if (value != null) {
229: cumulativeValue += value.doubleValue();
230: }
231:
232: if (isWorkingDay(currentDate.get(Calendar.DAY_OF_WEEK))) {
233: if (currentDate.getTimeInMillis() < now
234: .getTimeInMillis()) {
235: int pointValue = (int) Math.round(cumulativeValue);
236: lineData[seriesNum][dayIndex] = new Integer(
237: pointValue);
238: } else {
239: lineData[seriesNum][dayIndex] = null;
240: }
241: dayIndex++;
242: }
243:
244: currentDate.add(Calendar.DATE, 1);
245: }
246: }
247:
248: /**
249: * Returns the list of dates that make up the x-axis of the velocity graph. THe first day of each week
250: * is represented by the date all other days are represented by their index within the velocity.
251: *
252: * @param startDate the date at which the first entry in the chart starts at.
253: * @param lineData two dimensional array to be populated with the series point information.
254: *
255: * @return The names to be displayed for date entries on the x-axis.
256: */
257: private String[] createCategoryNames(Date startDate,
258: Integer[][] lineData) {
259: int numDays = lineData[0].length;
260: String[] categories = new String[numDays];
261:
262: Calendar currentDate = new GregorianCalendar();
263: currentDate.setTime(startDate);
264: int dayIndex = 0;
265:
266: while (dayIndex < numDays) {
267: int dayOfWeek = currentDate.get(Calendar.DAY_OF_WEEK);
268:
269: if (isWorkingDay(dayOfWeek)) {
270: if (isFirstDayOfWeek(dayOfWeek)) {
271: categories[dayIndex] = majorFormatter
272: .format(currentDate.getTime());
273: } else {
274: //categories[dayIndex] = Long.toString(dayIndex);
275: categories[dayIndex] = Long.toString(currentDate
276: .get(Calendar.DAY_OF_MONTH));
277: }
278: dayIndex++;
279: }
280:
281: currentDate.add(Calendar.DATE, 1);
282: }
283:
284: return categories;
285: }
286:
287: // Public methods =======================================================
288:
289: public void setStatistics(IterationStatisticsQuery statistics) {
290: // Before generating the graph data ensure all the required objects are set
291: //
292: seriesNames[COMPLETED_SERIES] = statistics
293: .getResourceString("iteration.statistics.velocity.series_completed");
294: seriesNames[REQUIRED_SERIES] = statistics
295: .getResourceString("iteration.statistics.velocity.series_required");
296:
297: seriesPrefix[COMPLETED_SERIES] = statistics
298: .getResourceString("iteration.statistics.velocity.prefix_completed");
299: seriesPrefix[REQUIRED_SERIES] = statistics
300: .getResourceString("iteration.statistics.velocity.prefix_required");
301:
302: majorFormatter = new SimpleDateFormat(statistics
303: .getResourceString("format.date"));
304:
305: Date velocityStart = statistics.getIteration().getStartDate();
306: Date velocityEnd = statistics.getIteration().getEndDate();
307:
308: // Build a list of all the dates at which tasks were added to the system. Any
309: // dates past the start or end of the iteration are added to the first and last
310: // day of the iteration.
311: //
312: Collection tasks = statistics.getIterationTasks();
313: HashMap effortAdded = new HashMap(tasks.size());
314:
315: Iterator iter = tasks.iterator();
316: while (iter.hasNext()) {
317: Task task = (Task) iter.next();
318: Date createdDate = task.getCreatedDate();
319:
320: if ((createdDate == null) || (createdDate.getTime() == 0)
321: || (createdDate.before(velocityStart))) {
322: createdDate = velocityStart;
323: } else if (createdDate.after(velocityEnd)) {
324: createdDate = velocityEnd;
325: }
326:
327: incrementValueOnDate(effortAdded, createdDate, task
328: .getEstimatedOriginalHours());
329: }
330:
331: // Build a list of all the dates at which tasks are completed.
332: //
333: HashMap effortCompleted = new HashMap(tasks.size());
334:
335: iter = tasks.iterator();
336: while (iter.hasNext()) {
337: Task task = (Task) iter.next();
338:
339: if (task.isCompleted()) {
340: Date completedDate = getCompletedDate(task);
341:
342: // If a task was completed but it didn't have any time entries then assume that
343: // it finished when it was created.
344: //
345: if (completedDate == null) {
346: completedDate = task.getCreatedDate();
347:
348: if ((completedDate == null)
349: || (completedDate.getTime() == 0)) {
350: completedDate = velocityStart;
351: }
352: }
353:
354: incrementValueOnDate(effortCompleted, completedDate,
355: task.getEstimatedOriginalHours());
356: }
357: }
358:
359: // Now that we know the range of the iteration we can go through each day calculating the
360: // hour completed and required on that single day.
361: //
362: values = initializeDataArray(velocityStart, velocityEnd);
363: addSeriesData(values, REQUIRED_SERIES, velocityStart,
364: effortAdded);
365: addSeriesData(values, COMPLETED_SERIES, velocityStart,
366: effortCompleted);
367: String[] categoryNames = createCategoryNames(velocityStart,
368: values);
369:
370: dataSet = new DefaultCategoryDataset();
371:
372: for (int seriesNum = 0; seriesNum < values.length; seriesNum++) {
373: for (int categoryNum = 0; categoryNum < values[seriesNum].length; categoryNum++) {
374: dataSet.addValue(values[seriesNum][categoryNum],
375: seriesNames[seriesNum],
376: categoryNames[categoryNum]);
377: }
378: }
379: }
380:
381: // DatasetProducer methods ===============================================
382:
383: public Object produceDataset(Map params)
384: throws DatasetProduceException {
385: return dataSet;
386: }
387:
388: public boolean hasExpired(Map params, Date since) {
389: return true;
390: }
391:
392: public String getProducerId() {
393: return TaskVelocityData.class.getName();
394: }
395:
396: // CategoryItemLinkGenerator methods ====================================
397:
398: public String generateLink(Object data, int series, Object category) {
399: return seriesNames[series];
400: }
401:
402: // CategoryToolTipGenerator methods =====================================
403:
404: public String generateToolTip(CategoryDataset categoryDataset,
405: int series, int category) {
406: return seriesPrefix[series] + values[series][category];
407: }
408: }
|